1 # Ractor is a Actor-model abstraction for Ruby that provides thread-safe parallel execution.
3 # Ractor.new can make a new Ractor, and it will run in parallel.
5 # # The simplest ractor
6 # r = Ractor.new {puts "I am in Ractor!"}
7 # r.take # wait for it to finish
8 # # here "I am in Ractor!" would be printed
10 # Ractors do not share usual objects, so the same kinds of thread-safety concerns such as data-race,
11 # race-conditions are not available on multi-ractor programming.
13 # To achieve this, ractors severely limit object sharing between different ractors.
14 # For example, unlike threads, ractors can't access each other's objects, nor any objects through
15 # variables of the outer scope.
18 # r = Ractor.new {puts "I am in Ractor! a=#{a}"}
19 # # fails immediately with
20 # # ArgumentError (can not isolate a Proc because it accesses outer variables (a).)
22 # On CRuby (the default implementation), Global Virtual Machine Lock (GVL) is held per ractor, so
23 # ractors are performed in parallel without locking each other.
25 # Instead of accessing the shared state, the objects should be passed to and from ractors via
26 # sending and receiving objects as messages.
30 # a_in_ractor = receive # receive blocks till somebody will pass message
31 # puts "I am in Ractor! a=#{a_in_ractor}"
35 # # here "I am in Ractor! a=1" would be printed
37 # There are two pairs of methods for sending/receiving messages:
39 # * Ractor#send and Ractor.receive for when the _sender_ knows the receiver (push);
40 # * Ractor.yield and Ractor#take for when the _receiver_ knows the sender (pull);
42 # In addition to that, an argument to Ractor.new would be passed to block and available there
43 # as if received by Ractor.receive, and the last block value would be sent outside of the
44 # ractor as if sent by Ractor.yield.
46 # A little demonstration on a classic ping-pong:
48 # server = Ractor.new do
49 # puts "Server starts: #{self.inspect}"
50 # puts "Server sends: ping"
51 # Ractor.yield 'ping' # The server doesn't know the receiver and sends to whoever interested
52 # received = Ractor.receive # The server doesn't know the sender and receives from whoever sent
53 # puts "Server received: #{received}"
56 # client = Ractor.new(server) do |srv| # The server is sent inside client, and available as srv
57 # puts "Client starts: #{self.inspect}"
58 # received = srv.take # The Client takes a message specifically from the server
59 # puts "Client received from " \
60 # "#{srv.inspect}: #{received}"
61 # puts "Client sends to " \
62 # "#{srv.inspect}: pong"
63 # srv.send 'pong' # The client sends a message specifically to the server
66 # [client, server].each(&:take) # Wait till they both finish
70 # Server starts: #<Ractor:#2 test.rb:1 running>
72 # Client starts: #<Ractor:#3 test.rb:8 running>
73 # Client received from #<Ractor:#2 rac.rb:1 blocking>: ping
74 # Client sends to #<Ractor:#2 rac.rb:1 blocking>: pong
75 # Server received: pong
77 # It is said that Ractor receives messages via the <em>incoming port</em>, and sends them
78 # to the <em>outgoing port</em>. Either one can be disabled with Ractor#close_incoming and
79 # Ractor#close_outgoing respectively. If a ractor terminated, its ports will be closed
82 # == Shareable and unshareable objects
84 # When the object is sent to and from the ractor, it is important to understand whether the
85 # object is shareable or unshareable. Most of objects are unshareable objects.
87 # Shareable objects are basically those which can be used by several threads without compromising
88 # thread-safety; e.g. immutable ones. Ractor.shareable? allows to check this, and Ractor.make_shareable
89 # tries to make object shareable if it is not.
91 # Ractor.shareable?(1) #=> true -- numbers and other immutable basic values are
92 # Ractor.shareable?('foo') #=> false, unless the string is frozen due to # freeze_string_literals: true
93 # Ractor.shareable?('foo'.freeze) #=> true
95 # ary = ['hello', 'world']
96 # ary.frozen? #=> false
97 # ary[0].frozen? #=> false
98 # Ractor.make_shareable(ary)
99 # ary.frozen? #=> true
100 # ary[0].frozen? #=> true
101 # ary[1].frozen? #=> true
103 # When a shareable object is sent (via #send or Ractor.yield), no additional processing happens,
104 # and it just becomes usable by both ractors. When an unshareable object is sent, it can be
105 # either _copied_ or _moved_. The first is the default, and it makes the object's full copy by
106 # deep cloning of non-shareable parts of its structure.
108 # data = ['foo', 'bar'.freeze]
110 # data2 = Ractor.receive
111 # puts "In ractor: #{data2.object_id}, #{data2[0].object_id}, #{data2[1].object_id}"
115 # puts "Outside : #{data.object_id}, #{data[0].object_id}, #{data[1].object_id}"
119 # In ractor: 340, 360, 320
120 # Outside : 380, 400, 320
122 # (Note that object id of both array and non-frozen string inside array have changed inside
123 # the ractor, showing it is different objects. But the second array's element, which is a
124 # shareable frozen string, has the same object_id.)
126 # Deep cloning of the objects may be slow, and sometimes impossible. Alternatively,
127 # <tt>move: true</tt> may be used on sending. This will <em>move</em> the object to the
128 # receiving ractor, making it inaccessible for a sending ractor.
130 # data = ['foo', 'bar']
132 # data_in_ractor = Ractor.receive
133 # puts "In ractor: #{data_in_ractor.object_id}, #{data_in_ractor[0].object_id}"
135 # r.send(data, move: true)
137 # puts "Outside: moved? #{Ractor::MovedObject === data}"
138 # puts "Outside: #{data.inspect}"
142 # In ractor: 100, 120
143 # Outside: moved? true
144 # test.rb:9:in `method_missing': can not send any methods to a moved object (Ractor::MovedError)
146 # Notice that even +inspect+ (and more basic methods like <tt>__id__</tt>) is inaccessible
149 # Besides frozen objects, there are shareable objects. Class and Module objects are shareable so
150 # the Class/Module definitions are shared between ractors. Ractor objects are also shareable objects.
151 # All operations for the shareable mutable objects are thread-safe, so the thread-safety property
152 # will be kept. We can not define mutable shareable objects in Ruby, but C extensions can introduce them.
154 # It is prohibited to access instance variables of mutable shareable objects (especially Modules and classes)
155 # from ractors other than main:
159 # attr_accessor :tricky
165 # r = Ractor.new(C) do |cls|
166 # puts "I see #{cls}"
167 # puts "I can't see #{cls.tricky}"
171 # # can not access instance variables of classes/modules from non-main Ractors (RuntimeError)
173 # Ractors can access constants if they are shareable. The main Ractor is the only one that can
174 # access non-shareable constants.
176 # GOOD = 'good'.freeze
180 # puts "GOOD=#{GOOD}"
185 # # can not access non-shareable objects in constant Object::BAD by non-main Ractor. (NameError)
187 # # Consider the same C class from above
191 # puts "I can't see #{C.tricky}"
195 # # can not access instance variables of classes/modules from non-main Ractors (RuntimeError)
197 # See also the description of <tt># shareable_constant_value</tt> pragma in
198 # {Comments syntax}[rdoc-ref:syntax/comments.rdoc] explanation.
200 # == Ractors vs threads
202 # Each ractor creates its own thread. New threads can be created from inside ractor
203 # (and, on CRuby, sharing GVL with other threads of this ractor).
207 # Thread.new {puts "Thread in ractor: a=#{a}"}.join
210 # # Here "Thread in ractor: a=1" will be printed
212 # == Note on code examples
214 # In examples below, sometimes we use the following method to wait till ractors that
215 # are not currently blocked will finish (or process till next blocking) method.
221 # It is **only for demonstration purposes** and shouldn't be used in a real code.
222 # Most of the times, just #take is used to wait till ractor will finish.
226 # See {Ractor design doc}[rdoc-ref:ractor.md] for more details.
231 # Ractor.new(*args, name: nil) {|*args| block } -> ractor
233 # Create a new Ractor with args and a block.
235 # A block (Proc) will be isolated (can't access to outer variables). +self+
236 # inside the block will refer to the current Ractor.
238 # r = Ractor.new { puts "Hi, I am #{self.inspect}" }
240 # # Prints "Hi, I am #<Ractor:#2 test.rb:1 running>"
242 # +args+ passed to the method would be propagated to block args by the same rules as
243 # objects passed through #send/Ractor.receive: if +args+ are not shareable, they
244 # will be copied (via deep cloning, which might be inefficient).
247 # puts "Passing: #{arg} (##{arg.object_id})"
248 # r = Ractor.new(arg) {|received_arg|
249 # puts "Received: #{received_arg} (##{received_arg.object_id})"
253 # # Passing: [1, 2, 3] (#280)
254 # # Received: [1, 2, 3] (#300)
256 # Ractor's +name+ can be set for debugging purposes:
258 # r = Ractor.new(name: 'my ractor') {}
260 # #=> #<Ractor:#3 my ractor test.rb:1 terminated>
262 def self.new(*args, name: nil, &block)
263 b = block # TODO: builtin bug
264 raise ArgumentError, "must be called with a block" unless block
265 loc = caller_locations(1, 1).first
266 loc = "#{loc.path}:#{loc.lineno}"
267 __builtin_ractor_create(loc, name, args, b)
270 # Returns the currently executing Ractor.
272 # Ractor.current #=> #<Ractor:#1 running>
275 rb_ractor_self(rb_ec_ractor_ptr(ec));
279 # Returns total count of Ractors currently running.
282 # r = Ractor.new(name: 'example') { Ractor.yield(1) }
283 # Ractor.count #=> 2 (main + example ractor)
284 # r.take # wait for Ractor.yield(1)
285 # r.take # wait till r will finish
289 ULONG2NUM(GET_VM()->ractor.cnt);
295 # Ractor.select(*ractors, [yield_value:, move: false]) -> [ractor or symbol, obj]
297 # Waits for the first ractor to have something in its outgoing port, reads from this ractor, and
298 # returns that ractor and the object received.
300 # r1 = Ractor.new {Ractor.yield 'from 1'}
301 # r2 = Ractor.new {Ractor.yield 'from 2'}
303 # r, obj = Ractor.select(r1, r2)
305 # puts "received #{obj.inspect} from #{r.inspect}"
306 # # Prints: received "from 1" from #<Ractor:#2 test.rb:1 running>
308 # If one of the given ractors is the current ractor, and it would be selected, +r+ will contain
309 # +:receive+ symbol instead of the ractor object.
311 # r1 = Ractor.new(Ractor.current) do |main|
312 # main.send 'to main'
313 # Ractor.yield 'from 1'
316 # Ractor.yield 'from 2'
319 # r, obj = Ractor.select(r1, r2, Ractor.current)
320 # puts "received #{obj.inspect} from #{r.inspect}"
321 # # Prints: received "to main" from :receive
323 # If +yield_value+ is provided, that value may be yielded if another Ractor is calling #take.
324 # In this case, the pair <tt>[:yield, nil]</tt> would be returned:
326 # r1 = Ractor.new(Ractor.current) do |main|
327 # puts "Received from main: #{main.take}"
330 # puts "Trying to select"
331 # r, obj = Ractor.select(r1, Ractor.current, yield_value: 123)
333 # puts "Received #{obj.inspect} from #{r.inspect}"
338 # Received from main: 123
339 # Received nil from :yield
341 # +move+ boolean flag defines whether yielded value should be copied (default) or moved.
342 def self.select(*ractors, yield_value: yield_unspecified = true, move: false)
343 raise ArgumentError, 'specify at least one ractor or `yield_value`' if yield_unspecified && ractors.empty?
346 const VALUE *rs = RARRAY_CONST_PTR_TRANSIENT(ractors);
348 VALUE v = ractor_select(ec, rs, RARRAY_LENINT(ractors),
349 yield_unspecified == Qtrue ? Qundef : yield_value,
350 (bool)RTEST(move) ? true : false, &rv);
351 return rb_ary_new_from_args(2, rv, v);
357 # Ractor.receive -> msg
359 # Receive an incoming message from the current Ractor's incoming port's queue, which was
360 # sent there by #send.
363 # v1 = Ractor.receive
364 # puts "Received: #{v1}"
368 # # Here will be printed: "Received: message1"
370 # Alternatively, private instance method +receive+ may be used:
374 # puts "Received: #{v1}"
378 # # Here will be printed: "Received: message1"
380 # The method blocks if the queue is empty.
383 # puts "Before first receive"
384 # v1 = Ractor.receive
385 # puts "Received: #{v1}"
386 # v2 = Ractor.receive
387 # puts "Received: #{v2}"
390 # puts "Still not received"
393 # puts "Still received only one"
399 # Before first receive
402 # Still received only one
405 # If close_incoming was called on the ractor, the method raises Ractor::ClosedError
406 # if there are no more messages in incoming queue:
413 # # in `receive': The incoming port is already closed => #<Ractor:#2 test.rb:1 running> (Ractor::ClosedError)
417 ractor_receive(ec, rb_ec_ractor_ptr(ec))
425 # same as Ractor.receive
428 ractor_receive(ec, rb_ec_ractor_ptr(ec))
435 # Ractor.receive_if {|msg| block } -> msg
437 # Receive only a specific message.
439 # Instead of Ractor.receive, Ractor.receive_if can provide a pattern
440 # by a block and you can choose the receiving message.
443 # p Ractor.receive_if{|msg| msg.match?(/foo/)} #=> "foo3"
444 # p Ractor.receive_if{|msg| msg.match?(/bar/)} #=> "bar1"
445 # p Ractor.receive_if{|msg| msg.match?(/baz/)} #=> "baz2"
458 # If the block returns a truthy value, the message will be removed from the incoming queue
460 # Otherwise, the message remains in the incoming queue and the following received
461 # messages are checked by the given block.
463 # If there are no messages left in the incoming queue, the method will
464 # block until new messages arrive.
466 # If the block is escaped by break/return/exception/throw, the message is removed from
467 # the incoming queue as if a truthy value had been returned.
470 # val = Ractor.receive_if{|msg| msg.is_a?(Array)}
471 # puts "Received successfully: #{val}"
477 # puts "2 non-matching sent, nothing received"
483 # 2 non-matching sent, nothing received
484 # Received successfully: [1, 2, 3]
486 # Note that you can not call receive/receive_if in the given block recursively.
487 # It means that you should not do any tasks in the block.
489 # Ractor.current << true
490 # Ractor.receive_if{|msg| Ractor.receive}
491 # #=> `receive': can not call receive/receive_if recursively (Ractor::Error)
493 def self.receive_if &b
494 Primitive.ractor_receive_if b
497 private def receive_if &b
498 Primitive.ractor_receive_if b
503 # ractor.send(msg, move: false) -> self
505 # Send a message to a Ractor's incoming queue to be consumed by Ractor.receive.
508 # value = Ractor.receive
509 # puts "Received #{value}"
512 # # Prints: "Received: message"
514 # The method is non-blocking (will return immediately even if the ractor is not ready
515 # to receive anything):
517 # r = Ractor.new {sleep(5)}
519 # puts "Sent successfully"
520 # # Prints: "Sent successfully" immediately
522 # Attempt to send to ractor which already finished its execution will raise Ractor::ClosedError.
527 # # "#<Ractor:#6 (irb):23 terminated>"
529 # # Ractor::ClosedError (The incoming-port is already closed)
531 # If close_incoming was called on the ractor, the method also raises Ractor::ClosedError.
539 # # Ractor::ClosedError (The incoming-port is already closed)
540 # # The error would be raised immediately, not when ractor will try to receive
542 # If the +obj+ is unshareable, by default it would be copied into ractor by deep cloning.
543 # If the <tt>move: true</tt> is passed, object is _moved_ into ractor and becomes
544 # inaccessible to sender.
546 # r = Ractor.new {puts "Received: #{receive}"}
548 # r.send(msg, move: true)
555 # in `p': undefined method `inspect' for #<Ractor::MovedObject:0x000055c99b9b69b8>
557 # All references to the object and its parts will become invalid in sender.
559 # r = Ractor.new {puts "Received: #{receive}"}
563 # r.send(ary, move: true)
566 # # Ractor::MovedError (can not send any methods to a moved object)
568 # # Ractor::MovedError (can not send any methods to a moved object)
570 # # => Array, it is different object
572 # # Ractor::MovedError (can not send any methods to a moved object)
573 # # ...but its item was still a reference to `s`, which was moved
575 # If the object was shareable, <tt>move: true</tt> has no effect on it:
577 # r = Ractor.new {puts "Received: #{receive}"}
578 # s = 'message'.freeze
579 # r.send(s, move: true)
580 # s.inspect #=> "message", still available
582 def send(obj, move: false)
584 ractor_send(ec, RACTOR_PTR(self), obj, move)
591 # Ractor.yield(msg, move: false) -> nil
593 # Send a message to the current ractor's outgoing port to be consumed by #take.
595 # r = Ractor.new {Ractor.yield 'Hello from ractor'}
597 # # Prints: "Hello from ractor"
599 # The method is blocking, and will return only when somebody consumes the
603 # Ractor.yield 'Hello from ractor'
604 # puts "Ractor: after yield"
607 # puts "Still not taken"
614 # Ractor: after yield
616 # If the outgoing port was closed with #close_outgoing, the method will raise:
620 # Ractor.yield 'Hello from ractor'
623 # # `yield': The outgoing-port is already closed (Ractor::ClosedError)
625 # The meaning of +move+ argument is the same as for #send.
626 def self.yield(obj, move: false)
628 ractor_yield(ec, rb_ec_ractor_ptr(ec), obj, move)
636 # Take a message from ractor's outgoing port, which was put there by Ractor.yield or at ractor's
640 # Ractor.yield 'explicit yield'
643 # puts r.take #=> 'explicit yield'
644 # puts r.take #=> 'last value'
645 # puts r.take # Ractor::ClosedError (The outgoing-port is already closed)
647 # The fact that the last value is also put to outgoing port means that +take+ can be used
648 # as some analog of Thread#join ("just wait till ractor finishes"), but don't forget it
649 # will raise if somebody had already consumed everything ractor have produced.
651 # If the outgoing port was closed with #close_outgoing, the method will raise Ractor::ClosedError.
655 # Ractor.yield 'Hello from ractor'
659 # # Ractor::ClosedError (The outgoing-port is already closed)
660 # # The error would be raised immediately, not when ractor will try to receive
662 # If an uncaught exception is raised in the Ractor, it is propagated on take as a
663 # Ractor::RemoteError.
665 # r = Ractor.new {raise "Something weird happened"}
670 # p e # => #<Ractor::RemoteError: thrown by remote Ractor.>
671 # p e.ractor == r # => true
672 # p e.cause # => #<RuntimeError: Something weird happened>
675 # Ractor::ClosedError is a descendant of StopIteration, so the closing of the ractor will break
676 # the loops without propagating the error:
679 # 3.times {|i| Ractor.yield "message #{i}"}
683 # loop {puts "Received: " + r.take}
684 # puts "Continue successfully"
688 # Received: message 0
689 # Received: message 1
690 # Received: message 2
691 # Received: finishing
692 # Continue successfully
695 ractor_take(ec, RACTOR_PTR(self))
700 loc = __builtin_cexpr! %q{ RACTOR_PTR(self)->loc }
701 name = __builtin_cexpr! %q{ RACTOR_PTR(self)->name }
702 id = __builtin_cexpr! %q{ INT2FIX(rb_ractor_id(RACTOR_PTR(self))) }
703 status = __builtin_cexpr! %q{
704 rb_str_new2(ractor_status_str(RACTOR_PTR(self)->status_))
706 "#<Ractor:##{id}#{name ? ' '+name : ''}#{loc ? " " + loc : ''} #{status}>"
711 # The name set in Ractor.new, or +nil+.
713 __builtin_cexpr! %q{RACTOR_PTR(self)->name}
722 # ractor.close_incoming -> true | false
724 # Closes the incoming port and returns its previous state.
725 # All further attempts to Ractor.receive in the ractor, and #send to the ractor
726 # will fail with Ractor::ClosedError.
728 # r = Ractor.new {sleep(500)}
729 # r.close_incoming #=> false
730 # r.close_incoming #=> true
732 # # Ractor::ClosedError (The incoming-port is already closed)
735 ractor_close_incoming(ec, RACTOR_PTR(self));
741 # ractor.close_outgoing -> true | false
743 # Closes the outgoing port and returns its previous state.
744 # All further attempts to Ractor.yield in the ractor, and #take from the ractor
745 # will fail with Ractor::ClosedError.
747 # r = Ractor.new {sleep(500)}
748 # r.close_outgoing #=> false
749 # r.close_outgoing #=> true
751 # # Ractor::ClosedError (The outgoing-port is already closed)
754 ractor_close_outgoing(ec, RACTOR_PTR(self));
760 # Ractor.shareable?(obj) -> true | false
762 # Checks if the object is shareable by ractors.
764 # Ractor.shareable?(1) #=> true -- numbers and other immutable basic values are frozen
765 # Ractor.shareable?('foo') #=> false, unless the string is frozen due to # freeze_string_literals: true
766 # Ractor.shareable?('foo'.freeze) #=> true
768 # See also the "Shareable and unshareable objects" section in the Ractor class docs.
769 def self.shareable? obj
771 RBOOL(rb_ractor_shareable_p(obj));
777 # Ractor.make_shareable(obj, copy: false) -> shareable_obj
779 # Make +obj+ shareable between ractors.
781 # +obj+ and all the objects it refers to will be frozen, unless they are
784 # If +copy+ keyword is +true+, the method will copy objects before freezing them
785 # This is safer option but it can take be slower.
787 # Note that the specification and implementation of this method are not
788 # mature and may be changed in the future.
791 # Ractor.shareable?(obj) #=> false
792 # Ractor.make_shareable(obj) #=> ["test"]
793 # Ractor.shareable?(obj) #=> true
794 # obj.frozen? #=> true
795 # obj[0].frozen? #=> true
797 # # Copy vs non-copy versions:
799 # obj1s = Ractor.make_shareable(obj1)
800 # obj1.frozen? #=> true
801 # obj1s.object_id == obj1.object_id #=> true
803 # obj2s = Ractor.make_shareable(obj2, copy: true)
804 # obj2.frozen? #=> false
805 # obj2s.frozen? #=> true
806 # obj2s.object_id == obj2.object_id #=> false
807 # obj2s[0].object_id == obj2[0].object_id #=> false
809 # See also the "Shareable and unshareable objects" section in the Ractor class docs.
810 def self.make_shareable obj, copy: false
813 rb_ractor_make_shareable_copy(obj);
817 rb_ractor_make_shareable(obj);
822 # get a value from ractor-local storage
824 Primitive.ractor_local_value(sym)
827 # set a value in ractor-local storage
829 Primitive.ractor_local_value_set(sym, val)
832 # returns main ractor
835 rb_ractor_self(GET_VM()->ractor.main_ractor);