Ractor, Ruby kodunu birden fazla CPU çekirdeğinde gerçekten paralel çalıştırmak için tasarlanmış, aktör modeline benzeyen bir eşzamanlılık yapısıdır. Her Ractor kendi nesneleriyle çalışır; Ractor'lar arasında yalnızca paylaşılabilir (shareable) nesneler ortak kullanılır, geri kalan her şey mesajla kopyalanır ya da taşınır. Ruby 4.0 ile mesajlaşmanın temeli Ractor::Port oldu; Ractor.yield ve Ractor#take kaldırıldı, sonucu almak için Ractor#value ve Ractor#join geldi.
Ractor API'si hâlâ deneysel: Ruby 4.0.9 ile ilk Ractor oluşturulduğunda "Ractor API is experimental and may change in future versions of Ruby." uyarısı basılıyor. Bu yazıdaki örneklerin tamamını 4.0.9 ile çalıştırdık ve yalnızca 4.0 belgelerinde yazan API'yi kullandık. İş parçacıkları ve fiber'lar için Ruby Thread ve Fiber yazısına, dilin geneli için Ruby öğrenme rehberi sayfasına bakabilirsiniz.
Ractor nedir, Thread'den farkı ne?

Bir Ruby süreci tek bir Ractor ile başlar; buna ana Ractor denir. Ractor.new { ... } yeni bir Ractor oluşturur ve blok çok çekirdekli bir makinede diğerleriyle paralel çalışabilir. Ana Ractor biterse diğerlerine de sonlanma isteği gönderilir. Her Ractor bir ya da daha fazla Thread içerir.
Asıl fark kilit (GVL) tarafındadır. Ractor belgesine göre CRuby'de GVL Ractor başına tutulur, bu yüzden farklı Ractor'lar paralel çalışabilir; aynı Ractor içindeki thread'ler ise ortak kilidi paylaştığı için birbirleriyle paralel çalışamaz. Ractor oluşturmanın maliyeti bir thread oluşturmanınkinden biraz fazladır.
| Özellik | Thread | Ractor |
|---|---|---|
| Ruby kodunu paralel çalıştırma | Aynı Ractor içinde hayır | Evet, farklı Ractor'lar arasında |
| Nesne paylaşımı | Tüm nesneler ortak | Yalnızca paylaşılabilir nesneler |
| İletişim | Ortak değişken, Queue, Mutex | Ractor::Port üzerinden mesaj |
| Sonucu alma | Thread#value | Ractor#value |
| Durum | Kararlı | Deneysel |
İlk Ractor ve deneysel uyarısı
r = Ractor.new { "Merhaba, ben bir Ractor'üm" }
r.value # => "Merhaba, ben bir Ractor'üm"
# stderr: dosya.rb:1: warning: Ractor API is experimental and may change in future versions of Ruby.Ractor#value, Ractor bitene kadar bekler ve bloğun son değerini döndürür. Belgeye göre bu değer çağıran Ractor'a taşınır; bu yüzden bir Ractor'ın sonucunu en fazla bir Ractor alabilir. Yalnızca bitmesini beklemek için Ractor#join kullanılır. Denememizde uyarı -W:no-experimental seçeneğiyle gizlendi; yine de uyarının amacı API'nin değişebileceğini hatırlatmaktır.
Blok yalıtımı: dış değişkenlere erişilemez
Ractor.new bloğu dış kapsamdan yalıtılır. Blok dışarıdaki bir yerel değişkene erişirse Ractor oluşturulurken ArgumentError fırlatılır. Bloğun içinde self, Ractor nesnesinin kendisidir:
a = 1
begin
Ractor.new { a + 1 }
rescue ArgumentError => e
puts "#{e.class}: #{e.message}"
end
# çıktı: ArgumentError: can not isolate a Proc because it accesses outer variables (a).Veriyi içeri aktarmanın doğru yolu argüman vermektir. Ractor.new metoduna verilen argümanlar blok parametresi olur, ama nesnenin kendisi değil, mesaj olarak gönderilmiş hali gelir. Blokların kapsam kuralları için Ruby blok ve yield yazısına bakın.
Paylaşılabilir ve paylaşılamaz nesneler
Nesnelerin çoğu paylaşılamazdır; yalnızca onu oluşturan Ractor kullanabilir. Paylaşılabilir nesneler arasında tüm sayılar, semboller, true, false, nil, paylaşılamaz bir şeye başvurmayan dondurulmuş metinler, sınıflar, modüller ve Ractor nesneleri bulunur. Dondurulmuş bir dizi bile, içinde dondurulmamış bir öğe varsa paylaşılamaz. Ractor.shareable? ile kontrol eder, Ractor.make_shareable ile nesneyi ve başvurduğu her şeyi dondurarak paylaşılabilir hale getirirsiniz:
Ractor.shareable?(42) # => true
Ractor.shareable?("metin") # => false
Ractor.shareable?("metin".freeze) # => true
Ractor.shareable?([1, [2], 3].freeze) # => false (içteki [2] donmamış)
Ractor.shareable?({c: Object}.freeze) # => true
liste = ["elma", "armut"]
Ractor.make_shareable(liste)
liste.frozen? # => true
liste[0].frozen? # => true
Ractor.shareable?(liste) # => truemake_shareable nesneyi yerinde dondurur. Orijinali değiştirmek istemiyorsanız copy: true vererek dondurulmuş bir kopya alabilirsiniz. Belge bu metodun tanımının henüz olgunlaşmadığını ve değişebileceğini not ediyor. Dondurma konusu için Ruby frozen string ve freeze yazısına bakın.
Ractor::Port ile mesajlaşma

Ruby 4.0'da Ractor'lar arası iletişimin temel aracı Ractor::Port sınıfıdır. Dört metodu vardır: send (takma adı <<), receive, close ve closed?. Kurallar şöyle:
- Bir porta herhangi bir Ractor mesaj gönderebilir, ama yalnızca portu oluşturan Ractor
receiveile okuyabilir. - Portun arkasında sınırsız boyutlu bir kuyruk vardır;
sendhiçbir zaman beklemez. receive, kuyruk boşsa mesaj gelene kadar geçerli thread'i bekletir.- Portu yalnızca onu oluşturan Ractor kapatabilir. Kapalı porta göndermek ve boş, kapalı porttan okumak
Ractor::ClosedErrorverir. - Bir Ractor bittiğinde portları kendiliğinden kapanır.
port = Ractor::Port.new
Ractor.new(port) do |port|
port << 1
port.send(2)
end
port.receive # => 1
port.receive # => 2Varsayılan port: Ractor#send ve Ractor.receive
Her Ractor'ın bir varsayılan portu vardır (Ractor#default_port). r.send(nesne) bu porta yazar, Ractor'ın içindeki Ractor.receive de buradan okur. Kısa iş akışları için ayrı port oluşturmanız gerekmez:
r = Ractor.new do
sayi = Ractor.receive
sayi * 2
end
r.send(21)
r.value # => 42Kapalı port
port = Ractor::Port.new
port.close
port.closed? # => true
begin
port.receive
rescue Ractor::ClosedError => e
puts "#{e.class}: #{e.message}"
end
# çıktı: Ractor::ClosedError: The port was already closedKopyalama ve taşıma
Paylaşılabilir bir nesne gönderildiğinde yalnızca başvurusu iletilir. Paylaşılamaz nesneler ise varsayılan olarak derin kopyalanır; bu yavaş olabilir ve bazı nesneler (örneğin Thread) kopyalanamaz. Alternatif move: true ile taşımadır: nesne alıcıya geçer ve gönderen artık ona erişemez. Taşınan nesneye metot çağırmak Ractor::MovedError verir:
str = "merhaba".dup
r = Ractor.new do
obj = Ractor.receive
obj << " dünya"
end
r.send(str, move: true)
r.value # => "merhaba dünya"
begin
str.size
rescue Ractor::MovedError => e
puts "#{e.class}: #{e.message}"
end
# çıktı: Ractor::MovedError: can not send any methods to a moved objectBirden çok Ractor beklemek: Ractor.select
Ractor.select 4.0'da yalnızca Ractor ve Port kabul eder. Portlar verildiğinde ilk mesaj gelen portu ve mesajı, Ractor'lar verildiğinde ilk biten Ractor'ı ve sonucunu iki öğeli bir dizi olarak döndürür. Aşağıdaki örnek üç Fibonacci hesabını paralel başlatıp sonuçları geldikçe topluyor:
def fib(n) = n < 2 ? 1 : fib(n - 2) + fib(n - 1)
rs = [20, 25, 30].map do |n|
Ractor.new(n) { |n| [n, fib(n)] }
end
sonuc = []
until rs.empty?
r, v = Ractor.select(*rs)
rs.delete(r)
sonuc << v
end
sonuc.sort # => [[20, 10946], [25, 121393], [30, 1346269]]Sonuçların geliş sırası çalıştırmaya göre değişebileceği için örnekte sort kullandık. Belge çok sayıda Ractor ile Ractor.select kullanımının select(2) ile aynı ölçeklenme sorununu taşıdığını belirtiyor.
Hata yayılımı ve yalıtım hataları
Ractor bloğunda yakalanmayan bir hata, value ya da join çağıran tarafa Ractor::RemoteError olarak ulaşır; asıl hata cause içindedir. Ractor kendi thread'inde çalıştığı için hata ayrıca standart hataya da raporlanır:
r = Ractor.new { raise "bir hata" }
begin
r.value
rescue Ractor::RemoteError => e
puts "#{e.class}: #{e.cause.class} #{e.cause.message}"
end
# çıktı: Ractor::RemoteError: RuntimeError bir hataYalıtım kuralları dilin kendisine de yansır. Yalnızca ana Ractor global değişkenlere erişebilir; paylaşılamaz bir değere başvuran sabitler, sınıf değişkenleri ve sınıfların paylaşılamaz örnek değişkenleri diğer Ractor'larda Ractor::IsolationError verir:
$sayac = 1
r = Ractor.new { $sayac }
begin
r.join
rescue Ractor::RemoteError => e
puts e.cause.message
end
# çıktı: can not access global variable $sayac from non-main RactorRactor ile kullanılacak sabitleri Ractor.make_shareable ile ya da dosya başına yazılan # shareable_constant_value: literal yönergesiyle paylaşılabilir yapmanız gerekir. Bir Proc nesnesini Ractor'lar arasında kullanmak için 4.0 ile gelen Ractor.shareable_proc var:
pr = Ractor.shareable_proc { |x| x * 3 }
Ractor.shareable?(pr) # => true
Ractor.new(pr) { |f| f.call(5) }.value # => 15Proc ve lambda farkı için Ruby Proc ve lambda farkı yazısına bakabilirsiniz.
Ruby 3.x Ractor kodunu 4.0'a taşımak

NEWS dosyasına göre Ractor.yield, Ractor#take, Ractor#close_incoming ve Ractor#close_outgoing kaldırıldı. Denememizde Ractor.respond_to?(:yield) ve r.respond_to?(:take) ikisi de false döndü. Taşırken şu eşleştirmeyi kullanabilirsiniz:
| Eski kullanım | Ruby 4.0 karşılığı |
|---|---|
| Ractor içinde Ractor.yield(x) | Dışarıdan verilen bir Port ile port.send(x) |
| r.take ile son değeri almak | r.value |
| Bitmesini beklemek | r.join |
| close_incoming, close_outgoing | Port#close ile portu kapatmak |
| Bitişi izlemek | r.monitor(port), port :exited ya da :aborted alır |
Sık yapılan hatalar
- Bloğun içinde dış yerel değişken kullanmak; argüman olarak verin.
- Bir Ractor'ın
valuedeğerini iki farklı Ractor'dan almaya çalışmak; belgeye göre değer yalnızca bir kez alınabilir. - Büyük paylaşılamaz yapıları sürekli göndermek; kopyalama yavaştır, uygunsa
move: trueya damake_shareablekullanın. - Global değişkenlere ya da dondurulmamış sabitlere Ractor içinden erişmek.
- Başka bir Ractor'ın oluşturduğu portu kapatmaya ya da okumaya çalışmak.
Sık Sorulan Sorular
Ractor üretimde kullanılabilir mi?
API deneysel olarak işaretli ve değişebilir. NEWS 4.0'da birçok kararlılık ve performans düzeltmesi yapıldığını, Ractor'ın deneysel statüden çıkmaya yaklaştığını söylüyor. Kullanacaksanız sürüm yükseltmelerinde API değişikliklerini izlemeniz gerekir.
Ractor içinde Thread oluşturabilir miyim?
Evet. Her Ractor'ın kendi ana thread'i vardır ve içinde yeni thread'ler açılabilir. Bu thread'ler aynı Ractor'ın kilidini paylaşır; Ractor içindeki thread güvenliği için yine Mutex gibi araçlara ihtiyacınız olur.
Ractor.yield neden çalışmıyor?
Ruby 4.0 ile kaldırıldı. Yerine bir Ractor::Port oluşturup Ractor'a argüman olarak verin ve Ractor içinden port.send ile yazın.
Hangi nesneler kopyalanmadan gönderilir?
Paylaşılabilir nesneler: sayılar, semboller, true, false, nil, paylaşılamaz bir şeye başvurmayan dondurulmuş metinler, sınıflar, modüller ve Ractor nesneleri gibi.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Ruby öğrenme rehberi · Ruby Thread ve Fiber · Ruby freeze ve frozen string · Proc ve lambda farkı · Ruby 4.0 yenilikleri · Ruby hata yönetimi