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?

Ractor nedir, Thread'den farkı ne? Ruby kodunu paralel çalıştırma, Nesne paylaşımı, İletişim, Sonucu alma

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.

ÖzellikThreadRactor
Ruby kodunu paralel çalıştırmaAynı Ractor içinde hayırEvet, farklı Ractor'lar arasında
Nesne paylaşımıTüm nesneler ortakYalnızca paylaşılabilir nesneler
İletişimOrtak değişken, Queue, MutexRactor::Port üzerinden mesaj
Sonucu almaThread#valueRactor#value
DurumKararlıDeneysel

İlk Ractor ve deneysel uyarısı

ruby
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:

ruby
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:

ruby
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)  # => true

make_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

Ractor::Port ile mesajlaşma: Bir porta herhangi bir Ractor mesaj gönderebilir, ama yalnızca portu oluşturan..., Portun arkasında sınırsız boyutlu bir kuyruk vardır; send hiçbir...

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 receive ile okuyabilir.
  • Portun arkasında sınırsız boyutlu bir kuyruk vardır; send hiç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::ClosedError verir.
  • Bir Ractor bittiğinde portları kendiliğinden kapanır.
ruby
port = Ractor::Port.new
Ractor.new(port) do |port|
  port << 1
  port.send(2)
end
port.receive # => 1
port.receive # => 2

Varsayı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:

ruby
r = Ractor.new do
  sayi = Ractor.receive
  sayi * 2
end
r.send(21)
r.value # => 42

Kapalı port

ruby
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 closed

Kopyalama 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:

ruby
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 object

Birden ç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:

ruby
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:

ruby
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 hata

Yalı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:

ruby
$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 Ractor

Ractor 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:

ruby
pr = Ractor.shareable_proc { |x| x * 3 }
Ractor.shareable?(pr)                  # => true
Ractor.new(pr) { |f| f.call(5) }.value # => 15

Proc ve lambda farkı için Ruby Proc ve lambda farkı yazısına bakabilirsiniz.

Ruby 3.x Ractor kodunu 4.0'a taşımak

Ruby 3.x Ractor kodunu 4.0'a taşımak: Ractor içinde Ractor.yield(x), r.take ile son değeri almak, Bitmesini beklemek, close_incoming, close_outgoing

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ımRuby 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 almakr.value
Bitmesini beklemekr.join
close_incoming, close_outgoingPort#close ile portu kapatmak
Bitişi izlemekr.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 value değ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: true ya da make_shareable kullanı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