Escolar Documentos
Profissional Documentos
Cultura Documentos
A few years ago, there used to be a very easy way to optimize code. If you found out that your processor-heavy code was running a bit slower than you wanted it to, the simple solution was just to wait for the next hardware iteration, which would magically amp up the clock rate on the !"s and your app would suddenly run faster. "nfortunately, those times are now past. #ecause of a little something called $transition energy% inside the logic gates &the incredibly tiny electronically controlled $switches% that run processors', it has become close to impossible to push the clock rate any further at a reasonable price. #ut, a smart solution was (uickly uncovered. Instead of trying to push one processor to run at break neck speed, we instead spread out the workload over multiple processors. )hat an excellent idea* +or people that write software, understanding concurrency and using it well suddenly becomes incredibly important. In order to scale up with the hardware, your code ,"-. use concurrency* .his seemed simple enough to everyone at first/ it couldn0t be that hard right1 )ell, it was. .he mechanism that is &arguably' most popular for concurrency is threads, which are immensely complicated. A few small mistakes here and there can wreak havoc. If you0re really interested in this sort of stuff, I0d recommend you read up a little. If you were brave enough to click a a few of those links, it shouldn0t take long to realize that thread management isn0t always a walk in the park. onsider, as an example, deadlocking.
(dead) Locking
-uppose you have two people that have a single copy of a book that they both want to read. Assuming that they won0t both be able to read the same book at the same time, so one person will have to wait until the other is done reading it. 2ow, consider a situation where each person thinks that the other person isn0t done reading the book and, as such, decides not to start reading it. 2ow, neither person reads the book. .his situation is known as a deadlock. If your program has two $threads% &which are analogous to the two people we discussed' and they both write to a file called $steve%. #oth of them cannot write to the file at the same time, so the file is $locked% while one thread is writing to it. A similar situation can be present for shared variables. 3f course, this situation works just great, but the problem occurs when there is some error that causes the file to be locked for both threads. If you0ve written any amount of thread driven code, you know this happens a lot. )hen it does, it is a complete pain to track down and fix.
Race Conditions
In deadlocking, the two threads wait around for each other forever. 4owever, deadlocking has a cousin that is just as bad, if not worse.
6ace conditions occur when two threads try to access and write to a variable at the same time. -o, both threads read the variable, then each one races to see who can write to the variable first7last. .his causes all kinds of problems, because it might cause one thread0s changes to the variable to be completely hammered by the other.
Solution?
.he two problems mentioned above are just the tip of the iceberg 8 there0s all kinds of other issues to be dealt with when using threads. Around the 59:;<s, people began thinking about where all of these problems were really coming from and how to deal with them. .hey found that nearly all of these issues are caused by sharing of state &i.e. variables, files, etc.' and locking. If you forget to lock a single shared resource, there0s going to be a boatload of trouble waiting for you with threads. ,ultiple solutions were proposed, one of which is evented I73, which means eventmachine for 6ubyists. Along with that, some bright academics came up with a new model that had taken some ideas from (uantum physics. Instead of sharing state, the entire concurrency system would be based on passing messages. .hey called this the actor model.
Actor Model
In the actor model, each object is an actor. =ach actor is meant to send and receive messages to other actors and may also create other actors if need be. .he main point around which all of this pivots is the fact that all communications can be asynchronous and no state is shared between the actors. .hat means that messages can be being sent while others are being received. Also, when an actor sends a message it doesn0t always have to wait for a response. -tate that is to be shared between processes is done entirely through means of messaging. If you didn0t understand a large portion of this, that0s perfectly fine, just follow along. 3f course, none of this happens by itself. A library called & elluloid'>http?77celluloid.io7@ makes this stuff happen and that0s what we0ll be using*
Celluloid
elluloid brings the actor model to 6uby, making writing concurrent applications incredibly easy. +irst of all, elluloid comes with built in deadlock protection, because all of the messaging between actors between is handled in a such a way that deadlocking is darn near impossible, as long as you0re not doing something crazy or messing with native &i.e. ' code.
If you0re familiar with =rlang &it is okay if you0re not', elluloid borrows one of its most important ideas? fault tolerance. elluloid automatically restarts and handles crashed actors, so you don0t have to worry about every last thing that could go wrong. .here0s all kinds of other features &linking, futures, etc.' that make threading a breeze, but there a few things to keep in mind.
The GIL
.he $regular% or vanilla ruby that we0re all used to is backed by either ,6I or BA6C, which are different types of interpreters7vitual machines for 6uby. 2ow, there is, debatably, a problem with this interpreter. .he thing is, all threads inside ,6I7BA6C aren0t really concurrent 8 everything is run under a single thread. .his is called the Dlobal Interpreter Eock. 6uby isn0t the only language with an interpreter that has this 8 so does !ython and don0t even get me started on threading with !4!. )hen a new thread is created, the result is the computation that is performed isn0t actually performed at the same time as other stuff is being done. An illusion is created that makes the user think that this is what is happening. +ortunately, there is a solution. Fust use a different interpreter* .ake your pick?
&F6uby'>jruby.org@ &6ubini.us'>http?77rubini.us7@
2ote that if you do choose to go with one of the above interpreters, make sure you are operating in 5.9 mode in order to be compaitible with elluloid.
Diving In
Eet0s get started with elluloid by writing a small actor 8 let0s make it read a file we tell it to, and then display the results when they are wanted.
require 'celluloid' class FilePutter include Celluloid def initialize(filename) @filename = filename end def load_file @file_contents = File.read @filename end def print p @file_contents end end
6unning that &with your choice of interpreter', you should get a dump of the kernel log &of course, assuming you0re using a !3-IH system, )indows users can replace that line with any file of their choice' followed by a message from elluloid telling you that two actors have been terminated. Alright, so, what just happened1 )e defined the +ile!utter actor and created an instance of it, which elluloid automatically pushes into its own thread* .here0s no difference in how we are calling the methods/ it is just like we would for a regular class, and it dumps the contents of a file. +irst, calling load_file loads the file, then we proceed on to printing the contents. 2ot too complicated. #ut, one thread isn0t all that interesting/ how about five1 =asy enough?
require 'celluloid' class FilePutter include Celluloid def initialize(filename) @filename = filename end def load_file @file_contents = File.read @filename end def print p @file_contents end end files = #"/ ar/lo!/"ernel.lo!"$ "/ ar/lo!/s%stem.lo!"$ "/ ar/lo!/ppp.lo!"$ "/ ar/lo!/secure.lo!"& files.eac' do (file( fp = FilePutter.new file fp.load_file fp.print end
And, just like that, we0ve created five threads which each read the files in the $files% array. #ut, all of the methods we0ve called so far have been called syncronously, meaning that we have to wait for them to end before proceeding. )hat if we just pushed them to the side and moved on1
.his is where it gets interesting. +irst of all, we combined the loading of the file and printing the contents into one method, namely load_file_and_print. .hen, notice inside the loop over the files array, we don0t call load_file_and_print, instead, we call async.load_file_and_print.
Wra
ing It !
)hen we call the given method with .async., elluloid runs that call asynchronously, allowing our program to move right along without waiting for the file to load or the printing to occur. .he asynchronous message delivering isn0t perfect. .he message might not be delivered, the actor might not respond, etc. #ut, how do you figure out when this happens1 Also, threads are never really completely independent of each other 8 how do you have them talking to each other1 .his, and, several other important actor model features and niceties are coming in part II, so, stay tuned* If you felt this article went a bit too slow, you0ll be satisfied with !art II ?' Please ask (uestions if you have any in the comments section.
.his is the second article in the three-part series. If you missed the first one, you can find it here elluloid has a ton more awesome tools to make concurrent programming incredibly easy in 6uby. Eet0s take a look at them.
Futures
.here are times when we don0t just want to discard the return value of a method we0ve called on an actor/ instead, we might want to use it somewhere else. +or that, elluloid provides futures. .he best way to learn about them is to see them in action. )e0ll write a small script that computes the -4A5 checksum of an array of files, then outputs them to the console. )ithout further ado, here it is?
require 'celluloid' require 'di!est/s'a)' class *+,Putter include Celluloid def initialize(filename) @filename = filename end def output(c'ec"sum_future) puts "-.@filename/ 0 -.c'ec"sum_future. alue/" end def c'ec"sum @file_contents = File.read(@filename) 1i!est22*+,).'e3di!est @file_contents end end files = #"/ ar/lo!/"ernel.lo!"$ "/ ar/lo!/s%stem.lo!"$ "/ ar/lo!/ppp.lo!"$ "/ ar/lo!/secure.lo!"& files.eac' do (file( s'a = *+,Putter.new file c'ec"sum_future = s'a.future 2c'ec"sum s'a.output c'ec"sum_future end
+irst of all, consider the checksum method. It is (uite straightforward, we use the Kigest??-4A5 to compute the checksum of the contents of a file that the actor is given.
Eook at the files.each loop. .his is where it gets interesting. +irst, we create the actor and assign it a file. .hen, instead of just calling the checksum method, we call it using a future. #y doing this, a elluloid??+uture object is immediately returned, instead of blocking. .hen, we take this future object and pass it on to the output method inside the actor. Inside the output method, the value of the checksum is needed* -o, it is attained from the future object0s value method, which blocks until a value is available. .hat solves the problem* Bou might be thinking, $hey, this does pretty much the same thing as the last example*% 4owever, in the last example, in order to do the file related operations asynchronously, we dumped everything into a single method. )ith futures, we are able to cleanly seperate our code. Also, there are use cases where it is only possible to use futures. +or example, if one is writing a library, the result of the checksum function must be a future since the user of the library should be able to add in their own code.
)e use elluloid??+uture to push a block into its own thread. elluloid manages everything about that thread, whose return value we can use later on &using the future0s return value, of course'. -o, this little part of elluloid can be plugged into literally any application and once mastered, can be incredibly useful. "se it wisely*
If everything goes well, the markup is putsd. #ut, what if things start going wrong1 )e0re not really doing much about that. +or that purpose, elluloid provides a mechanism known as a supervisor. 4ere it is in action?
require 'celluloid' require 'net/'ttp' class 5ar"upPutter include Celluloid def initialize(url) @url = url end def output(mar"up_future) puts "-.@url/"
puts "-.mar"up_future. alue/" puts "0000000000000000000000000000" end def !et_mar"up 6et22+77P.!et(89:.parse(@url)) end end we4sites = #"'ttp2//!oo!le.com/"$ "'ttp2//%a'oo.com/"$ "'ttp2//ru4%source.com/"$ "'ttp2//tum4lr.com/"& we4sites.eac' do (we4site( super isor = 5ar"upPutter.super ise_as 2mp$ we4site mp = Celluloid22,ctor#2mp& mar"up_future = mp.future 2!et_mar"up mp.output mar"up_future end
.here0s several new concepts here, so pay close attention. +irst of all, the 5ar"upPutter class is left untouched. In other words, the implementation of the business logic is left unchanged* 2ow, we call the super ise method on the 5ar"upPutter class. .his does three things, first, it creates &and puts into motion' an actor that is an instance of 5ar"upPutter. -econdly, it returns a supervisor object, which can do some interesting things. +inally, it takes its first argument &which is $mp%', and puts an entry of that name in the registry. .he elluloid registry is a bit like a phonebook 8 the actors that are in there can be accessed by name. -o, on the next line, we use the elluloid registry to look up 2mp. .he code after that is (uite straightforward 8 simply using a future to output the markup. )ith two lines of code added, elluloid automatically takes care of restarting and keeping track of actors when they crash* In case one of the actors hits some kind of exception &e.g. the website does not respond to the re(uest and the re(uest times out', the actor is immediately restarted by the elluloid core. If you0ve written this kind of threading code the old fashioned way, you know that this is a very finicky and difficult process, but it is handled entirely by elluloid for us*
require 'celluloid' class +ello*pace,ctor include Celluloid def sa%_ms! print "+ello$ " Celluloid22,ctor#2world&.sa%_ms! end end class ;orld,ctor include Celluloid def sa%_ms! print "world<" Celluloid22,ctor#2newline&.sa%_ms! end end class 6ewline,ctor include Celluloid def sa%_ms! print "=n" end end Celluloid22,ctor#2world& = ;orld,ctor.new Celluloid22,ctor#2newline& = 6ewline,ctor.new +ello*pace,ctor.new.sa%_ms!
)e start out by defining three actors, which each say a part of the $4ello, world*n% message. +ello*pace,ctor uses the registry to look up the ;orld,ctor instance and calls sa%_ms! on it, then, ;orld,ctor does the same for 6ewline,ctor. -o, long story short, the actor communication is done with the actor 6egistry, where we are able to give the actors names. As we know, another way to make actors work together is futures 8 have futures passed around between actors in order to get return values.
5;
(ooling
If you have read up a bit about how web servers operate, you know how important thread pools are. !ools in elluloid are awesome/ they are completely transparent. I think they are probably my favorite feature of elluloid &with so much cool stuff, its hard to choose*'. )e0ll write a simple example to demonstrate how amazing they are?
require 'celluloid' require 'mat'n' class Prime;or"er include Celluloid def prime(num4er) if num4er.prime> puts num4er end end end pool = Prime;or"er.pool (?..)@@@).to_a.map do (i( pool.prime< i end sleep )@@
+irst, we define the Prime;or"er class. .he $)orker% in the name signifies that it is to be used with a pool 8 threads that are part of thread pools are usually called workers. .he function of the prime method in Prime;or"er is to print a number if it is prime &this uses the Nmathn0 module introduced in 5.9 8 you can write your own prime number checker if you like'. .he interesting part is when we introduce the pool by calling the pool method on Prime;or"er. .he $pool% object has all the methods of Prime;or"er, but, it actually creates as many instances of Prime;or"er as the processor has cores. .herefore, if you have a (uad core processor, that would create four actors. )hen methods are called on $pool%, elluloid decides which actor out of the pool to invoke. +ollowing that, we have a map over a large range, in which we call prime &remember, it is called asynchronously because of the bang' on pool. .his automatically distributes the workload over your processors* )ow. It took maybe four or five lines of code extra to acheive complete concurrency. .hat0s amazing.
55
At the end of the program, there is a sleep call. .here is a good reason for this. -ince we are calling prime asynchronously, the main thread &which is the 6uby thread' exits when it is done telling all the actors $hey, remember to print out this prime%. 4owever, the actors aren0t done actually printing the primes by the time the main thread exits, so the output never reaches the terminal. #ut, the sleep command keeps the main thread alive for long enough so that all the output comes out correctly. Also notice that since we are calling prime asynchronously, there is no gurantee of the order of the primes that are outputted.
Wra
ing It !
I hope you enjoyed the article, and that you0re as excited about elluloid as I am. -o far, we0ve discussed how to use the various parts of elluloid are to be used seperately with small examples. In Part 3, we0ll cover how all of this ties together, create some more complex programs, and cover more features, such as Einking. Ko ask any (uestions you have in the comments section below ?'
What is it?
)hat will be our objective1 Its actually (uite simple? we0ll build an 4..! server. ,ore precisely, we0ll build a very incomplete, yet easily extendable, 4..! server written in 6uby with the elluoid library. Kon0t expect it to be something like Apache or .hin 8 but, we0ll learn a ton in the process about elluloid and 4..!, which &arguably' is the important networking protocol for developers to understand.
5A
In case you don0t know, 4..! is a protocol that is used to transfer 4.,E from server to client &this isn0t always the case, but it is the most common', and the most common, for the client to tell the server to do certain things. .he protocol is based on re(uests. -o, the client can issue a $D=.% re(uest to the server in order to get the 4.,E for a certain page. Also, the client can issue a $!3-.% re(uest to provide some data to the server. An important point about 4..! is that the protocol itself is stateless. .his means that each re(uest has no knowledge of any previouse re(uests. )e will only implement the D=. re(uest/ this is for two reasons. +irstly, it is a core function of the 4..! server. -econd, it is very easy to implement. #ut, we will write our code so that it is modular enough for other methods to be added (uite easily.
Starting +ut
+irst of all, let0s build a (uick and dirty prototype. 6uby has built in support for socket communication, which is all attained with the simple $re(uire Nsocket0% statement. "sing that, and all the magic from elluloid, here is our prototype?
require 'soc"et' require 'celluloid' class +77P*er er include Celluloid def initialize(port) @port = port end def start @ser er = 7CP*er er.new(@port) loop . client = @ser er.accept 'eaders = "+77P/).) ?@@ AB=r=n1ate2 7ue$ )C 1ec ?@)@ )@2CD2CE F57=r=n*er er2 9u4%=r=nContent07%pe2 te3t/'tmlG c'arset=iso0DDEH0)=r=n=r=n" client.puts 'eaders client.puts "I'tmlJI/'tmlJ" client.close / end end 's = +77P*er er.new K@@@ 's.start
)e have an +77P*er er class, which revolves under its $start% method. .his method simply starts a server, using the 7CP*er er class from the socket module'.
5G
.hen, we do @ser er.accept. .his is very important, because this is a blocking call. ,eaning that work won0t move any further until a client has come in to be served. .hen, we simply write the 4..! headers and 4.,E ®ardless of what type of re(uest we0ve received.' 3f course, there0s an aparent problem. .his isn0t concurrent. -ince all of the calls we0re using block, we0re just doing each client synchronously. .hat0s no good*
Taking it As"nc
.he solution to this dilemma comes in the form of writing another actor. 4ere0s the code?
require 'soc"et' require 'celluloid' class ,nswer,ctor include Celluloid def initialize(client) @client = client end def start @client.puts 'eaders = "+77P/).) ?@@ AB=r=n1ate2 7ue$ )C 1ec ?@)@ )@2CD2CE F57=r=n*er er2 9u4%=r=nContent07%pe2 te3t/'tmlG c'arset=iso0DDEH0)=r=n=r=n" @client.puts 'eaders @client.puts "I'tmlJI/'tmlJ" loop . -"ind of Lust 'an! around and 4loc" t'e actor / end end class +77P*er er include Celluloid def initialize(port) @port = port end def start @ser er = 7CP*er er.new(@port) loop . aa = ,nswer,ctor.new @ser er.accept puts "waddup" aa.start< / end end
5I
.hat0s pretty big to process at once, but we0ll simply take it step by step. is just another actor 8 it creates a new thread when an instance of it is created. .he real work is happening in the start method.
,nswer,ctor
4ere, we get a hold of the client socket &with which we can talk to the client' and write the header and 4.,E to it and then just wait around. In the +77P*er er class &which is also an actor*', we call aa.start<, which means that start is called asynchronously, so, we can move right along to the next client. .o test if this really is working, open up two telnet &or netcat, which is my favorite' sessions to our homebrew 4..! server, and you should see that you get a response on both &which is also the reason why we have the loop at the end of the start method in the AnswerActor class 8 you have to be able to see the parallel connections to believe them*' #ut, we0re not being too efficient with this. 3ur actors are kind of just sitting around once they0re created, eating up resources. )hat can elluloid do for us1
5J
def start @ser er = 7CP*er er.new(@port) client = nil pool = ,nswer;or"er.pool(size2 E@) loop . client = @ser er.accept pool.start< client / end end 's = +77P*er er.new K@@@ 's.start
If you look at the modified code in +77P*er er.start, it is (uite clear. )e simply create a pool of J; actors &and, therefore, J; threads' which we can then assign work. All of that with one line of code. .hat0s awesome* )hy J;1 )ell, just a random number I picked. .here0s been some good work behind selecting optimal sizes of thread pools, if you0re interested. In case an exception occurs for an actor within the pool, it will be restarted automatically and ready to use the next time it is needed*
Ans'ering Re-uests
-o far, we0ve only spit back just a pair of html tags 8 we haven0t actually listened to what the user is re(uesting. Eet0s work that in. Coila?
require 'soc"et' require 'celluloid' class Muer% attr_accessor 2t%pe$ 2url$ 2ot'er def initialize (quer%_strin!) @t%pe$ @url$ @ot'er = quer%_strin!.split " " end end class ,nswer;or"er include Celluloid def process_!et ... end def start(client) @client = client
5L
client.puts 'eaders = "+77P/).) ?@@ AB=r=n1ate2 7ue$ )C 1ec ?@)@ )@2CD2CE F57=r=n*er er2 9u4%=r=nContent07%pe2 te3t/'tmlG c'arset=iso0DDEH0)=r=n=r=n" client.puts 'eaders loop . quer% = Muer%.new client.readline process_!et if quer%.t%pe == "FN7" / end end class +77P*er er include Celluloid def initialize(port) @port = port end def start @ser er = 7CP*er er.new(@port) client = nil pool = ,nswer;or"er.pool(size2 E@) loop . client = @ser er.accept pool.start< client / end end 's = +77P*er er.new )OOO 's.start
Again, there are a number of changes. )e0ve added the Muer% class, which has a type, url and other attributes to it, representing an 4..! re(uest. As an example, a D=. re(uest looks like $D=. 7index.html 4..!75.5O, where 7index.html is the "6E being re(uested and 4..!75.5 is the protocol being used &as opposed to 4..!75.;'. .hen, in ,nswer;or"er, we call a process_!et method if the (uery type is D=.. #ut, we haven0t defined what exactly process_!et should do 8 let0s make a simple way to view files inside a certain folder.
5M
4ere it is?
require 'soc"et' require 'celluloid' class Muer% attr_accessor 2t%pe$ 2url$ 2ot'er def initialize (quer%_strin!) @t%pe$ @url$ @ot'er = quer%_strin!.split " " end end class ,nswer;or"er include Celluloid def process_!et(quer%) dir_pat' = "ser er" filepat' = dir_pat' P quer%.url if File.e3ists> filepat' @client.puts (File.open(filepat').read) else @client.puts "Can't find file" end end def process_req(quer%) process_!et quer% if quer%.t%pe == "FN7" end def start(client) @client = client 'eaders = "+77P/).) ?@@ AB=r=n1ate2 7ue$ )C 1ec ?@)@ )@2CD2CE F57=r=n*er er2 9u4%=r=nContent07%pe2 te3t/'tmlG c'arset=iso0DDEH0)=r=n=r=n" client.puts 'eaders loop . quer% = Muer%.new client.readline process_req quer% / end end class +77P*er er include Celluloid def initialize(port) @port = port end def start @ser er = 7CP*er er.new(@port) client = nil pool = ,nswer;or"er.pool(size2 E@)
5:
loop . client = @ser er.accept pool.start< client / end end 's = +77P*er er.new K@@@ 's.start
It simply takes the $server% folder &in the same folder as the 4..! server0s source code itself', and returns the contents of a given file. And, we0ve also sorted out the way we handle what type of re(uest has been sent by the client, to make the server easy to extend. 2ote that, because of the simple way this has been done, directly plugging in 5AM.;.;.5?G;;; into your browser will not work 8 you will have to use 5AM.;.;.5?G;;;7index.html or some other filename*
Wrapping it Up
)e0ve built a &very' rudimentary 4..! server using elluloid. In doing so, we0ve seen how actors can work together in simple situations. 3f course, you will write applications larger than this one using elluloid &I sure hope so*' and you probably will use more advanced features. 6emember, however, that the basics are very important. I hope seeing some of the concepts come together made the learning easier. If you liked the article, do .weet it out to your followers. .hanks for reading*
59
A;
class C'opstic" def initialize @mute3 = 5ute3.new end def ta"e @mute3.loc" end def drop @mute3.unloc" rescue 7'readNrror puts "7r%in! to drop a c'opstic" not acquired" end def in_use> @mute3.loc"ed> end end class 7a4le def initialize(num_seats) @c'opstic"s = num_seats.times.map . C'opstic".new / end def left_c'opstic"_at(position) inde3 = (position 0 )) Q @c'opstic"s.size @c'opstic"s#inde3& end def ri!'t_c'opstic"_at(position) inde3 = position Q @c'opstic"s.size @c'opstic"s#inde3& end def c'opstic"s_in_use @c'opstic"s.select . (f( f.in_use> /.size end end
.he C'opstic" class is just a thin wrapper around a regular 6uby mutex that will ensure that two philosophers can not grab the same chopstick at the same time. .he 7a4le class deals with the geometry of the problem/ it knows where each seat is at the table, which chopstick is to the left or to the right of that seat, and how many chopsticks are currently in use. 2ow that youQve seen the basic domain objects that model this problem, weQll look at different ways of implementing the behavior of the philosophers. )eQll start with what doesn't work.
A5
def dine(ta4le$ position) @left_c'opstic" = ta4le.left_c'opstic"_at(position) @ri!'t_c'opstic" = ta4le.ri!'t_c'opstic"_at(position) loop do t'in" eat end end def t'in" puts "-.@name/ is t'in"in!" end def eat ta"e_c'opstic"s puts "-.@name/ is eatin!." drop_c'opstic"s end def ta"e_c'opstic"s @left_c'opstic".ta"e @ri!'t_c'opstic".ta"e end def drop_c'opstic"s @left_c'opstic".drop @ri!'t_c'opstic".drop end end
If youQre still scratching your head, consider what happens when each philosopher object is given its own thread, and all the philosophers attempt to eat at the same time. In this naive implementation, it is possible to reach a state in which every philosopher picks up their left-hand chopstick, leaving no chopsticks on the table. In that scenario, every philosopher would simply wait forever for their right-hand chopstick to become available -resulting in a deadlock. Bou can reproduce the problem by running the following code?
names = Qw.+eraclitus ,ristotle Npictetus *c'open'auer Popper/ p'ilosop'ers = names.map . (name( P'ilosop'er.new(name) / ta4le = 7a4le.new(p'ilosop'ers.size) t'reads = p'ilosop'ers.map.wit'_inde3 do (p'ilosop'er$ i( 7'read.new . p'ilosop'er.dine(ta4le$ i) / end t'reads.eac'(R2Loin) sleep
6uby is smart enough to inform you of what went wrong, so you should end up seeing a backtrace that looks something like this?
,ristotle is t'in"in!
AA
Popper is eatin!. Popper is t'in"in! Npictetus is eatin!. Npictetus is t'in"in! +eraclitus is eatin!. +eraclitus is t'in"in! *c'open'auer is eatin!. *c'open'auer is t'in"in! dinin!_p'ilosop'ers_uncoordinated.r42OH2in SLoin'2 deadloc" detected (fatal) from dinin!_p'ilosop'ers_uncoordinated.r42OH2in Seac'' from dinin!_p'ilosop'ers_uncoordinated.r42OH2in SImainJ
In many situations, the most simple solution tends to be the best one, but this is obviously not one of those cases. -ince weQve learned the hard way that the philosophers cannot be safely left to their own devices, weQll need to do more to make sure their behaviors remain coordinated.
Introducing the ;aiter object re(uires us to make some minor changes to our P'ilosop'er object, but they are fairly straightforward? AG
class P'ilosop'er - ... all omitted code same as 4efore def dine(ta4le$ position$ waiter) @left_c'opstic" = ta4le.left_c'opstic"_at(position) @ri!'t_c'opstic" = ta4le.ri!'t_c'opstic"_at(position) loop do t'in" - instead of callin! eat() directl%$ ma"e a request to t'e waiter waiter.ser e(ta4le$ self) end end def eat - remo ed ta"e_c'opstic"s call$ as t'at's now 'andled 4% t'e waiter puts "-.@name/ is eatin!." drop_c'opstic"s end end
.he runner code also needs minor tweaks, but is mostly similar to what you saw earlier?
names = Qw.+eraclitus ,ristotle Npictetus *c'open'auer Popper/ p'ilosop'ers = names.map . (name( P'ilosop'er.new(name) / ta4le = 7a4le.new(p'ilosop'ers.size) waiter = ;aiter.new(p'ilosop'ers.size 0 )) t'reads = p'ilosop'ers.map.wit'_inde3 do (p'ilosop'er$ i( 7'read.new . p'ilosop'er.dine(ta4le$ i$ waiter) / end t'reads.eac'(R2Loin) sleep
.his approach is reasonable and solves the deadlock issue, but using mutexes to synchronize code re(uires some low level thinking. =ven in this simple problem, there were several gotchas to consider. As programs get more complicated, it becomes really difficult to keep track of critical regions while ensuring that the code behaves properly when accessing them. .he actor model is meant to provide a more systematic and natural way of sharing data between threads. )eQll now take a look at an actor-based solution to this problem so that we can see how it compares to this mutex-based approach.
AI
)eQll spend the rest of the article explaining the inner workings of this code, so donQt worry about understanding every last detail. Instead, just try to get a basic idea of whatQs going on here?
class P'ilosop'er include Celluloid def initialize(name) @name = name end *witc'in! to t'e actor model requires us !et rid of our more procedural e ent loop in fa or of a messa!e0oriented approac' usin! recursion. 7'e call to t'in"() e entuall% leads to a call to eat()$ w'ic' in turn calls 4ac" to t'in"()$ completin! t'e loop.
def dine(ta4le$ position$ waiter) @waiter = waiter @left_c'opstic" = ta4le.left_c'opstic"_at(position) @ri!'t_c'opstic" = ta4le.ri!'t_c'opstic"_at(position) t'in" end def t'in" puts "-.@name/ is t'in"in!." sleep(rand) - ,s%nc'ronousl% notifies t'e waiter o4Lect t'at - t'e p'ilosop'or is read% to eat @waiter.as%nc.request_to_eat(,ctor.current) end def eat ta"e_c'opstic"s puts "-.@name/ is eatin!." sleep(rand) drop_c'opstic"s - ,s%nc'ronousl% notifies t'e waiter - t'at t'e p'ilosop'er 'as finis'ed eatin! @waiter.as%nc.done_eatin!(,ctor.current) t'in" end def ta"e_c'opstic"s @left_c'opstic".ta"e @ri!'t_c'opstic".ta"e end def drop_c'opstic"s @left_c'opstic".drop @ri!'t_c'opstic".drop
AJ
end - 7'is code is necessar% in order for Celluloid to s'ut down cleanl% def finalize drop_c'opstic"s end end class ;aiter include Celluloid def initialize @eatin! = #& end 4ecause s%nc'ronized data access is ensured 4% t'e actor model$ t'is code is muc' more simple t'an its mute304ased counterpart. +owe er$ t'is approac' requires two met'ods (one to start and one to stop t'e eatin! process)$ w'ere t'e pre ious approac' used a sin!le ser e() met'od.
def request_to_eat(p'ilosop'er) return if @eatin!.include>(p'ilosop'er) @eatin! II p'ilosop'er p'ilosop'er.as%nc.eat end def done_eatin!(p'ilosop'er) @eatin!.delete(p'ilosop'er) end end
.he runner code is similar to before, with only some very minor changes?
names = Qw.+eraclitus ,ristotle Npictetus *c'open'auer Popper/ p'ilosop'ers = names.map . (name( P'ilosop'er.new(name) / waiter = ;aiter.new - no lon!er needs a "capacit%" ar!ument ta4le = 7a4le.new(p'ilosop'ers.size) p'ilosop'ers.eac'_wit'_inde3 do (p'ilosop'er$ i( - 6o lon!er manuall% create a t'read$ rel% on as%nc() to do t'at for us. p'ilosop'er.as%nc.dine(ta4le$ i$ waiter) end sleep
.he runtime behavior of this solution is similar to that of our mutex-based solution. 4owever, the following differences in implementation are worth noting?
=ach class that mixes in Celluloid becomes an actor with its own thread of execution.
AL
.he elluloid library intercepts any method call run through the as%nc proxy object and stores it in the actorQs mailbox. .he actorQs thread will se(uentially execute those stored methods, one after another. .his behavior makes it so that we donQt need to manage threads and mutex synchronization explicitly. .he elluloid library handles that under the hood in an object-oriented manner. If we encapsulate all data inside actor objects, only the actorQs thread will be able to access and modify its own data. .hat prevents the possibility of two threads writing to a critical region at the same time, which eliminates the risk of deadlocks and data corruption.
.hese features are very useful for simplifying the way we think about concurrent programming, but youQre probably wondering how much magic is involved in implementing them. EetQs build our own minimal drop-in replacement for elluloid to find out*
AM
end end def as%nc @as%nc_pro3% end def send_later(met'$ Tar!s) @mail4o3 II #met'$ ar!s& end def terminate @runnin! = false end def met'od_missin!(met'$ Tar!s) process_messa!e(met'$ Tar!s) end pri ate def process_in4o3 w'ile @runnin! met'$ ar!s = @mail4o3.pop process_messa!e(met'$ Tar!s) end rescue N3ception =J e3 puts "Nrror w'ile runnin! actor2 -.e3/" end def process_messa!e(met'$ Tar!s) @mute3.s%nc'ronize do @tar!et.pu4lic_send(met'$ Tar!s) end end end class ,s%ncPro3% def initialize(actor) @actor = actor end def met'od_missin!(met'$ Tar!s) @actor.send_later(met'$ Tar!s) end end end
.his code mostly builds upon concepts that have already been covered in this article, so it shouldnQt be too hard to follow with a bit of effort. .hat said, combining meta-programming techni(ues and concurrency can lead to code that makes your eyes glaze over, so we should also make an attempt to discuss how this module works at the high level. EetQs do that now* Any class that includes the ,ctor module will be converted into an actor and will be able to receive asynchronous calls. )e accomplish this by overriding the constructor of the target class so that we can return a proxy object every time an object of that class is instantiated. )e also store the proxy object in a thread level variable. .his is necessary because when sending
A:
messages between actors, if we refer to self in method calls we will exposed the inner target object, instead of the proxy. .his same gotcha is also present in elluloid. "sing this mixin, whenever we attempt to create an instance of a P'ilosop'er object, we will actually receive an instance of ,ctor22Pro3%. .he P'ilosop'er class is left mostly untouched, and so the actor-like behavior is handled entirely by the proxy object. "pon instantiation, that proxy creates a mailbox to store the incoming asynchronous messages and a thread to process those messages. .he inbox is a thread-safe (ueue that ensures that incoming message are processed se(uentially even if they arrive at the same time. )henever the inbox is empty, the actorQs thread will be blocked until a new message needs to be processed. .his is roughly how things work in elluloid as well, although its implementation is much more complex due to the many additional features it offers. -till, if you understand this code, youQre well on your way to having a working knowledge of what the actor model is all about.
A solution that leads to deadlocks A coordinated mutex-based solution An actor-based solution using elluloid An actor-based solution using a hand-rolled actor library ,inimal implementation of the actor model A9
G;