Você está na página 1de 30

An Introduction to Celluloid

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

fp = FilePutter.new "/ ar/lo!/"ernel.lo!" fp.load_file fp.print

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 elluloid really shines?


require 'celluloid' class FilePutter include Celluloid def initialize(filename) @filename = filename end def load_file_and_print @file_contents = File.read @filename 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.as%nc.load_file_and_print end

.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.

Making An" #lock Concurrent


.here is a very cool use for futures, namely, they allow us to push block of code to another thread incredibly easily. heck it out?
require 'celluloid' def some_met'od(future) -do somet'in! craz% al = future. alue -do somet'in! wit' al end future = Celluloid22Future.new do -incredi4l% comple3 computation end some_met'od(future)

)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*

Catching $rrors % Su ervisors


.o see how error handling works in elluloid, we0re going to build a simple tool that gets the 4.,E of various websites. 4ere it is, with the stuff we0ve learned so far?
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( mp = 5ar"upPutter.new we4site mar"up_future = mp.future 2!et_mar"up mp.output mar"up_future end

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*

Co&&unication #et'een Actors


In nearly all applications, actors will not be working in isolated environments 8 they will be communicating with other actors. Fust to explain how communication between actors works in elluloid, we0ll write three actors to print out $4ello, world*% when run correctly. heck it out?

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.

#locking Calls Inside Actors


If you have experience with =vent,achine, you know that you can0t mix =vent,achine with any other library for I3 8 the library needs to be =vent,achine compatible. As such, you aren0t able to utilize the full power of the 6uby community. Instead, you are stuck with the far smaller =vent,achine community. )ith elluloid, this isn0t the case* -ince the actors are all in their own threads, it is perfectly okay for method calls inside actors to block, since it only blocks that one actor* #ut, beware. Ko not make infinitely blocking calls in actors &such as listening on a socket' 8 this leads to all messages going to that actor to beome paused, which is bad*

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 ?'

The Big Project


-o, assuming you0ve read the previous parts of this series, you should have a somewhat working knowledge of the various parts of elluloid. #ut, so far, we haven0t really covered how all of these parts come together to form a complete application. )e0re going to do that here* -o, let0s dive in*

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.

A Little A)out *TT(


3f course, if we want to build an 4..! server, we need to be in the know about what 4..! is and how it functions.

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 &regardless 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

's = +77P*er er.new K@@@ 's.start

.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

Going ,or a S'i&


)e can use pools* Det it, that0s why I called this section $going for a swim%* 2o1 )ell, on to the code?
require 'soc"et' require 'celluloid' class ,nswer;or"er include Celluloid def start(client) 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

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.

Ans'ering the G$Ts


#eware, this is a horrible implementation of process_!et, but, it is meant to be very simple and straightforward since it really has nothing to do with the scope of this article, which is to learn more about elluloid.

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 gentle introduction to actor-based concurrency


.his issue was a collaboration with Alberto +ernPndez apel, a 6uby developer from -pain. Although it has been through many revisions since we started, AlbertoQs ideas, code, and explanations provided an excellent starting point that lead us to publish this article. onventional wisdom says that concurrent programming is hard, especially in 6uby. .his basic assumption is what lead many 6ubyists to take an interest in languages like =rlang and -cala -- their baked in support for the actor model is meant to make concurrent systems much easier for everyday programmers to implement and understand. #ut do you really need to look outside of 6uby to find concurrency primitives that can make your work easier1 .he answer to that (uestion probably depends on the levels of concurrency and availability that you re(uire, but things have definitely been shaping up in recent years. In particular, the elluloid framework has brought us a convenient and clean way to implement actor-based concurrent systems in 6uby. In order to appreciate what elluloid can do for you, you first need to understand what the actor model is, and what benefits it offers over the traditional approach of directly using threads and locks for concurrent programming. In this article, weQll try to shed some light on those points by solving a classic concurrency puzzle in three ways? "sing 6ubyQs built-in primitives &threads and mutex locks', using the elluloid framework, and using a minimal implementation of the actor model that weQll build from scratch. #y the end of this article, you certainly wonQt be a concurrency expert if you arenQt already, but youQll have a nice head start on some basic concepts that will help you decide how to tackle concurrent programming within your own projects. EetQs begin*

The Dining (hiloso hers (ro)le&


.he Kining !hilosophers problem was formulated by =dsger Kjisktra in 59LJ to illustrate the kind of issues we can find when multiple processes compete to gain access to exclusive resources. In this problem, five philosophers meet to have dinner. .hey sit at a round table and each one has a bowl of rice in front of them. .here are also five chopsticks, one between each philosopher. .he philosophers spent their time thinking about The Meaning of Life. )henever they get hungry, they try to eat. #ut a philosopher needs a chopstick in each hand in order to grab the rice. If any other philosopher has already taken one of those chopsticks, the hungry philosopher will wait until that chopstick is available. .his problem is interesting because if it is not properly solved it can easily lead to deadlock issues. )eQll take a look at those issues soon, but first letQs convert this problem domain into a few basic 6uby objects.

Modeling the table and its chopsticks


All three of the solutions weQll discuss in this article rely on a C'opstic" class and a 7a4le class. .he definitions of both classes are shown below?

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.

A solution that leads to deadlocks


.he P'ilosop'er class shown below would seem to be the most straightforward solution to this problem, but has a fatal flaw that prevents it from being thread safe. an you spot it1
class P'ilosop'er def initialize(name) @name = name end

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.

A coordinated mutex-based solution


3ne easy solution to this issue is introduce a ;aiter object into the mix. In this model, the philosopher must ask the waiter before eating. If the number of chopsticks in use is four or more, the waiter will make the philosopher wait until someone finishes eating. .his will ensure that at least one philosopher will be able to eat at any time, avoiding the deadlock condition. .hereQs still a catch, though. +rom the moment the waiter checks the number of chopstick in use until the next philosopher starts to eat we have a critical region in our program? If we let two concurrent threads execute that code at the same time there is still a chance of a deadlock. +or example, suppose the waiter checks the number of chopsticks used and see it is G. At that moment, the scheduler yields control to another philosopher who is just picking the chopstick. )hen the execution flow comes back to the original thread, it will allow the original philosopher to eat, even if there may be more than four chopsticks already in use. .o avoid this situation we need to protect the critical region with a mutex, as shown below?
class ;aiter def initialize(capacit%) @capacit% = capacit% @mute3 = 5ute3.new end def ser e(ta4le$ p'ilosop'er) @mute3.s%nc'ronize do sleep(rand) w'ile ta4le.c'opstic"s_in_use J= @capacit% p'ilosop'er.ta"e_c'opstic"s end p'ilosop'er.eat end end

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.

An actor.)ased solution using Celluloid


)eQll now rework our P'ilosop'er and ;aiter classes to make use of elluloid. ,uch of the code will remain the same, but some important details will change. .he full class definitions are shown below to preserve context, but the changed portions are marked with comments.

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*

Rolling our o'n actor &odel


elluloid provides much more functionality than what we can discuss in this article, but building a barebones implementation of the actor model is within our reach. In fact, the following :; lines of code are enough to serve as a replacement for our use of elluloid in the previous example?
require 't'read' module ,ctor - 7o use t'is$ %ou'd include ,ctor instead of Celluloid module Class5et'ods def new(Tar!s$ R4loc") Pro3%.new(super) end end class II self def included("lass) "lass.e3tend(Class5et'ods) end def current 7'read.current#2actor& end end class Pro3% def initialize(tar!et) @tar!et = tar!et @mail4o3 = Mueue.new @mute3 = 5ute3.new @runnin! = true @as%nc_pro3% = ,s%ncPro3%.new(self) @t'read = 7'read.new do 7'read.current#2actor& = self process_in4o3

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.

Actors are helpful, but are not a golden hammer


=ven this minimal implementation of the actor model gets the low-level concurrency primitives out of our ordinary class definitions, and into a centralized place where it can be handled in a consistent and reliable way. elluloid goes a lot farther than we did here by providing excellent fault tolerance mechanisms, the ability to recover from failures, and lots of other interesting stuff. 4owever, these benefits do come with their own share of costs and potential pitfalls. -o what can go wrong when using actors in 6uby1 )eQve already hinted at the potential issues that can arise due to the issue of self schizophrenia in proxy objects. !erhaps more complicated is the issue of mutable state? while using actors guarantees that the state within an object will be accessed se(uentially, it does not provide the same guarantee for the messages that are being passed around between objects. In languages like =rlang, messages consist of immutable parameters, so consistency is enforced at the language level. In 6uby, we donQt have that constraint, so we either need to solve this problem by convention, or by freezing the objects we pass around as arguments -- which is (uite restrictive* )ithout attempting to enumerate all the other things that could go wrong, the point here is simply that there is no such thing as a golden hammer when it comes to concurrent programming. 4opefully this article has given you a basic sense of both the benefits and drawbacks of applying the actor model in 6uby, along with enough background knowledge to apply some of these ideas in your own projects. If it has done so, please do share your story.

Source code from this article


All of the code from this article is in !racticing 6ubyQs example repository, but the links below highlight the main points of interest?

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

hopsticks class definition .able class definition

G;

Você também pode gostar