In this short snippet I introduced our Java programmers to Ruby threads by showing them a simple example that used the Ruby classes Thread and Monitor which are very similar to their java equivalents. Consider the following example:
The last line represents the final result printed out: 14141. But if you take a look back at the code you can see that there are two threads created, t1 and t2, each of which is counting 10000 times and incrementing the same counter 'c'. You might easily expect this to result in the counter reaching 20000, but it did not. This is a simple example of a very well known problem with multiple threads accessing shared resources, and the solution is equally well known.
The key is the line '@count += 1' where the internal counter is incremented. The reason this fails to count all the way to 20000, the number of times the 'tick' method was called, is because this operation is not atomic. As indicated in the pseudo-code to the right of that line above, there are actually three separate steps to this increment operation, and it is very likely that one thread might be part way through this when the other enters. So thread t1 gets the current value of @count, but before it can store the incremented value, thread t2 gets the current value (now old value). t1 stores the incremented value, but then t2 also increments and stores the old value, so the final result is only one increment, not two. If this happened every time, the total would be only 10000, and if it never happened the total would be 20000, so we can see from the result 14141 I got above that it happened more than half the time. Just how many times this happens in reality depends on many factors, so the final total is quite unreliable.
As I said before, the solution to this is well known, but before we get to that let me comment quickly on the differences between this code and the Java equivalent. In java we would also create a Thread class and run it. But in order to pass the block of code in, we would have to instantiate a Runnable instance, with a run method, which is a lot more 'plumbing' than in Ruby. Another difference is that Ruby starts the thread on construction, while in Java you still need to call the start() method. I always thought the Java way made complete sense, but have yet to write a Ruby thread where I missed being able to call start later. In the end, a single line for constructing a thread, passing it code to run in a Ruby block and having it run directly is very concise and convenient, and certainly much easier to read.
Now for the solution to the problem. In Java we would use a concept called a 'monitor' which is built into every java Object, with the result that all objects have methods like wait() and notify(), and Java has the language keyword 'synchronized' which can be applied to methods and blocks of code. In some ways this could be seen as a case where Java has more convenience than Ruby, which does not have the Monitor built into every single object. But Ruby's overall elegance still keeps it beautifully concise. In Ruby you need to extend the class 'Monitor', or include the mixin 'MonitorMixin' if you have already extended some other class. See the example below where I've indicated with arrows the changes required to use the Monitor.
The blue arrows include Monitor support, and the red arrow is the key change to the code to make the '@count += 1' line atomic to multi-threaded access. Very easy indeed. In this case Java would have been simpler. We would not need any of the blue changes, and the red change could be done exactly as in ruby by synchronizing a block, or even simpler by synchronizing a method. But still the overall application is much shorter in Ruby, and the block synchronization is by itself simpler than java due to Ruby's cool code blocks, which make the Monitor#synchronize method look remarkably similar to the java 'synchronized' keyword. In fact this is a nice example of how Ruby has avoided the need for many keywords, and even leads to the subject of extending the language through libraries, which is beyond the topic of this snippet :-)