Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

동시성 심층 분석:멀티스레딩

이전 버전의 Ruby Magic에서 여러 프로세스를 사용하여 채팅 시스템을 구현하는 방법을 보여주었습니다. 이번에는 여러 스레드를 사용하여 동일한 작업을 수행하는 방법을 보여드리겠습니다.

빠른 요약

기본 설정에 대한 전체 설명을 보려면 이전 기사를 확인하십시오. 하지만 빠르게 알려드리자면, 채팅 시스템은 다음과 같습니다.

이전에 사용한 것과 동일한 클라이언트를 사용하고 있습니다.

# client.rb
# $ ruby client.rb
require 'socket'
client = TCPSocket.open(ARGV[0], 2000)
 
Thread.new do
  while line = client.gets
    puts line.chop
  end
end
 
while input = STDIN.gets.chomp
  client.puts input
end

서버의 기본 설정은 동일합니다.

# server_threads.rb
# $ ruby server_threads.rb
require 'socket'
 
puts 'Starting server on port 2000'
 
server = TCPServer.open(2000)

이 기사의 예제에 사용된 전체 소스 코드는 GitHub에서 사용할 수 있으므로 직접 실험해 볼 수 있습니다.

다중 스레드 채팅 서버

이제 우리는 다중 프로세스 구현과 다른 부분에 도달했습니다. 멀티 스레딩 사용 하나의 Ruby 프로세스로 동시에 여러 작업을 수행할 수 있습니다. 작업을 수행하는 여러 스레드를 생성하여 이를 수행합니다.

스레드

스레드는 독립적으로 실행되어 프로세스 내에서 코드를 실행합니다. 여러 스레드가 동일한 프로세스에 있을 수 있으며 메모리를 공유할 수 있습니다.

<img src="/images/blog/2017-04/threads.png">

수신 채팅 메시지를 저장하려면 일부 저장 공간이 필요합니다. 우리는 일반 Array를 사용할 것입니다. 하지만 Mutex도 필요합니다. 한 스레드만 동시에 메시지를 변경하도록 합니다(Mutex 조금 후에 작동합니다.

mutex = Mutex.new
messages = []

다음으로 채팅 클라이언트로부터 들어오는 연결을 수락하는 루프를 시작합니다. 연결이 설정되면 해당 클라이언트 연결에서 들어오고 나가는 메시지를 처리하기 위해 스레드를 생성합니다.

Thread.new server.accept까지 호출 차단 무언가를 반환하고 새로 생성된 스레드에서 다음 블록을 생성합니다. 그런 다음 스레드의 코드는 전송된 첫 번째 줄을 읽고 이를 닉네임으로 저장합니다. 마침내 메시지를 보내고 읽기 시작합니다.

loop do
  Thread.new(server.accept) do |socket|
    nickname = read_line_from(socket)
 
    # Send incoming message (coming up)
 
    # Read incoming messages (coming up)
  end
end

뮤텍스

뮤텍스는 여러 스레드가 배열과 같은 공유 리소스를 사용하는 방법을 조정할 수 있도록 하는 개체입니다. 스레드는 액세스가 필요함을 나타낼 수 있으며 이 시간 동안 다른 스레드는 공유 리소스에 액세스할 수 없습니다.

서버는 소켓에서 들어오는 메시지를 읽습니다. synchronize를 사용합니다. 메시지 저장소를 잠그기 위해 메시지 Array에 메시지를 안전하게 추가할 수 있습니다. .

# Read incoming messages
while incoming = read_line_from(socket)
  mutex.synchronize do
    messages.push(
      :time => Time.now,
      :nickname => nickname,
      :text => incoming
    )
  end
end

마지막으로 Thread 서버에서 수신한 모든 새 메시지가 클라이언트로 전송되고 있는지 확인하기 위해 루프에서 지속적으로 실행되는 이 생성됩니다. 다시 잠금을 가져서 다른 스레드가 방해하지 않는다는 것을 알 수 있습니다. 루프의 틱으로 완료되면 잠시 잠자고 계속됩니다.

# Send incoming message
Thread.new do
  sent_until = Time.now
  loop do
    messages_to_send = mutex.synchronize do
      get_messages_to_send(nickname, messages, sent_until).tap do
        sent_until = Time.now
      end
    end
    messages_to_send.each do |message|
      socket.puts "#{message[:nickname]}: #{message[:text]}"
    end
    sleep 0.2
  end
end

글로벌 인터프리터 잠금

Ruby의 GIL(Global Interpreter Lock) 때문에 Ruby가 "실제" 스레딩을 수행할 수 없다는 이야기를 들었을 것입니다. 이것은 부분적으로 사실입니다. GIL은 모든 Ruby 코드 실행에 대한 잠금 장치이며 Ruby 프로세스가 여러 CPU를 동시에 사용하는 것을 방지합니다. IO 작업(예:이 기사에서 사용한 네트워크 연결)은 GIL 외부에서 작동하므로 이 경우 실제로 적절한 동시성을 얻을 수 있습니다.

결론

이제 연결당 스레드를 사용하여 단일 프로세스 내에서 실행 중인 채팅 서버가 있습니다. 이것은 다중 프로세스 구현보다 훨씬 적은 리소스를 사용합니다. 코더의 세부 정보를 보고 싶거나 시도해보고 싶다면 여기에서 예제 코드를 찾을 수 있습니다.

이 시리즈의 마지막 기사에서는 단일 스레드와 이벤트 루프를 사용하여 동일한 채팅 서버를 구현합니다. 이론적으로 이것은 스레드 구현보다 적은 리소스를 사용해야 합니다!