5 ms·
Ruby core classes aren't thread safe
- wuest 14y agoNice write-up. It's easy to fall into the trap of assuming that things are "thread-safe" when writing under the iron fist of a Global Interpreter Lock. Ruby threads seems to be a fairly narrow topic on which to base a book; I'll look forward to seeing what all the book covers.
- niggler 14y agoDoes the spec mandate thread safety? (I guess I should ask if there's a spec or is MRI the reference implementation)
- jstorimer 14y agoAFAIK there is no spec. MRI is the reference implementation, but many things are experimental or intentionally unspecified. Given that MRI ships with a GIL, the only core classes that are intentionally aware of multi-threading concerns are Mutex, ConditionVariable, and Queue.
- masklinn 14y agoEven a thread-aware collection where collection methods are synchronized on an internal lock (as in Java 1.0 — this was quickly dropped as it's effectively useless) wouldn't help here: having `[]` and `[]=` safe will not make calling `[]`, performing an addition and then calling `[]=` safe.
- aardvark179 14y agoA GIL does not mean classes should ignore concurrency concerns, it's still possible to get odd behaviour from things like hash table implementations in a GIL based interpreter when inserting objects as you may end up thread switching mid operation. What saves you most of the time is that it isn't worth switching threads too often so normally you get lucky.
- steveklabnik 14y agoThere is an ISO spec, but it's not really relevant to the future of the language.
- exabrial 14y agoDoes ruby have a spec? If not, I don't really see this as a problem... synchronization, especially in a dynamic language that's already really slow, would just add more overhead.
- chc 14y agoIt does, though it's the sort that's mainly derived from the canonical implementation -- see RubySpec.
- pieter 14y agoThis still doesn't explain why the MRI implementation is accidentally threadsafe. Why doesn't the interpreter switch threads after reading the value from the hash but before storing the updated value?
- deleted 14y ago[deleted]
- sabat 14y agoDue to the Global Interpreter Lock (GIL), your whole script is wrapped in one giant mutex. That means that you don't have code running in true parallel, so the data is not corrupted as it is in JRuby and Rubinius (which both implement real, true, parallel threads).
- sabat 14y agoNot sure why someone chose to downvote this to 0, but here's how the OP put it: The global lock is a feature of MRI that basically wraps a big mutex around all of your code. That's right, even if you're using multiple threads on a multi-core CPU, if your code is running on MRI it will not run in parallel.
- masklinn 14y agoOP is wrong, MRI will release the GIL (and yield to other threads) on IO and after running a bit (which can yield convoy effect actually lowering the performances). C extensions can also release the GIL when not manipulating VM structures. Technically the GIL is there to protect the VM's internal state, so its operational semantics are that it's taken and released for each bytecode instruction. However the efficiency would stink (short of awesome automatic lock elision or merging), so GIL-based VMs usually have a higher threshold, either based on some sort of instructions count (which may or may not map 1:1 to bytecode instructions) or an internal "wall clock" timer. I think MRI's the latter but don't quote me on it.
- jstorimer 14y ago
- masklinn 14y agoUnder the semantics described here, Java or C# core classes aren't "thread-safe" either and I'd expect the vast majority of standard libraries to completely fail the test (potential winners: Clojure using an immutable collection bound on an atom, as they have compare-and-swap semantics; and probably Haskell somehow), the example code requires performing the following actions atomically: * Loading an instance-local collection * Fetching a numeric value from the collection * Incrementing or decrementing the numeric value * Putting the incremented value back Even if the collection is "synchronized" (each method call takes a collection-local lock), because the value is altered outside the collection there's no way for the change to be atomic unless it's wrapped in a transaction block or protected by a lock. As far as I can think, the only ways for core classes to "be thread-safe" considering the example (keeping mutable collections semantics) would be to either have collections dedicated specifically to the operation such as Java6's AtomicIntegerArray (http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/atomic/AtomicIntegerArray.html http://docs.oracle.com/javase/6/docs/api/java/util/concurren...), or to have the collection apply the operation internally e.g. Hash#apply(key, &operation) used roughly like this: def decrease item @stock.apply(item) {|from| from - 1} end
- dons 14y ago> and probably Haskell somehow By design - pure values are always thread safe.
- masklinn 14y agoPure values are not really relevant: you still need to update a binding somewhere to synchronize, and that's sufficient for your race. The clojure example wouldn't be safe if atoms weren't compare-and-set: have collection state A, thread one applies A->B, thread two applies A->C, the two threads set the atom (atomically but not CAS) and an increment has been lost even though all values are pure.
- tomp 14y agoYes, but the point is, that in a functional language, hash-tables need not be thread-safe, as they are immutable. Only the variable binding has to be transactional. In a functional language, the code would be: transaction { local a = !x local b = copy a with b[i] = a[i] + 1 x := b } where `!x` is referencing a transactional variable and `:=` is setting it.
- tomp 14y agoIt's not that Arrays are not thread safe; it's just that the code was written in a non-thread-safe way. Writing x[i] -= 1 actually means x[i] = x[i] - 1 So, there's a read, a subtraction, and a write, and they all happen sequentially. Since they are not in a transaction or protected by a mutex, nothing guarantees that other thread don't mutate `x[i]` in the mean time. This has nothing to do with Ruby, and nothing to do with multicore, either. Even on a single CPU core, threads might interleave and cause unexpected behavior.
- SeoxyS 14y agoI came to this comment thread specifically to point this out, but you beat me to it. It has nothing to do thread safety, and everything to do with atomicity. This is not a single atomic operation, but rather three atomic (and thread-safe) operations which are bound together with the assumption that the entire thing is atomic when it is not. # you might as well imagine this happening original = x[i] new_val = original - 1 x[i] = new_val Which is why you need to use a mutex to make the entire operation atomic, at a small performance cost. The Objective-C compiler actually adds a nice bit of syntax for this, you can simply wrap your code in @synchronized: @synchronized(self) { int original = x[i]; int new_val = original - 1; x[i] = new_val; }
- danabramov 14y agoIf I understand correctly, this corresponds to `lock` in C#: lock (_gate) { x[i] --; } At least in C# it is considered preferable to lock on a private field, as opposed to locking on `this`, so nobody else also locks on your instance, potentially causing a deadlock. I suppose this applies to `@synchronized` in Objective C as well. I like that .NET Framework also provides some useful atomic methods, including this one: Interlocked.Decrement (ref x[i]); They come in handy.
- niggler 14y agoThat's not quite true. The language spec can mandate that the -= be atomic (the X86 equivalent is mandating a LOCK).
- incidently 14y agoThe article seems to be wrong in several aspects ... First, the issue described has nothing to do with arrays; the same problem happens when using a plain number: class Inventory def initialize(nb) @nb_items = nb end def decrease @nb_items -= 1 end def nb_items @nb_items end end @inventory = Inventory.new(4000) threads = Array.new 400.times do threads << Thread.new do 10.times do @inventory.decrease end end end threads.each(&:join) puts @inventory.nb_items Second, the mutex in the OP's code synchronizes the whole block passed to a thread, i.e. there's no parallelism at all (the second thread waits until the first one finishes, and so on). It should rather be something like: class Inventory def initialize(nb) @nb_items = nb @lock = Mutex.new end def decrease @lock.synchronize do @nb_items -= 1 end end def nb_items @nb_items end end @inventory = Inventory.new(4000) threads = Array.new 400.times do threads << Thread.new do 10.times do @inventory.decrease end end end threads.each(&:join) puts @inventory.nb_items
- jstorimer 14y agoHi. OP here. Thanks for the comments about atomicity vs. thread-safety. Absolutely on point. The article started out demonstrating what happened with concurrent Array mutation, but then I put in that += operation and didn't address it. Sorry for not making the distinction. Atomicity is absolutely a different issue than a thread-safe collection. I'm publishing something new tomorrow that addresses this point. To bring things back to code, the point I was originally trying to make is that this code is not thread-safe. array = [] threads = [] 10.times do threads << Thread.new do 100.times { array.push(rand) } end end threads.each(&:join) # 10 threads each inserted 100 values, result should be 1000 puts array.size Specifically, too many Ruby programmers won't think twice about this operation not being thread-safe: array.push(item) But there's no such guarantee. This is demonstrated nicely when this code example is run on an implementation with no global lock, try it on JRuby.
- JulianMorrison 14y agoThere is one core library class that's thread safe, and it's Queue. Otherwise, take a lock, or don't share data.
- marshray 14y agoLet's ban this term "thread safe" and instead say what we really mean. But first, let's figure out what we really mean.
- deleted 14y ago[deleted]