https://blog.appsignal.com/2022/08/24/an-introduction-to-ractors-in-ruby.html * Features Monitoring features + Error tracking + Performance monitoring + Host monitoring + Anomaly detection + Workflow + Metric dashboards + Uptime monitoring * Languages Supported Languages + Ruby (on Rails) APM + Elixir APM + Node.js APM + JavaScript Error Tracking * Docs * Blog * Pricing * Login * Start free trial Menu Monitoring features * Error tracking * Performance monitoring * Host monitoring * Anomaly detection * Workflow * Metric dashboards * Uptime monitoring Supported Languages * Ruby (on Rails) APM * Elixir APM * Node.js APM * JavaScript Error Tracking * Blog * Pricing * Login * Start free trial ruby An Introduction to Ractors in Ruby Abiodun OlowodeAbiodun Olowode Abiodun Olowode on Aug 24, 2022 An Introduction to Ractors in Ruby In this post, we'll dive into ractors in Ruby, exploring how to build a ractor. You'll send and receive messages in ractors, and learn about shareable and unshareable objects. But first, let's define the actor model and ractors, and consider when you should use ractors. What is the Actor Model? In computer science, the object-oriented model is very popular, and in the Ruby community, many people are used to the term 'everything is an object'. Similarly, let me introduce you to the actor model, within which 'everything is an actor'. The actor model is a mathematical model of concurrent computation in which the universal primitive/fundamental agent of computation is an actor. An actor is capable of the following: * Receiving messages and responding to the sender * Sending messages to other actors * Determining how to respond to the next message received * Creating several other actors * Making local decisions * Performing actions (e.g., mutating data in a database) Actors communicate via messages, process one message at a time, and maintain their own private state. However, they can modify this state via messages received, eliminating the need for a lock or mutex. Received messages are processed one message at a time in the order of FIFO (first in, first out). The message sender is decoupled (isolated) from the sent communication, enabling asynchronous communication. A few examples of the actor model implementation are akka, elixir, pulsar, celluloid, and ractors. A few examples of concurrency models include threads, processes, and futures. What Are Ractors in Ruby? Ractor is an actor-model abstraction that provides a parallel execution feature without thread-safety concerns. Just like threads, ractors provide true parallelism. However, unlike threads, they do not share everything. Most objects are unshareable, and when they are made shareable, are protected by an interpreter or locking mechanism. Ractors are also unable to access any objects through variables not defined within their scope. This means that we can be free of the possibility of race conditions. In 2015, when Ruby 3.0.0 was released, these were the words of Matz: It's multi-core age today. Concurrency is very important. With Ractor, along with Async Fiber, Ruby will be a real concurrent language. Ractors do not claim to have solved all thread-safety problems. In the Ractor documentation, the following is clearly stated: There are several blocking operations (waiting send, waiting yield, and waiting take) so you can make a program which has dead-lock and live-lock issues. Some kind of shareable objects can introduce transactions (STM, for example). However, misusing transactions will generate inconsistent state. Without ractors, you need to trace all state mutations to debug thread-safety issues. However, the beauty of ractors is that we can concentrate our efforts on suspicious shared code. When and Why Should I Use Ractors in Ruby? When you create a ractor for the first time, you'll get a warning like this one: :267: warning: Ractor is experimental, and the behavior may change in future versions of Ruby! Also there are many implementation issues. However, that does not mean that you should avoid using ractors. Due to parallel execution, ractors can complete processes way faster than when processes are carried out synchronously. In the Ruby 3.0.0 release notes, you'll find this benchmark example of the Tak function, where it is executed sequentially four times, and four times in parallel with ractors: def tarai(x, y, z) = x <= y ? y : tarai(tarai(x-1, y, z), tarai(y-1, z, x), tarai(z-1, x, y)) require 'benchmark' Benchmark.bm do |x| # sequential version x.report('seq'){ 4.times{ tarai(14, 7, 0) } } # parallel version with ractors x.report('par'){ 4.times.map do Ractor.new { tarai(14, 7, 0) } end.each(&:take) } end The results are as follows: Benchmark result: user system total real seq 64.560736 0.001101 64.561837 ( 64.562194) par 66.422010 0.015999 66.438009 ( 16.685797) The Ruby 3.0.0 release notes state: The result was measured on Ubuntu 20.04, Intel(R) Core(TM) i7-6700 (4 cores, 8 hardware threads). It shows that the parallel version is 3.87 times faster than the sequential version. So if you need a faster process execution time that can run in parallel on machines with multiple cores, ractors are not a bad idea at all. Modifying class/module objects on multi-ractor programs can introduce race conditions and should be avoided as much as possible. However, most objects are unshareable, so the need to implement locks to prevent race conditions becomes obsolete. If objects are shareable, they are protected by an interpreter or locking mechanism. Creating Your First Ractor in Ruby Creating a ractor is as easy as creating any class instance. Call Ractor.new with a block -- Ractor.new { block }. This block is run in parallel with every other ractor. It is important to note that every example shown from this point onwards was performed in Ruby 3.1.2. r = Ractor.new { puts "This is my first ractor" } # This is my first ractor # create a ractor with a name r = Ractor.new name: 'second_ractor' do puts "This is my second ractor" end # This is my second ractor r.name # => "second_ractor" Arguments can also be passed to Ractor.new, and these arguments become parameters for the ractor block. my_array = [4,5,6] Ractor.new my_array do |arr| puts arr.each(&:to_s) end # 4 # 5 # 6 Recall how we talked about ractors being unable to access objects defined outside their scope? Let's see an example of that: outer_scope_object = "I am an outer scope object" Ractor.new do puts outer_scope_object end # :267:in `new': can not isolate a Proc because it accesses outer variables (outer_scope_object). (ArgumentError) We get an error on the invocation of .new, related to a Proc not being isolated. This is because Proc#isolate is called at a ractor's creation to prevent sharing unshareable objects. However, objects can be passed to and from ractors via messages. Sending and Receiving Messages in Ractors Ractors send messages via an outgoing port and receive messages via an incoming port. The incoming port can hold an infinite number of messages and runs on the FIFO principle. The .send method works the same way a mailman delivers a message in the mail. The mailman takes the message and drops it at the door (incoming port) of the ractor. However, dropping a message at a person's door is not enough to get them to open it. .receive is then available for the ractor to open the door and receive whatever message has been dropped. The ractor might want to do some computation with that message and return a response, so how do we get it? We ask the mailman to .take the response. tripple_number_ractor = Ractor.new do puts "I will receive a message soon" msg = Ractor.receive puts "I will return a tripple of what I receive" msg * 3 end # I will receive a message soon tripple_number_ractor.send(15) # mailman takes message to the door # I will return a tripple of what I receive tripple_number_ractor.take # mailman takes the response # => 45 As seen above, the return value of a ractor is also a sent message and can be received via .take. Since this is an outgoing message, it goes to the outgoing port. Here's a simple example: r = Ractor.new do 5**2 end r.take # => 25 Besides returning a message, a ractor can also send a message to its outgoing port via .yield. r = Ractor.new do squared = 5**2 Ractor.yield squared*2 puts "I just sent a message out" squared*3 end r.take # => 50 r.take # => 75 The first message sent to the outgoing port is squared*2, and the next message is squared*3. Therefore, when we call .take, we get 50 first. We have to call .take a second time to get 75 as two messages are sent to the outgoing port. Let's put this all together in one example of customers sending their orders to a supermarket and receiving the fulfilled orders: supermarket = Ractor.new do loop do order = Ractor.receive puts "The supermarket is preparing #{order}" Ractor.yield "This is #{order}" end end customers = 5.times.map{ |i| Ractor.new supermarket, i do |supermarket, i| supermarket.send("a pack of sugar for customer #{i}") fulfilled_order = supermarket.take puts "#{fulfilled_order} received by customer #{i}" end } The output is as follows: The supermarket is preparing a pack of sugar for customer 3 The supermarket is preparing a pack of sugar for customer 2 This is a pack of sugar for customer 3 received by customer 3 The supermarket is preparing a pack of sugar for customer 1 This is a pack of sugar for customer 2 received by customer 2 The supermarket is preparing a pack of sugar for customer 0 This is a pack of sugar for customer 1 received by customer 1 This is a pack of sugar for customer 0 received by customer 0 The supermarket is preparing a pack of sugar for customer 4 This is a pack of sugar for customer 4 received by customer 4 Running it a second time yields: The supermarket is preparing a pack of sugar for customer 0 This is a pack of sugar for customer 0 received by customer 0 The supermarket is preparing a pack of sugar for customer 4 This is a pack of sugar for customer 4 received by customer 4 The supermarket is preparing a pack of sugar for customer 1 This is a pack of sugar for customer 1 received by customer 1 The supermarket is preparing a pack of sugar for customer 3 The supermarket is preparing a pack of sugar for customer 2 This is a pack of sugar for customer 3 received by customer 3 This is a pack of sugar for customer 2 received by customer 2 The output can most definitely be in a different order every time we run this (because ractors run concurrently, as we have established). A few things to note about sending and receiving messages: * Messages can also be sent using << msg, instead of .send(msg). * You can add a condition to a .receive using receive_if. * When .send is called on a ractor that is already terminated (not running), you get a Ractor::ClosedError. * A ractor's outgoing port closes after .take is called on it if it runs just once (not in a loop). r = Ractor.new do Ractor.receive end # => # r << 5 # => # r.take # => 5 r << 9 # :583:in `send': The incoming-port is already closed (Ractor::ClosedError) r.take # :694:in `take': The outgoing-port is already closed (Ractor::ClosedError) * Objects can be moved to a destination ractor via .send(obj, move: true) or .yield(obj, move: true). These objects become inaccessible at the previous destination, raising a Ractor::MovedError when you try to call any other methods on the moved objects. r = Ractor.new do Ractor.receive end outer_object = "outer" r.send(outer_object, move: true) # => # outer_object + "moved" # `method_missing': can not send any methods to a moved object (Ractor::MovedError) * Threads cannot be sent as messages using .send and .yield. Doing this results in a TypeError. r = Ractor.new do Ractor.yield(Thread.new{}) end # :627:in `yield': allocator undefined for Thread (TypeError) Shareable and Unshareable Objects Shareable objects are objects that can be sent to and from a ractor without compromising thread safety. An immutable object is a good example because once created, it cannot be changed -- e.g., numbers and booleans. You can check the shareability of an object via Ractor.shareable? and make an object shareable via Ractor.make_shareable. Ractor.shareable?(5) # => true Ractor.shareable?(true) # => true Ractor.shareable?([4]) # => false Ractor.shareable?('string') # => false As seen above, immutable objects are shareable and mutable ones aren't. In Ruby, we usually call the .freeze method on a string to make it immutable. This is the same method ractors apply to make an object shareable. str = 'string' Ractor.shareable?(str) # => false Ractor.shareable?(str.freeze) # => true arr = [4] arr.frozen? # => false Ractor.make_shareable(arr) # => [4] arr.frozen? # => true Messages sent via ractors can either be shareable or unshareable. When shareable, the same object is passed around. However, when unshareable, ractors perform a full copy of the object by default and send the full copy instead. SHAREABLE = 'share'.freeze # => "share" SHAREABLE.object_id # => 350840 r = Ractor.new do loop do msg = Ractor.receive puts msg.object_id end end r.send(SHAREABLE) # 350840 NON_SHAREABLE = 'can not share me' NON_SHAREABLE.object_id # => 572460 r.send(NON_SHAREABLE) # 610420 As seen above, the shareable object is the same within and outside the ractor. However, the unshareable one isn't because the ractor has a different object, just identical to it. Another method to send an exact object when it is unshareable is the previously discussed move: true. This moves an object to a destination without needing to perform a copy. A few things to note about sharing objects in ractors: * Ractor objects are also shareable objects. * Constants that are shareable, but defined outside the scope of a ractor, can be accessed by a ractor. Recall our outer_scope_object example? Give it another try, defined as OUTER_SCOPE_OBJECT = "I am an outer scope object".freeze. * Class and module objects are shareable, but instance variables or constants defined within them are not if assigned to unshareable values. class C CONST = 5 @share_me = 'share me'.freeze @keep_me = 'unaccessible' def bark 'barked' end end Ractor.new C do |c| puts c::CONST puts c.new.bark puts c.instance_variable_get(:@share_me) puts c.instance_variable_get(:@keep_me) end # 5 # barked # share me # (irb):161:in `instance_variable_get': can not get unshareable values from instance variables of classes/modules from non-main Ractors (Ractor::IsolationError) * An incoming port or outgoing port can be closed using Ractor# close_incoming and Ractor#close_outgoing, respectively. Wrap Up and Further Reading on Ractors In this article, we introduced the concept of ractors, including when and why to use them and how to get started. We also looked at how they communicate with one another, what objects are shareable and unshareable, and how to make objects shareable. Ractors go deeper than this. Many other public methods can be called on ractors, like select to wait for the success of take, yield and receive, count, current, etc. To expand your knowledge about ractors, check out the ractor documentation. This GitHub gist might also interest you if you'd like to experimentally compare ractors with threads. Ractors are indeed experimental, but they certainly look like they have a bright future in Ruby's evolution. Happy coding! P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post! Write for our blog Would you like to contribute to the AppSignal blog? We're looking for skilled mid/senior-level Ruby, Elixir, and Node.js writers. Find out more and apply Share this article Copy link Twitter RSS Abiodun OlowodeAbiodun Olowode Abiodun Olowode Our guest author Abiodun is a software engineer who works with Ruby/ Rails and React. She is passionate about sharing knowledge via writing/speaking and spends her free time singing, binge-watching movies, and watching football games. All articles by Abiodun Olowode Newsletter LogoNewsletter Logo Magicians never share their secrets. But we do. Subscribe to our Ruby Magic email series and get in-depth Ruby articles. [ ] [ ] Subscribe [ ] Our favorite Ruby articles * Troubleshooting ActiveRecord PerformanceTroubleshooting ActiveRecord Performance Troubleshooting ActiveRecord Performance By Tomas Fernandez on Feb 24 * Building a Multi-tenant Ruby on Rails App With SubdomainsBuilding a Multi-tenant Ruby on Rails App With Subdomains Building a Multi-tenant Ruby on Rails App With Subdomains By Pawel Dabrowski on Dec 2 * Building a Rails App With Multiple SubdomainsBuilding a Rails App With Multiple Subdomains Building a Rails App With Multiple Subdomains By Prathamesh Sonpatki on Mar 4 * Rails is Fast: Optimize Your View PerformanceRails is Fast: Optimize Your View Performance Rails is Fast: Optimize Your View Performance By Swaathi Kakarla on Jan 22 * Sidekiq Optimization and MonitoringSidekiq Optimization and Monitoring Sidekiq Optimization and Monitoring By Stefan Verkerk on Oct 29 More articles * Aug 10, 2022A Deep Dive into Memory Leaks in Ruby * Aug 03, 2022Connect a Ruby on Rails App with React in a Monolith * Aug 02, 2022Monitor Alpine Linux ARM Hosts with AppSignal * Jul 28, 2022AppSignal for Ruby Gem 3.1: MRI VM Magic Dashboard * Jul 27, 2022How to Track Down Memory Leaks in Ruby * Jul 13, 2022Deploy Your Ruby on Rails App Using Capistrano * Jul 06, 2022Get Started with Hotwire in Your Ruby on Rails App * Jun 22, 2022State Machines in Ruby: An Introduction * Jun 08, 2022Add Feature Flags in Ruby on Rails with Flipper * May 25, 2022An Introduction to Polymorphism in Ruby on Rails AppSignal monitors your apps AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some! Discover AppSignal []AppSignal monitors your appsAppSignal monitors your apps []AppSignal monitors your appsAppSignal monitors your apps