1 # \Ractor is an Actor-model abstraction for Ruby that provides thread-safe parallel execution.
3 # Ractor.new makes a new \Ractor, which can 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!" is printed
10 # Ractors do not share all objects with each other. There are two main benefits to this: across ractors, thread-safety
11 # concerns such as data-races and race-conditions are not possible. The other benefit is parallelism.
13 # To achieve this, object sharing is limited across ractors.
14 # For example, unlike in threads, ractors can't access all the objects available in other ractors. Even objects normally
15 # available through variables in the outer scope are prohibited from being used across ractors.
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 # The object must be explicitly shared:
24 # r = Ractor.new(a) { |a1| puts "I am in Ractor! a=#{a1}"}
26 # On CRuby (the default implementation), Global Virtual Machine Lock (GVL) is held per ractor, so
27 # ractors can perform in parallel without locking each other. This is unlike the situation with threads
30 # Instead of accessing shared state, objects should be passed to and from ractors by
31 # sending and receiving them as messages.
35 # a_in_ractor = receive # receive blocks until somebody passes a message
36 # puts "I am in Ractor! a=#{a_in_ractor}"
40 # # Here, "I am in Ractor! a=1" is printed
42 # There are two pairs of methods for sending/receiving messages:
44 # * Ractor#send and Ractor.receive for when the _sender_ knows the receiver (push);
45 # * Ractor.yield and Ractor#take for when the _receiver_ knows the sender (pull);
47 # In addition to that, any arguments passed to Ractor.new are passed to the block and available there
48 # as if received by Ractor.receive, and the last block value is sent outside of the
49 # ractor as if sent by Ractor.yield.
51 # A little demonstration of a classic ping-pong:
53 # server = Ractor.new(name: "server") do
54 # puts "Server starts: #{self.inspect}"
55 # puts "Server sends: ping"
56 # Ractor.yield 'ping' # The server doesn't know the receiver and sends to whoever interested
57 # received = Ractor.receive # The server doesn't know the sender and receives from whoever sent
58 # puts "Server received: #{received}"
61 # client = Ractor.new(server) do |srv| # The server is sent to the client, and available as srv
62 # puts "Client starts: #{self.inspect}"
63 # received = srv.take # The client takes a message from the server
64 # puts "Client received from " \
65 # "#{srv.inspect}: #{received}"
66 # puts "Client sends to " \
67 # "#{srv.inspect}: pong"
68 # srv.send 'pong' # The client sends a message to the server
71 # [client, server].each(&:take) # Wait until they both finish
73 # This will output something like:
75 # Server starts: #<Ractor:#2 server test.rb:1 running>
77 # Client starts: #<Ractor:#3 test.rb:8 running>
78 # Client received from #<Ractor:#2 server test.rb:1 blocking>: ping
79 # Client sends to #<Ractor:#2 server test.rb:1 blocking>: pong
80 # Server received: pong
82 # Ractors receive their messages via the <em>incoming port</em>, and send them
83 # to the <em>outgoing port</em>. Either one can be disabled with Ractor#close_incoming and
84 # Ractor#close_outgoing, respectively. When a ractor terminates, its ports are closed
87 # == Shareable and unshareable objects
89 # When an object is sent to and from a ractor, it's important to understand whether the
90 # object is shareable or unshareable. Most Ruby objects are unshareable objects. Even
91 # frozen objects can be unshareable if they contain (through their instance variables) unfrozen
94 # Shareable objects are those which can be used by several threads without compromising
95 # thread-safety, for example numbers, +true+ and +false+. Ractor.shareable? allows you to check this,
96 # and Ractor.make_shareable tries to make the object shareable if it's not already, and gives an error
99 # Ractor.shareable?(1) #=> true -- numbers and other immutable basic values are shareable
100 # Ractor.shareable?('foo') #=> false, unless the string is frozen due to # frozen_string_literal: true
101 # Ractor.shareable?('foo'.freeze) #=> true
102 # Ractor.shareable?([Object.new].freeze) #=> false, inner object is unfrozen
104 # ary = ['hello', 'world']
105 # ary.frozen? #=> false
106 # ary[0].frozen? #=> false
107 # Ractor.make_shareable(ary)
108 # ary.frozen? #=> true
109 # ary[0].frozen? #=> true
110 # ary[1].frozen? #=> true
112 # When a shareable object is sent (via #send or Ractor.yield), no additional processing occurs
113 # on it. It just becomes usable by both ractors. When an unshareable object is sent, it can be
114 # either _copied_ or _moved_. The first is the default, and it copies the object fully by
115 # deep cloning (Object#clone) the non-shareable parts of its structure.
117 # data = ['foo', 'bar'.freeze]
119 # data2 = Ractor.receive
120 # puts "In ractor: #{data2.object_id}, #{data2[0].object_id}, #{data2[1].object_id}"
124 # puts "Outside : #{data.object_id}, #{data[0].object_id}, #{data[1].object_id}"
126 # This will output something like:
128 # In ractor: 340, 360, 320
129 # Outside : 380, 400, 320
131 # Note that the object ids of the array and the non-frozen string inside the array have changed in
132 # the ractor because they are different objects. The second array's element, which is a
133 # shareable frozen string, is the same object.
135 # Deep cloning of objects may be slow, and sometimes impossible. Alternatively, <tt>move: true</tt> may
136 # be used during sending. This will <em>move</em> the unshareable object to the receiving ractor, making it
137 # inaccessible to the sending ractor.
139 # data = ['foo', 'bar']
141 # data_in_ractor = Ractor.receive
142 # puts "In ractor: #{data_in_ractor.object_id}, #{data_in_ractor[0].object_id}"
144 # r.send(data, move: true)
146 # puts "Outside: moved? #{Ractor::MovedObject === data}"
147 # puts "Outside: #{data.inspect}"
151 # In ractor: 100, 120
152 # Outside: moved? true
153 # test.rb:9:in `method_missing': can not send any methods to a moved object (Ractor::MovedError)
155 # Notice that even +inspect+ (and more basic methods like <tt>__id__</tt>) is inaccessible
158 # Class and Module objects are shareable so the class/module definitions are shared between ractors.
159 # \Ractor objects are also shareable. All operations on shareable objects are thread-safe, so the thread-safety property
160 # will be kept. We can not define mutable shareable objects in Ruby, but C extensions can introduce them.
162 # It is prohibited to access (get) instance variables of shareable objects in other ractors if the values of the
163 # variables aren't shareable. This can occur because modules/classes are shareable, but they can have
164 # instance variables whose values are not. In non-main ractors, it's also prohibited to set instance
165 # variables on classes/modules (even if the value is shareable).
169 # attr_accessor :tricky
173 # C.tricky = "unshareable".dup
175 # r = Ractor.new(C) do |cls|
176 # puts "I see #{cls}"
177 # puts "I can't see #{cls.tricky}"
178 # cls.tricky = true # doesn't get here, but this would also raise an error
182 # # can not access instance variables of classes/modules from non-main Ractors (RuntimeError)
184 # Ractors can access constants if they are shareable. The main \Ractor is the only one that can
185 # access non-shareable constants.
187 # GOOD = 'good'.freeze
191 # puts "GOOD=#{GOOD}"
196 # # can not access non-shareable objects in constant Object::BAD by non-main Ractor. (NameError)
198 # # Consider the same C class from above
202 # puts "I can't see #{C.tricky}"
206 # # can not access instance variables of classes/modules from non-main Ractors (RuntimeError)
208 # See also the description of <tt># shareable_constant_value</tt> pragma in
209 # {Comments syntax}[rdoc-ref:syntax/comments.rdoc] explanation.
211 # == Ractors vs threads
213 # Each ractor has its own main Thread. New threads can be created from inside ractors
214 # (and, on CRuby, they share the GVL with other threads of this ractor).
218 # Thread.new {puts "Thread in ractor: a=#{a}"}.join
221 # # Here "Thread in ractor: a=1" will be printed
223 # == Note on code examples
225 # In the examples below, sometimes we use the following method to wait for ractors that
226 # are not currently blocked to finish (or to make progress).
232 # It is **only for demonstration purposes** and shouldn't be used in a real code.
233 # Most of the time, #take is used to wait for ractors to finish.
237 # See {Ractor design doc}[rdoc-ref:ractor.md] for more details.
242 # Ractor.new(*args, name: nil) {|*args| block } -> ractor
244 # Create a new \Ractor with args and a block.
246 # The given block (Proc) will be isolated (can't access any outer variables). +self+
247 # inside the block will refer to the current \Ractor.
249 # r = Ractor.new { puts "Hi, I am #{self.inspect}" }
251 # # Prints "Hi, I am #<Ractor:#2 test.rb:1 running>"
253 # Any +args+ passed are propagated to the block arguments by the same rules as
254 # objects sent via #send/Ractor.receive. If an argument in +args+ is not shareable, it
255 # will be copied (via deep cloning, which might be inefficient).
258 # puts "Passing: #{arg} (##{arg.object_id})"
259 # r = Ractor.new(arg) {|received_arg|
260 # puts "Received: #{received_arg} (##{received_arg.object_id})"
264 # # Passing: [1, 2, 3] (#280)
265 # # Received: [1, 2, 3] (#300)
267 # Ractor's +name+ can be set for debugging purposes:
269 # r = Ractor.new(name: 'my ractor') {}; r.take
271 # #=> #<Ractor:#3 my ractor test.rb:1 terminated>
273 def self.new(*args, name: nil, &block)
274 b = block # TODO: builtin bug
275 raise ArgumentError, "must be called with a block" unless block
276 if __builtin_cexpr!("RBOOL(ruby_single_main_ractor)")
277 warn("Ractor is experimental, and the behavior may change in future versions of Ruby! " \
278 "Also there are many implementation issues.", uplevel: 0, category: :experimental)
280 loc = caller_locations(1, 1).first
281 loc = "#{loc.path}:#{loc.lineno}"
282 __builtin_ractor_create(loc, name, args, b)
285 # Returns the currently executing Ractor.
287 # Ractor.current #=> #<Ractor:#1 running>
290 rb_ractor_self(rb_ec_ractor_ptr(ec));
294 # Returns the number of Ractors currently running or blocking (waiting).
297 # r = Ractor.new(name: 'example') { Ractor.yield(1) }
298 # Ractor.count #=> 2 (main + example ractor)
299 # r.take # wait for Ractor.yield(1)
300 # r.take # wait until r will finish
304 ULONG2NUM(GET_VM()->ractor.cnt);
310 # Ractor.select(*ractors, [yield_value:, move: false]) -> [ractor or symbol, obj]
312 # Wait for any ractor to have something in its outgoing port, read from this ractor, and
313 # then return that ractor and the object received.
315 # r1 = Ractor.new {Ractor.yield 'from 1'}
316 # r2 = Ractor.new {Ractor.yield 'from 2'}
318 # r, obj = Ractor.select(r1, r2)
320 # puts "received #{obj.inspect} from #{r.inspect}"
321 # # Prints: received "from 1" from #<Ractor:#2 test.rb:1 running>
322 # # But could just as well print "from r2" here, either prints could be first.
324 # If one of the given ractors is the current ractor, and it is selected, +r+ will contain
325 # the +:receive+ symbol instead of the ractor object.
327 # r1 = Ractor.new(Ractor.current) do |main|
328 # main.send 'to main'
329 # Ractor.yield 'from 1'
332 # Ractor.yield 'from 2'
335 # r, obj = Ractor.select(r1, r2, Ractor.current)
336 # puts "received #{obj.inspect} from #{r.inspect}"
337 # # Could print: received "to main" from :receive
339 # If +yield_value+ is provided, that value may be yielded if another ractor is calling #take.
340 # In this case, the pair <tt>[:yield, nil]</tt> is returned:
342 # r1 = Ractor.new(Ractor.current) do |main|
343 # puts "Received from main: #{main.take}"
346 # puts "Trying to select"
347 # r, obj = Ractor.select(r1, Ractor.current, yield_value: 123)
349 # puts "Received #{obj.inspect} from #{r.inspect}"
354 # Received from main: 123
355 # Received nil from :yield
357 # +move+ boolean flag defines whether yielded value will be copied (default) or moved.
358 def self.select(*ractors, yield_value: yield_unspecified = true, move: false)
359 raise ArgumentError, 'specify at least one ractor or `yield_value`' if yield_unspecified && ractors.empty?
361 if ractors.delete Ractor.current
367 __builtin_ractor_select_internal ractors, do_receive, !yield_unspecified, yield_value, move
372 # Ractor.receive -> msg
374 # Receive a message from the incoming port of the current ractor (which was
375 # sent there by #send from another ractor).
378 # v1 = Ractor.receive
379 # puts "Received: #{v1}"
383 # # Here will be printed: "Received: message1"
385 # Alternatively, the private instance method +receive+ may be used:
389 # puts "Received: #{v1}"
393 # # This prints: "Received: message1"
395 # The method blocks if the queue is empty.
398 # puts "Before first receive"
399 # v1 = Ractor.receive
400 # puts "Received: #{v1}"
401 # v2 = Ractor.receive
402 # puts "Received: #{v2}"
405 # puts "Still not received"
408 # puts "Still received only one"
414 # Before first receive
417 # Still received only one
420 # If close_incoming was called on the ractor, the method raises Ractor::ClosedError
421 # if there are no more messages in the incoming queue:
428 # # in `receive': The incoming port is already closed => #<Ractor:#2 test.rb:1 running> (Ractor::ClosedError)
432 ractor_receive(ec, rb_ec_ractor_ptr(ec))
440 # same as Ractor.receive
443 ractor_receive(ec, rb_ec_ractor_ptr(ec))
450 # Ractor.receive_if {|msg| block } -> msg
452 # Receive only a specific message.
454 # Instead of Ractor.receive, Ractor.receive_if can be given a pattern (or any
455 # filter) in a block and you can choose the messages to accept that are available in
456 # your ractor's incoming queue.
459 # p Ractor.receive_if{|msg| msg.match?(/foo/)} #=> "foo3"
460 # p Ractor.receive_if{|msg| msg.match?(/bar/)} #=> "bar1"
461 # p Ractor.receive_if{|msg| msg.match?(/baz/)} #=> "baz2"
474 # If the block returns a truthy value, the message is removed from the incoming queue
476 # Otherwise, the message remains in the incoming queue and the next messages are checked
477 # by the given block.
479 # If there are no messages left in the incoming queue, the method will
480 # block until new messages arrive.
482 # If the block is escaped by break/return/exception/throw, the message is removed from
483 # the incoming queue as if a truthy value had been returned.
486 # val = Ractor.receive_if{|msg| msg.is_a?(Array)}
487 # puts "Received successfully: #{val}"
493 # puts "2 non-matching sent, nothing received"
499 # 2 non-matching sent, nothing received
500 # Received successfully: [1, 2, 3]
502 # Note that you can not call receive/receive_if in the given block recursively.
503 # You should not do any tasks in the block other than message filtration.
505 # Ractor.current << true
506 # Ractor.receive_if{|msg| Ractor.receive}
507 # #=> `receive': can not call receive/receive_if recursively (Ractor::Error)
509 def self.receive_if &b
510 Primitive.ractor_receive_if b
513 # same as Ractor.receive_if
514 private def receive_if &b
515 Primitive.ractor_receive_if b
520 # ractor.send(msg, move: false) -> self
522 # Send a message to a Ractor's incoming queue to be accepted by Ractor.receive.
525 # value = Ractor.receive
526 # puts "Received #{value}"
529 # # Prints: "Received: message"
531 # The method is non-blocking (will return immediately even if the ractor is not ready
532 # to receive anything):
534 # r = Ractor.new {sleep(5)}
536 # puts "Sent successfully"
537 # # Prints: "Sent successfully" immediately
539 # An attempt to send to a ractor which already finished its execution will raise Ractor::ClosedError.
544 # # "#<Ractor:#6 (irb):23 terminated>"
546 # # Ractor::ClosedError (The incoming-port is already closed)
548 # If close_incoming was called on the ractor, the method also raises Ractor::ClosedError.
556 # # Ractor::ClosedError (The incoming-port is already closed)
557 # # The error is raised immediately, not when the ractor tries to receive
559 # If the +obj+ is unshareable, by default it will be copied into the receiving ractor by deep cloning.
560 # If <tt>move: true</tt> is passed, the object is _moved_ into the receiving ractor and becomes
561 # inaccessible to the sender.
563 # r = Ractor.new {puts "Received: #{receive}"}
565 # r.send(msg, move: true)
572 # in `p': undefined method `inspect' for #<Ractor::MovedObject:0x000055c99b9b69b8>
574 # All references to the object and its parts will become invalid to the sender.
576 # r = Ractor.new {puts "Received: #{receive}"}
580 # r.send(ary, move: true)
583 # # Ractor::MovedError (can not send any methods to a moved object)
585 # # Ractor::MovedError (can not send any methods to a moved object)
587 # # => Array, it is different object
589 # # Ractor::MovedError (can not send any methods to a moved object)
590 # # ...but its item was still a reference to `s`, which was moved
592 # If the object is shareable, <tt>move: true</tt> has no effect on it:
594 # r = Ractor.new {puts "Received: #{receive}"}
595 # s = 'message'.freeze
596 # r.send(s, move: true)
597 # s.inspect #=> "message", still available
599 def send(obj, move: false)
601 ractor_send(ec, RACTOR_PTR(self), obj, move)
608 # Ractor.yield(msg, move: false) -> nil
610 # Send a message to the current ractor's outgoing port to be accepted by #take.
612 # r = Ractor.new {Ractor.yield 'Hello from ractor'}
614 # # Prints: "Hello from ractor"
616 # This method is blocking, and will return only when somebody consumes the
620 # Ractor.yield 'Hello from ractor'
621 # puts "Ractor: after yield"
624 # puts "Still not taken"
631 # Ractor: after yield
633 # If the outgoing port was closed with #close_outgoing, the method will raise:
637 # Ractor.yield 'Hello from ractor'
640 # # `yield': The outgoing-port is already closed (Ractor::ClosedError)
642 # The meaning of the +move+ argument is the same as for #send.
643 def self.yield(obj, move: false)
645 ractor_yield(ec, rb_ec_ractor_ptr(ec), obj, move)
653 # Get a message from the ractor's outgoing port, which was put there by Ractor.yield or at ractor's
657 # Ractor.yield 'explicit yield'
660 # puts r.take #=> 'explicit yield'
661 # puts r.take #=> 'last value'
662 # puts r.take # Ractor::ClosedError (The outgoing-port is already closed)
664 # The fact that the last value is also sent to the outgoing port means that +take+ can be used
665 # as an analog of Thread#join ("just wait until ractor finishes"). However, it will raise if
666 # somebody has already consumed that message.
668 # If the outgoing port was closed with #close_outgoing, the method will raise Ractor::ClosedError.
672 # Ractor.yield 'Hello from ractor'
676 # # Ractor::ClosedError (The outgoing-port is already closed)
677 # # The error would be raised immediately, not when ractor will try to receive
679 # If an uncaught exception is raised in the Ractor, it is propagated by take as a
680 # Ractor::RemoteError.
682 # r = Ractor.new {raise "Something weird happened"}
687 # p e # => #<Ractor::RemoteError: thrown by remote Ractor.>
688 # p e.ractor == r # => true
689 # p e.cause # => #<RuntimeError: Something weird happened>
692 # Ractor::ClosedError is a descendant of StopIteration, so the termination of the ractor will break
693 # out of any loops that receive this message without propagating the error:
696 # 3.times {|i| Ractor.yield "message #{i}"}
700 # loop {puts "Received: " + r.take}
701 # puts "Continue successfully"
705 # Received: message 0
706 # Received: message 1
707 # Received: message 2
708 # Received: finishing
709 # Continue successfully
712 ractor_take(ec, RACTOR_PTR(self))
717 loc = __builtin_cexpr! %q{ RACTOR_PTR(self)->loc }
718 name = __builtin_cexpr! %q{ RACTOR_PTR(self)->name }
719 id = __builtin_cexpr! %q{ UINT2NUM(rb_ractor_id(RACTOR_PTR(self))) }
720 status = __builtin_cexpr! %q{
721 rb_str_new2(ractor_status_str(RACTOR_PTR(self)->status_))
723 "#<Ractor:##{id}#{name ? ' '+name : ''}#{loc ? " " + loc : ''} #{status}>"
728 # The name set in Ractor.new, or +nil+.
730 __builtin_cexpr! %q{RACTOR_PTR(self)->name}
739 # ractor.close_incoming -> true | false
741 # Closes the incoming port and returns whether it was already closed. All further attempts
742 # to Ractor.receive in the ractor, and #send to the ractor will fail with Ractor::ClosedError.
744 # r = Ractor.new {sleep(500)}
745 # r.close_incoming #=> false
746 # r.close_incoming #=> true
748 # # Ractor::ClosedError (The incoming-port is already closed)
751 ractor_close_incoming(ec, RACTOR_PTR(self));
757 # ractor.close_outgoing -> true | false
759 # Closes the outgoing port and returns whether it was already closed. All further attempts
760 # to Ractor.yield in the ractor, and #take from the ractor will fail with Ractor::ClosedError.
762 # r = Ractor.new {sleep(500)}
763 # r.close_outgoing #=> false
764 # r.close_outgoing #=> true
766 # # Ractor::ClosedError (The outgoing-port is already closed)
769 ractor_close_outgoing(ec, RACTOR_PTR(self));
775 # Ractor.shareable?(obj) -> true | false
777 # Checks if the object is shareable by ractors.
779 # Ractor.shareable?(1) #=> true -- numbers and other immutable basic values are frozen
780 # Ractor.shareable?('foo') #=> false, unless the string is frozen due to # frozen_string_literal: true
781 # Ractor.shareable?('foo'.freeze) #=> true
783 # See also the "Shareable and unshareable objects" section in the \Ractor class docs.
784 def self.shareable? obj
786 RBOOL(rb_ractor_shareable_p(obj));
792 # Ractor.make_shareable(obj, copy: false) -> shareable_obj
794 # Make +obj+ shareable between ractors.
796 # +obj+ and all the objects it refers to will be frozen, unless they are
799 # If +copy+ keyword is +true+, it will copy objects before freezing them, and will not
800 # modify +obj+ or its internal objects.
802 # Note that the specification and implementation of this method are not
803 # mature and may be changed in the future.
806 # Ractor.shareable?(obj) #=> false
807 # Ractor.make_shareable(obj) #=> ["test"]
808 # Ractor.shareable?(obj) #=> true
809 # obj.frozen? #=> true
810 # obj[0].frozen? #=> true
812 # # Copy vs non-copy versions:
814 # obj1s = Ractor.make_shareable(obj1)
815 # obj1.frozen? #=> true
816 # obj1s.object_id == obj1.object_id #=> true
818 # obj2s = Ractor.make_shareable(obj2, copy: true)
819 # obj2.frozen? #=> false
820 # obj2s.frozen? #=> true
821 # obj2s.object_id == obj2.object_id #=> false
822 # obj2s[0].object_id == obj2[0].object_id #=> false
824 # See also the "Shareable and unshareable objects" section in the Ractor class docs.
825 def self.make_shareable obj, copy: false
828 rb_ractor_make_shareable_copy(obj);
832 rb_ractor_make_shareable(obj);
837 # get a value from ractor-local storage
839 Primitive.ractor_local_value(sym)
842 # set a value in ractor-local storage
844 Primitive.ractor_local_value_set(sym, val)
847 # returns main ractor
850 rb_ractor_self(GET_VM()->ractor.main_ractor);