Trong edition trước Ruby Magic chúng tôi show implement chat system dùng multiple processes. Lần này show làm cùng việc dùng multiple threads.
Quick Recap
Muốn full explanation basic setup xem previous article. Nhắc nhanh: chat system trông như này:
Ta dùng same client 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 endBasic setup server same:
# server_threads.rb # $ ruby server_threads.rb require 'socket' puts 'Starting server on port 2000' server = TCPServer.open(2000)Full source code dùng trong examples bài này available trên GitHub, bạn tự experiment.
Multi-threaded Chat Server
Bây giờ phần khác biệt so với multi-process implementation. Dùng Multi-threading ta làm multiple things cùng lúc với chỉ một Ruby process. Làm bằng cách spawn multiple threads làm việc.
Threads
Thread chạy independently, execute code trong process. Multiple threads sống trong same process và share memory.
<img src="/images/blog/2017-04/threads.png"> Storage cần lưu incoming chat messages. Ta dùng plain Array, nhưng cũng cần Mutex đảm bảo chỉ một thread thay đổi messages cùng lúc (sẽ thấy Mutex work như thế nào).
mutex = Mutex.new messages = []Tiếp ta start loop accept incoming connections từ chat clients. Khi connection established, spawn thread handle incoming/outgoing messages từ client connection đó.
Thread.new call block cho đến server.accept return something, rồi yield block sau trong newly created thread. Code trong thread đọc first line sent lưu as nickname. Cuối cùng start send/read messages.
loop do Thread.new(server.accept) do |socket| nickname = read_line_from(socket) # Send incoming message (coming up) # Read incoming messages (coming up) end endMutex
Mutex là object cho multiple threads coordinate dùng shared resources, như array. Thread indicate cần access, trong lúc này other threads không access shared resource.
Server đọc incoming messages từ socket. Dùng synchronize get lock trên messages store, safely thêm message vào Array messages.
# Read incoming messages while incoming = read_line_from(socket) mutex.synchronize do messages.push( :time => Time.now, :nickname => nickname, :text => incoming ) end endCuối cùng, Thread spawn chạy liên tục loop, đảm bảo all new messages nhận được bởi server được gửi cho client. Lại get lock biết other threads không interfere. Sau xong tick loop sleep chút rồi continue.
# 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 endGlobal Interpreter Lock
Bạn nghe story Ruby không làm "real" threading vì Global Interpreter Lock (GIL). Partially true. GIL là lock quanh execution all Ruby code, ngăn Ruby process dùng multiple CPUs concurrently. IO operations (như network connections bài này) operate outside GIL, nghĩa là actually achieve decent concurrency trường hợp này.
Kết Luận
Bây giờ có chat server chạy trong single process dùng thread per connection. Sử dụng ít resources hơn multi-process implementation. Muốn xem code details hoặc try, tìm example code here.
Final article series này implement same chat server dùng single thread và event loop. Theoretically dùng ít resources hơn thread implementation!