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

동시성 심층 분석:다중 프로세스

동시성 마스터에 대한 이전 Ruby Magic 기사에서 우리는 Ruby 개발자로서 사용할 수 있는 동시성을 달성하는 세 가지 방법을 소개했습니다. 이 기사는 각 방법에 대해 자세히 살펴보는 3부작 시리즈의 첫 번째 기사입니다.

첫 번째:다중 프로세스 . 이 방법을 사용하면 마스터 프로세스가 여러 작업자 프로세스로 분기됩니다. 작업자 프로세스가 실제 작업을 수행하고 마스터가 작업자를 관리합니다.

<블록 인용>

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

채팅 시스템을 구축하자!

채팅 시스템을 구축하는 것은 동시성에 뛰어드는 좋은 방법입니다. 여러 클라이언트와의 연결을 유지할 수 있는 채팅 시스템의 서버 구성 요소가 필요합니다. 이렇게 하면 한 클라이언트에서 받은 메시지를 연결된 다른 모든 클라이언트에 배포할 수 있습니다.

채팅 서버는 왼쪽 탭에서 실행 중입니다. 오른쪽 탭에서 실행 중인 두 개의 채팅 클라이언트가 있습니다. 클라이언트가 보낸 모든 메시지는 다른 모든 클라이언트가 수신합니다.

채팅 클라이언트

이 기사는 채팅 서버에 초점을 맞추고 있지만, 이 서버와 통신하려면 먼저 채팅 클라이언트가 필요합니다. 다음 코드는 매우 간단한 클라이언트가 될 것입니다. (더 완전한 예제는 GitHub에서 찾을 수 있습니다.)

# 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

클라이언트는 포트 2000에서 실행되는 서버에 대한 TCP 연결을 엽니다. 연결되면 puts 서버가 보내는 모든 것이므로 채팅은 터미널 출력에서 ​​볼 수 있습니다. 마지막으로, 입력한 줄을 서버로 보내는 while 루프가 있습니다. 이 루프는 연결된 다른 모든 클라이언트로 보냅니다.

채팅 서버

이 예에서 클라이언트는 다른 클라이언트와 통신하기 위해 채팅 서버에 연결합니다. 세 가지 동시성 접근 방식 모두에 대해 Ruby 표준 라이브러리의 동일한 TCP 서버를 사용합니다.

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

이 시점까지 코드는 세 가지 동시성 모델 모두에 대해 동일합니다. 모든 모델의 채팅 서버는 두 가지 시나리오를 처리해야 합니다.

  1. 고객의 새로운 연결을 수락합니다.
  2. 클라이언트로부터 메시지를 수신하고 다른 모든 클라이언트에게 보냅니다.

다중 ​​프로세스 채팅 서버

다중 프로세스 채팅 서버로 이 두 가지 시나리오를 처리하기 위해 클라이언트 연결당 프로세스를 생성할 것입니다. 이 프로세스는 해당 클라이언트에 대해 보내고 받는 모든 메시지를 처리합니다. 원래 서버 프로세스를 분기하여 이러한 프로세스를 만들 수 있습니다.

포킹 프로세스

fork 메서드를 호출하면 프로세스가 있는 것과 똑같은 상태로 현재 프로세스의 복사본이 생성됩니다.

분기된 프로세스에는 자체 프로세스 ID가 있으며 top과 같은 도구에서 별도로 표시됩니다. 또는 활동 모니터. 다음과 같습니다.

시작하는 프로세스를 마스터 프로세스라고 하고 마스터 프로세스에서 분기된 프로세스를 작업자 프로세스라고 합니다.

이 새로 분기된 작업자 프로세스는 완전히 별개의 프로세스이므로 마스터 프로세스와 메모리를 공유할 수 없습니다. 그들 사이에 소통할 무언가가 필요합니다.

유닉스 파이프

프로세스 간 통신을 위해 Unix 파이프를 사용합니다. Unix 파이프는 두 프로세스 간에 양방향 바이트 스트림을 설정하고 이를 사용하여 한 프로세스에서 다른 프로세스로 데이터를 보낼 수 있습니다. 운 좋게도 Ruby는 이 파이프 주위에 멋진 래퍼를 제공하므로 바퀴를 다시 발명할 필요가 없습니다.

다음 예제에서 우리는 읽기와 쓰기 끝이 있는 Ruby에서 파이프를 설정하고 fork 마스터 프로세스. fork에 전달되는 블록 내의 코드 분기된 프로세스에서 실행 중입니다. 원래 프로세스는 이 블록 이후에 계속됩니다. 그런 다음 분기된 프로세스에서 원래 프로세스로 메시지를 씁니다.

reader, writer = IO.pipe
 
fork do
  # This is running in the forked process.
  writer.puts 'Hello from the forked process'
end
 
# This is running in the original process, it will puts the
# message from the forked process.
puts reader.gets

파이프를 사용하면 프로세스가 서로 완전히 격리된 경우에도 개별 프로세스 간에 통신할 수 있습니다.

채팅 서버의 구현

먼저 모든 클라이언트와 "작성자"(파이프의 쓰기 끝)에 대한 파이프를 추적하도록 배열을 설정하여 클라이언트와 통신할 수 있습니다. 그런 다음 클라이언트에서 들어오는 모든 메시지가 다른 모든 클라이언트로 전송되는지 확인합니다.

client_writers = []
master_reader, master_writer = IO.pipe
 
write_incoming_messages_to_child_processes(master_reader, client_writers)

write_incoming_messages_to_child_processes의 구현을 찾을 수 있습니다. 작동 방식에 대한 세부 정보를 보려면 GitHub에서.

새 연결 수락

들어오는 연결을 수락하고 파이프를 설정해야 합니다. 새 작성자는 client_writers에 푸시됩니다. 정렬. 주 프로세스는 배열을 반복하고 파이프에 작성하여 각 작업자 프로세스에 메시지를 보낼 수 있습니다.

그런 다음 마스터 프로세스를 분기하면 분기된 작업자 프로세스 내의 코드가 클라이언트 연결을 처리합니다.

loop do
  while socket = server.accept
    # Create a client reader and writer so that the master
    # process can write messages back to us.
    client_reader, client_writer = IO.pipe
 
    # Put the client writer on the list of writers so the
    # master process can write to them.
    client_writers.push(client_writer)
 
    # Fork child process, everything in the fork block
    # only runs in the child process.
    fork do
      # Handle connection
    end
  end
end

클라이언트 연결 처리

클라이언트 연결도 처리해야 합니다.

분기된 프로세스는 클라이언트에서 닉네임을 가져오는 것으로 시작됩니다(클라이언트는 기본적으로 닉네임을 보냅니다). 그 후 write_incoming_messages_to_client에서 스레드를 시작합니다. 메인 프로세스의 메시지를 수신합니다.

마지막으로 분기된 프로세스는 들어오는 메시지를 수신 대기하고 마스터 프로세스로 보내는 루프를 시작합니다. 마스터 프로세스는 다른 작업자 프로세스가 메시지를 수신하도록 합니다.

nickname = read_line_from(socket)
puts "#{Process.pid}: Accepted connection from #{nickname}"
 
write_incoming_messages_to_client(nickname, client_reader, socket)
 
# Read incoming messages from the client.
while incoming = read_line_from(socket)
  master_writer.puts "#{nickname}: #{incoming}"
end
 
puts "#{Process.pid}: Disconnected #{nickname}"

작동하는 채팅 시스템

이제 전체 채팅 시스템이 작동합니다! 그러나 보시다시피 다중 처리를 사용하는 프로그램을 작성하는 것은 상당히 복잡하고 많은 리소스를 사용합니다. 매우 튼튼하다는 것이 장점입니다. 자식 프로세스 중 하나가 충돌하면 나머지 시스템은 계속 작동합니다. 예제 코드를 실행하고 kill -9 <process-id>를 실행하여 시도할 수 있습니다. 프로세스 중 하나에서(서버의 로그 출력에서 ​​프로세스 ID를 찾을 수 있음).

다음 기사에서는 스레드만 사용하여 동일한 채팅 시스템을 구현하므로 하나의 프로세스와 더 적은 메모리를 사용하여 동일한 기능을 가진 서버를 실행할 수 있습니다.