Skip to content

Commit 06dc588

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

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1980
-946
lines changed
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+
File renamed without changes.

doc/actor/examples.out.rb

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
3+
File renamed without changes.

doc/actor/init.rb

+2
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

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

doc/actress/quick.in.rb renamed to doc/actor/quick.in.rb

+8-18
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)