Logo of AppSignal

Menu

Ruby Magic

Concurrency Deep Dive: Multi-threading

Thijs Cadier on

In the previous edition of Ruby Magic we showed how you can implement a chat system using multiple processes. This time we’ll show you how you can do the same thing using multiple threads.

Quick recap

If you want to get a full explanation of the basic setup check out the previous article. But to remind you quickly: this is what our chat system looks like:

Chat example

We’re using the same client we used earlier:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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

The basic setup for the server is the same:

1
2
3
4
5
6
7
# server_threads.rb
# $ ruby server_threads.rb
require 'socket'

puts 'Starting server on port 2000'

server = TCPServer.open(2000)

The full source code that is used in the examples in this article is available on GitHub, so you can experiment with it yourself.

Multi-threaded chat server

Now we’re getting to the part that is different compared to the multi-process implementation. Using Multi-threading we can do multiple things at the same time with just one Ruby process. We will do this by spawning multiple threads that do the work.

Threads

A thread runs independently, executing code within a process. Multiple threads can live in the same process and they can share memory.

Some storage will be needed to store the incoming chat messages. We’ll be using a plain Array, but we also need a Mutex to make sure that only one thread changes the messages at the same time (we’ll see how the Mutex works in a bit).

1
2
mutex = Mutex.new
messages = []

Next up we start a loop in which we’ll accept incoming connections from chat clients. Once a connection has been established, we’ll spawn a thread to handle the incoming and outgoing messages from that client connection.

The Thread.new call blocks until server.accept returns something, and then yields the following block in the newly created thread. The code in the thread then proceeds to read the first line that’s sent and stores this as the nickname. Finally it starts sending and reading messages.

1
2
3
4
5
6
7
8
9
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

A mutex is an object that lets multiple threads coordinate how they use shared resources, such as an array. A thread can indicate that it needs access, and during this time other threads cannot access the shared resource.

The server reads incoming messages from the socket. It uses synchronize to get a lock on the messages store, so it can safely add a message to the messages Array.

1
2
3
4
5
6
7
8
9
10
# Read incoming messages
while incoming = read_line_from(socket)
  mutex.synchronize do
    messages.push(
      :time => Time.now,
      :nickname => nickname,
      :text => incoming
    )
  end
end

Finally, a Thread is spawned that runs continuously in a loop, to make sure all the new messages that have been received by the server are being sent to the client. Again it gets a lock so it knows that other threads are not interfering. After it’s done with a tick of the loop it sleeps for a bit and then continues.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 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

Global interpreter lock

You might have heard the story that Ruby cannot do “real” threading because of Ruby’s Global Interpreter Lock (GIL). This is partially true. The GIL is a lock around the execution of all Ruby code and prevents a Ruby process from using multiple CPUs concurrently. IO operations (such as the network connections we used in this article) operate outside of the GIL, which means you can actually achieve decent concurrency in this case.

Concluding

Now we have a chat server running within a single process using a thread per connection. This will use a lot less resources than the multi-process implementation. If you want to see the details of the code or try it you can find the example code here.

In the final article in this series we’ll implement this same chat server using a single thread and an event loop. Theoretically this should even use less resources than the thread implementation!

Latest Ruby Magic articles (see all)

Go back

Subscribe to

Ruby Magic

Magicians never share their secrets. But we do. Sign up for our Ruby Magic email series and receive deep insights about garbage collection, memory allocation, concurrency and much more.