15

In Ruby, this code is not threadsafe if array is modified by many threads:

array = []
array << :foo # many threads can run this code

Why is the << operation not thread safe?

Grijesh Chauhan
  • 57,103
  • 20
  • 141
  • 208
Sławosz
  • 11,187
  • 15
  • 73
  • 106

5 Answers5

11

Actually using MRI (Matz's Ruby implementation) the GIL (Global Interpreter Lock) makes any pure C-function atomic.

Since Array#<< is implemented as pure C-code in MRI, this operation will be atomic. But note this only applies to MRI. On JRuby this is not the case.

To completely understand what is going on I suggest you read these two articles, which explains everything very well:

Nobody Understands the GIL
Nobody Understands the GIL - part 2

Casper
  • 33,403
  • 4
  • 84
  • 79
9

array is your program variable when you apply an operation like << to it. It happens in three-steps:

  • The variable is first copied into a CPU register.
  • The CPU performs computations.
  • The CPU writes back the result to variable memory.

So this high-level single-operation is performed in three steps. In between these steps, due to thread-context switching, other thread may read the same (old) value of the variable. That's why it's not an atomic operation.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Grijesh Chauhan
  • 57,103
  • 20
  • 141
  • 208
  • 2
    you may like to read: [**Atomic Operations in Ruby**](http://moonbase.rydia.net/mental/blog/programming/atomic-operations-in-ruby.html) – Grijesh Chauhan Jul 20 '13 at 20:27
  • 1
    did you read this? [Updated with Jörg's Sept 2011 comment](http://stackoverflow.com/questions/56087/does-ruby-have-real-multithreading/57802#57802) – Grijesh Chauhan Jul 20 '13 at 20:31
  • @theTinMan Lots of thanks! my English is poor :( but I notice carefully your and previous edited to my answer I will improve in next answer. Thanks! – Grijesh Chauhan Jul 20 '13 at 21:51
  • 2
    That's ok. No thanks are needed. It's part of how we give back to the community. – the Tin Man Jul 20 '13 at 21:56
  • This is a array operation in C or assembly. In Ruby, it also involves method lookups, argument handling, etc. – Linuxios Jul 21 '13 at 03:24
  • @GrijeshChauhan Array#append of MRI is atomic at least? right? https://www.jstorimer.com/blogs/workingwithcode/8085491-nobody-understands-the-gil – chobo Dec 07 '18 at 02:39
9

If you have multiple threads accessing the same array, use Ruby's built-in Queue class. It nicely handles producers and consumers.

This is the example from the documentation:

require 'thread'

queue = Queue.new

producer = Thread.new do
  5.times do |i|
    sleep rand(i) # simulate expense
    queue << i
    puts "#{i} produced"
  end
end

consumer = Thread.new do
  5.times do |i|
    value = queue.pop
    sleep rand(i/2) # simulate expense
    puts "consumed #{value}"
  end
end

consumer.join
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
1

Because Ruby is a very high level language, nothing is really atomic at the OS level. Only very simple assembly operations are atomic at the OS level (OS dependant), and every Ruby operation, even a simple 1 + 1 corresponds to hundreds or thousands of assembly instructions executed, such as method lookups, garbage collection, object initialization, scope calculations, etc.

If you need to make operations atomic, use Mutexes.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Linuxios
  • 34,849
  • 13
  • 91
  • 116
0

Just riffing off of @Linuxios and @TheTinMan: high-level language (HLL) operations in general are not atomic. Atomicity is (generally) not an issue in single-threaded programs. In multi-threaded programs, you (the programmer) have to reason about it at a much higher granularity than a single HLL operation, so having individual HLL operations that are atomic doesn't actually help you that much. On the flip side, although making an HLL operation atomic takes only a few machine instructions before and after—at least on modern hardware—the static (binary size) and dynamic (execution time) overheads add up. Even worse, explicit atomicity pretty much disables all optimization because compilers cannot move instructions across atomic operations. No real benefit + significant cost = non-starter.

Amir Roth
  • 101
  • 2