Skip to content

Commit 06dc588

Browse files
author
Petr Chalupa
committed
Merge pull request #132 from ruby-concurrency/actress
Adding features to Actors
2 parents bc8dc63 + 799cfe6 commit 06dc588

57 files changed

Lines changed: 1980 additions & 946 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
require 'benchmark'
2-
require 'concurrent/actress'
3-
Concurrent::Actress.i_know_it_is_experimental!
2+
require 'concurrent/actor'
3+
Concurrent::Actor.i_know_it_is_experimental!
4+
45
require 'celluloid'
56
require 'celluloid/autostart'
67

8+
# require 'stackprof'
9+
# require 'profiler'
10+
711
logger = Logger.new($stderr)
812
logger.level = Logger::INFO
913
Concurrent.configuration.logger = lambda do |level, progname, message = nil, &block|
@@ -30,26 +34,26 @@ def counting(count, ivar)
3034
ivar.set count
3135
end
3236
end
33-
end
37+
end if defined? Celluloid
3438

3539
threads = []
3640

41+
# Profiler__.start_profile
42+
# StackProf.run(mode: :cpu,
43+
# interval: 10,
44+
# out: File.join(File.dirname(__FILE__), 'stackprof-cpu-myapp.dump')) do
3745
Benchmark.bmbm(10) do |b|
3846
[2, adders_size, adders_size*2, adders_size*3].each do |adders_size|
3947

40-
b.report(format('%5d %4d %s', ADD_TO*counts_size, adders_size, 'actress')) do
48+
b.report(format('%5d %4d %s', ADD_TO*counts_size, adders_size, 'concurrent')) do
4149
counts = Array.new(counts_size) { [0, Concurrent::IVar.new] }
4250
adders = Array.new(adders_size) do |i|
43-
Concurrent::Actress::AdHoc.spawn("adder#{i}") do
51+
Concurrent::Actor::AdHoc.spawn("adder#{i}") do
4452
lambda do |(count, ivar)|
45-
if count.nil?
46-
terminate!
53+
if count < ADD_TO
54+
adders[(i+1) % adders_size].tell [count+1, ivar]
4755
else
48-
if count < ADD_TO
49-
adders[(i+1) % adders_size].tell [count+1, ivar]
50-
else
51-
ivar.set count
52-
end
56+
ivar.set count
5357
end
5458
end
5559
end
@@ -65,32 +69,38 @@ def counting(count, ivar)
6569

6670
threads << Thread.list.size
6771

68-
adders.each { |a| a << [nil, nil] }
72+
adders.each { |a| a << :terminate! }
6973
end
7074

71-
b.report(format('%5d %4d %s', ADD_TO*counts_size, adders_size, 'celluloid')) do
72-
counts = []
73-
counts_size.times { counts << [0, Concurrent::IVar.new] }
75+
if defined? Celluloid
76+
b.report(format('%5d %4d %s', ADD_TO*counts_size, adders_size, 'celluloid')) do
77+
counts = []
78+
counts_size.times { counts << [0, Concurrent::IVar.new] }
7479

75-
adders = []
76-
adders_size.times do |i|
77-
adders << Counter.new(adders, i)
78-
end
80+
adders = []
81+
adders_size.times do |i|
82+
adders << Counter.new(adders, i)
83+
end
7984

80-
counts.each_with_index do |count, i|
81-
adders[i % adders_size].counting *count
82-
end
85+
counts.each_with_index do |count, i|
86+
adders[i % adders_size].counting *count
87+
end
8388

84-
counts.each do |count, ivar|
85-
raise unless ivar.value >= ADD_TO
86-
end
89+
counts.each do |count, ivar|
90+
raise unless ivar.value >= ADD_TO
91+
end
8792

88-
threads << Thread.list.size
93+
threads << Thread.list.size
8994

90-
adders.each(&:terminate)
95+
adders.each(&:terminate)
96+
end
9197
end
9298
end
9399
end
100+
# end
101+
# Profiler__.stop_profile
94102

95103
p threads
96104

105+
# Profiler__.print_profile $stdout
106+

doc/actor/examples.out.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
3+

doc/actor/init.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
require 'concurrent/actor'
2+
Concurrent::Actor.i_know_it_is_experimental!

doc/actor/main.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Actor model
2+
3+
- Light-weighted.
4+
- Inspired by Akka and Erlang.
5+
- Modular.
6+
7+
Actors are sharing a thread-pool by default which makes them very cheap to create and discard.
8+
Thousands of actors can be created, allowing you to break the program into small maintainable pieces,
9+
without violating the single responsibility principle.
10+
11+
## What is an actor model?
12+
13+
[Wiki](http://en.wikipedia.org/wiki/Actor_model) says:
14+
The actor model in computer science is a mathematical model of concurrent computation
15+
that treats _actors_ as the universal primitives of concurrent digital computation:
16+
in response to a message that it receives, an actor can make local decisions,
17+
create more actors, send more messages, and determine how to respond to the next
18+
message received.
19+
20+
## Why?
21+
22+
Concurrency is hard this is one of many ways how to simplify the problem.
23+
It is simpler to reason about actors than about locks (and all their possible states).
24+
25+
## How to use it
26+
27+
{include:file:doc/actor/quick.out.rb}
28+
29+
## Messaging
30+
31+
Messages are processed in same order as they are sent by a sender. It may interleaved with
32+
messages form other senders though. There is also a contract in actor model that
33+
messages sent between actors should be immutable. Gems like
34+
35+
- [Algebrick](https://github.com/pitr-ch/algebrick) - Typed struct on steroids based on
36+
algebraic types and pattern matching
37+
- [Hamster](https://github.com/hamstergem/hamster) - Efficient, Immutable, Thread-Safe
38+
Collection classes for Ruby
39+
40+
are very useful.
41+
42+
### Dead letter routing
43+
44+
see {AbstractContext#dead_letter_routing} description:
45+
46+
> {include:Actor::AbstractContext#dead_letter_routing}
47+
48+
## Architecture
49+
50+
Actors are running on shared thread poll which allows user to create many actors cheaply.
51+
Downside is that these actors cannot be directly used to do IO or other blocking operations.
52+
Blocking operations could starve the `default_task_pool`. However there are two options:
53+
54+
- Create an regular actor which will schedule blocking operations in `global_operation_pool`
55+
(which is intended for blocking operations) sending results back to self in messages.
56+
- Create an actor using `global_operation_pool` instead of `global_task_pool`, e.g.
57+
`AnIOActor.spawn name: :blocking, executor: Concurrent.configuration.global_operation_pool`.
58+
59+
Each actor is composed from 4 parts:
60+
61+
### {Reference}
62+
{include:Actor::Reference}
63+
64+
### {Core}
65+
{include:Actor::Core}
66+
67+
### {AbstractContext}
68+
{include:Actor::AbstractContext}
69+
70+
### {Behaviour}
71+
{include:Actor::Behaviour}
72+
73+
## Speed
74+
75+
Simple benchmark Actor vs Celluloid, the numbers are looking good
76+
but you know how it is with benchmarks. Source code is in
77+
`examples/actor/celluloid_benchmark.rb`. It sends numbers between x actors
78+
and adding 1 until certain limit is reached.
79+
80+
Benchmark legend:
81+
82+
- mes. - number of messages send between the actors
83+
- act. - number of actors exchanging the messages
84+
- impl. - which gem is used
85+
86+
### JRUBY
87+
88+
Rehearsal --------------------------------------------------------
89+
50000 2 concurrent 24.110000 0.800000 24.910000 ( 7.728000)
90+
50000 2 celluloid 28.510000 4.780000 33.290000 ( 14.782000)
91+
50000 500 concurrent 13.700000 0.280000 13.980000 ( 4.307000)
92+
50000 500 celluloid 14.520000 11.740000 26.260000 ( 12.258000)
93+
50000 1000 concurrent 10.890000 0.220000 11.110000 ( 3.760000)
94+
50000 1000 celluloid 15.600000 21.690000 37.290000 ( 18.512000)
95+
50000 1500 concurrent 10.580000 0.270000 10.850000 ( 3.646000)
96+
50000 1500 celluloid 14.490000 29.790000 44.280000 ( 26.043000)
97+
--------------------------------------------- total: 201.970000sec
98+
99+
mes. act. impl. user system total real
100+
50000 2 concurrent 9.820000 0.510000 10.330000 ( 5.735000)
101+
50000 2 celluloid 10.390000 4.030000 14.420000 ( 7.494000)
102+
50000 500 concurrent 9.880000 0.200000 10.080000 ( 3.310000)
103+
50000 500 celluloid 12.430000 11.310000 23.740000 ( 11.727000)
104+
50000 1000 concurrent 10.590000 0.190000 10.780000 ( 4.029000)
105+
50000 1000 celluloid 14.950000 23.260000 38.210000 ( 20.841000)
106+
50000 1500 concurrent 10.710000 0.250000 10.960000 ( 3.892000)
107+
50000 1500 celluloid 13.280000 30.030000 43.310000 ( 24.620000) (1)
108+
109+
### MRI 2.1.0
110+
111+
Rehearsal --------------------------------------------------------
112+
50000 2 concurrent 4.640000 0.080000 4.720000 ( 4.852390)
113+
50000 2 celluloid 6.110000 2.300000 8.410000 ( 7.898069)
114+
50000 500 concurrent 6.260000 2.210000 8.470000 ( 7.400573)
115+
50000 500 celluloid 10.250000 4.930000 15.180000 ( 14.174329)
116+
50000 1000 concurrent 6.300000 1.860000 8.160000 ( 7.303162)
117+
50000 1000 celluloid 12.300000 7.090000 19.390000 ( 17.962621)
118+
50000 1500 concurrent 7.410000 2.610000 10.020000 ( 8.887396)
119+
50000 1500 celluloid 14.850000 10.690000 25.540000 ( 24.489796)
120+
---------------------------------------------- total: 99.890000sec
121+
122+
mes. act. impl. user system total real
123+
50000 2 concurrent 4.190000 0.070000 4.260000 ( 4.306386)
124+
50000 2 celluloid 6.490000 2.210000 8.700000 ( 8.280051)
125+
50000 500 concurrent 7.060000 2.520000 9.580000 ( 8.518707)
126+
50000 500 celluloid 10.550000 4.980000 15.530000 ( 14.699962)
127+
50000 1000 concurrent 6.440000 1.870000 8.310000 ( 7.571059)
128+
50000 1000 celluloid 12.340000 7.510000 19.850000 ( 18.793591)
129+
50000 1500 concurrent 6.720000 2.160000 8.880000 ( 7.929630)
130+
50000 1500 celluloid 14.140000 10.130000 24.270000 ( 22.775288) (1)
131+
132+
*Note (1):* Celluloid is using thread per actor so this bench is creating about 1500
133+
native threads. Actor is using constant number of threads.
Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
class Counter
1+
class Counter < Concurrent::Actor::Context
22
# Include context of an actor which gives this class access to reference and other information
3-
# about the actor, see CoreDelegations.
4-
include Concurrent::Actress::Context
3+
# about the actor, see PublicDelegations.
54

65
# use initialize as you wish
76
def initialize(initial_value)
@@ -10,16 +9,11 @@ def initialize(initial_value)
109

1110
# override on_message to define actor's behaviour
1211
def on_message(message)
13-
case message
14-
when Integer
12+
if Integer === message
1513
@count += message
16-
when :terminate
17-
terminate!
18-
else
19-
raise 'unknown'
2014
end
2115
end
22-
end
16+
end #
2317

2418
# Create new actor naming the instance 'first'.
2519
# Return value is a reference to the actor, the actual actor is never returned.
@@ -35,7 +29,7 @@ def on_message(message)
3529
counter.ask(0).value
3630

3731
# Terminate the actor.
38-
counter.tell(:terminate)
32+
counter.tell(:terminate!)
3933
# Not terminated yet, it takes a while until the message is processed.
4034
counter.terminated?
4135
# Waiting for the termination.
@@ -52,22 +46,18 @@ def on_message(message)
5246

5347

5448
# Lets define an actor creating children actors.
55-
class Node
56-
include Concurrent::Actress::Context
57-
49+
class Node < Concurrent::Actor::Context
5850
def on_message(message)
5951
case message
6052
when :new_child
61-
spawn self.class, :child
53+
Node.spawn :child
6254
when :how_many_children
6355
children.size
64-
when :terminate
65-
terminate!
6656
else
6757
raise 'unknown'
6858
end
6959
end
70-
end
60+
end #
7161

7262
# Actors are tracking parent-child relationships
7363
parent = Node.spawn :parent

0 commit comments

Comments
 (0)