Computer >> Máy Tính >  >> Lập trình >> Ruby

Concurrency Deep Dive:Đa luồng

Trong phiên bản trước của Ruby Magic, chúng tôi đã chỉ ra cách bạn có thể triển khai hệ thống trò chuyện bằng cách sử dụng nhiều quy trình. Lần này, chúng tôi sẽ chỉ cho bạn cách bạn có thể làm điều tương tự bằng cách sử dụng nhiều chuỗi.

Tóm tắt nhanh

Nếu bạn muốn được giải thích đầy đủ về cách thiết lập cơ bản, hãy xem bài viết trước. Nhưng xin nhắc bạn một cách nhanh chóng:đây là giao diện của hệ thống trò chuyện của chúng tôi:

Chúng tôi đang sử dụng cùng một ứng dụng mà chúng tôi đã sử dụng trước đó:

# 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

Thiết lập cơ bản cho máy chủ giống nhau:

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

Mã nguồn đầy đủ được sử dụng trong các ví dụ trong bài viết này có sẵn trên GitHub, vì vậy bạn có thể tự thử nghiệm với nó.

Máy chủ trò chuyện đa luồng

Bây giờ chúng ta đang đi đến phần khác biệt so với việc thực hiện nhiều quy trình. Sử dụng Đa luồng chúng ta có thể làm nhiều việc cùng lúc chỉ với một quy trình Ruby. Chúng tôi sẽ thực hiện việc này bằng cách tạo ra nhiều chuỗi thực hiện công việc.

Chủ đề

Một luồng chạy độc lập, thực thi mã trong một quy trình. Nhiều luồng có thể tồn tại trong cùng một quy trình và chúng có thể chia sẻ bộ nhớ.

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

Sẽ cần một số bộ nhớ để lưu trữ các tin nhắn trò chuyện đến. Chúng tôi sẽ sử dụng Array đơn giản , nhưng chúng tôi cũng cần một Mutex để đảm bảo rằng chỉ một chuỗi thay đổi các thông báo cùng một lúc (chúng ta sẽ xem cách Mutex hoạt động một chút).

mutex = Mutex.new
messages = []

Tiếp theo, chúng tôi bắt đầu một vòng lặp trong đó chúng tôi sẽ chấp nhận các kết nối đến từ các ứng dụng khách trò chuyện. Khi kết nối đã được thiết lập, chúng tôi sẽ tạo ra một chuỗi để xử lý các tin nhắn đến và đi từ kết nối máy khách đó.

Thread.new khối lệnh gọi cho đến khi server.accept trả về một cái gì đó, và sau đó chèn khối sau trong chuỗi mới được tạo. Sau đó, mã trong chuỗi sẽ tiếp tục đọc dòng đầu tiên được gửi và lưu trữ nó dưới dạng biệt hiệu. Cuối cùng, nó bắt đầu gửi và đọc tin nhắn.

Vòng lặp
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

Mutex

Mutex là một đối tượng cho phép nhiều luồng điều phối cách chúng sử dụng tài nguyên được chia sẻ, chẳng hạn như một mảng. Một chuỗi có thể chỉ ra rằng nó cần quyền truy cập và trong thời gian này các chuỗi khác không thể truy cập tài nguyên được chia sẻ.

Máy chủ đọc tin nhắn đến từ ổ cắm. Nó sử dụng synchronize để có được một khóa trên kho lưu trữ tin nhắn, vì vậy nó có thể thêm một tin nhắn vào Array một cách an toàn .

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

Cuối cùng, một Thread được tạo ra chạy liên tục trong một vòng lặp, để đảm bảo rằng tất cả các thông báo mới mà máy chủ nhận được đang được gửi đến máy khách. Một lần nữa, nó nhận được một khóa để nó biết rằng các luồng khác không can thiệp vào. Sau khi hoàn thành với một dấu tích của vòng lặp, nó sẽ ngủ một chút và sau đó tiếp tục.

# 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

Khóa thông dịch viên toàn cầu

Bạn có thể đã nghe câu chuyện rằng Ruby không thể thực hiện phân luồng "thực" vì Khóa thông dịch viên toàn cầu của Ruby (GIL). Điều này đúng một phần. GIL là một khóa xung quanh việc thực thi tất cả mã Ruby và ngăn quá trình Ruby sử dụng nhiều CPU đồng thời. Các hoạt động IO (chẳng hạn như các kết nối mạng mà chúng tôi sử dụng trong bài viết này) hoạt động bên ngoài GIL, có nghĩa là bạn thực sự có thể đạt được đồng thời tốt trong trường hợp này.

Kết luận

Bây giờ chúng tôi có một máy chủ trò chuyện chạy trong một quy trình duy nhất bằng cách sử dụng Athread trên mỗi kết nối. Điều này sẽ sử dụng ít tài nguyên hơn nhiều so với thực hiện nhiều quy trình. Nếu bạn muốn xem chi tiết của trình mã, hãy thử nó, bạn có thể tìm mã ví dụ tại đây.

Trong bài viết cuối cùng của loạt bài này, chúng tôi sẽ triển khai cùng một máy chủ trò chuyện này bằng cách sử dụng một chuỗi duy nhất và một vòng lặp sự kiện. Về mặt lý thuyết, điều này thậm chí sẽ sử dụng ít tài nguyên hơn so với việc triển khai luồng!