2 puan yazan GN⁺ 2025-11-01 | 1 yorum | WhatsApp'ta paylaş
  • Futurelock, tek bir task birden fazla Future'ı aynı anda yönettiğinde, bunlardan birinin başka bir Future'ın kaynağına ihtiyaç duymasına rağmen artık poll edilmemesi sonucu ortaya çıkan bir deadlock olayıdır
  • tokio::select! yapısında referans verilen Future (&mut future) ile await içeren bir branch birlikte kullanıldığında kolayca ortaya çıkabilir
  • Bu sorun, task ile Future sorumluluklarının ayrıştırılamamasından kaynaklanır; aynı task iki Future'ı da beklerken yalnızca birini poll eden yapı nedeniyle sistem durma noktasına gelir
  • FuturesUnordered, bounded channel, Stream gibi yapılarda da benzer biçimde ortaya çıkabilir
  • Güvenli eşzamansız tasarım için temel yaklaşım, Future'ı tokio::spawn ile ayrı bir task'a ayırmak veya select içinde await kullanmaktan kaçınmaktır

Futurelock kavramı ve örnek

  • Futurelock, Future A'nın elindeki kaynağa Future B ihtiyaç duyarken, bu iki Future'dan sorumlu task'ın artık A'yı poll etmemesi durumunda ortaya çıkar
  • Örnek kodda tokio::select! içinde &mut future1 ve sleep aynı anda beklenir; sleep önce tamamlanırsa future1 hâlâ lock bekleyen durumda kalır
  • Sonrasında future3 aynı lock'ı ister, ancak lock future1 için tahsis edilmiştir ve future1 poll edilmediği için program kalıcı olarak durur

tokio::select! ve Mutex etkileşimi

  • tokio::sync::Mutex, adil (fair) bir lock'tır ve lock'ı bekleme sırasına göre verir
  • Lock future1'e devredilir, ancak task artık yalnızca future3'ü poll ettiği için future1 çalışmaz
  • Mutex yalnızca sıradaki bekleyen task'ı uyandırır; hangi Future'ın gerçekten poll edildiğini bilemez

Futurelock'ın genel nedenleri

  • Task T'nin Future F1'i beklediği, F1'in F2'ye bağımlı olduğu ve F2'nin de yeniden T tarafından poll edilmeyi gerektirdiği döngüsel bağımlılık yapısı
  • Özellikle şu durumlarda görülür
    • tokio::select! içinde &mut future kullanıp başka bir branch'te await yapmak
    • FuturesOrdered veya FuturesUnordered içinde bazı Future'lar tamamlandıktan sonra başka eşzamansız işler yapmak
    • Elle yazılmış Future implementasyonlarında benzer davranışlar

Stream'lerde ve diğer yapılarda görülen örnekler

  • FuturesOrdered ya da FuturesUnordered içinde bir Future çıkarıldıktan sonra, onunla ilişkili kaynağı kullanan başka bir Future beklenirse Futurelock oluşabilir
  • join_all, tüm Future'ları poll etmeyi sürdürdüğü için Futurelock oluşturmaz

Gerçek vaka ve debugging

  • Omicron#9259 vakasında tüm veritabanı erişim Future'ları Futurelock'a takıldı ve HTTP istekleri sonsuza kadar bekledi
  • mpsc channel gönderimi bloke olmuştu, ancak alıcı taraf boş görünüyordu; bu yüzden nedenin bulunması zordu
  • Debugging sırasında tokio-console gibi araçlar yardımcı olabilir, ancak çoğu durumda kök nedeni izlemek çok zordur

Futurelock'tan kaçınma yönergeleri

  • Bir task birden fazla Future'ı poll ederken, başlatılmış bir Future'ın poll edilmesini durdurmamaya dikkat etmek gerekir
  • Mümkünse Future'ları yeni bir task olarak spawn ederek bağımsız çalıştırın
    • JoinHandletokio::select! içine vermek Futurelock riskini ortadan kaldırır
  • tokio::select! kullanırken dikkat edilmesi gerekenler
    • &mut future ile await'i aynı anda kullanmayın
    • Her iki koşul da varsa Futurelock riski yüksektir
  • Stream kullanırken her Future'ı ayrı bir task olarak çalıştırmak için JoinSet kullanın
  • bounded channel kapasitesini artırmak temel bir çözüm değildir
    • Bunun yerine try_send() kullanarak blocking'den kaçınılabilir

Hatalı kaçınma kalıpları

  • Channel kapasitesini sonsuza kadar artırmak gerçekçi değildir ve gecikme ile bellek artışı gibi yan etkilere yol açar
  • Future'lar arası bağımlılıkları kaldırmaya çalışmak, bakım sırasında yeni bağımlılıklar eklenebileceği için kırılgandır
  • Güvenli olan tek yöntem, tokio::spawn ile task'ları ayırmaktır

Gelecekteki iyileştirmeler ve güvenlik değerlendirmeleri

  • Clippy lint'leriyle tokio::select! içinde &mut future kullanımı veya await bulunması durumunda uyarı verme olasılığı gündeme getiriliyor
  • Futurelock, bir hizmet engelleme (DoS) biçiminde kötüye kullanılabilir; ancak özünde anormal bir çalışma durumu olduğu için önlenmesi gerekir

1 yorum

 
GN⁺ 2025-11-01
Hacker News görüşleri
  • Belgeyi gözden geçirince oldukça şeffaf ve kapsamlı bir rapor gibi hissettirdi
    Özellikle dipnotlar bölümü ilginçti
    Rust'ın cancellation safety sorununu bilmeyen çok kişi vardı ve bu sorunun Omicron geneline yayılmış olma ihtimali etkileyiciydi
    Rust'ın seçilme nedeni C'nin bellek güvenliği sorunlarından kaçınmaktı, ama bu kez çalışma zamanında yakalanması zor cancellation hatalarının ortaya çıkması ironik geldi
    Derleyicinin yardımcı olamadığı dinamik özellikleri programcının bizzat garanti etmek zorunda olması özellikle can sıkıcıydı

    • Bu tür sorunlardan kaçınmak için daha üst bir soyutlama katmanı gerekmiyor mu diye düşündüm
      Rust'ın eşzamanlılık modelinde de hâlâ deadlock olasılığı var gibi görünüyor
      RAII tarzı kaynak yönetimi bunun gibi sorunları engeller sanılırdı ama pratikte öyle olmadığı kafa karıştırıcı
      Bunun yalnızca uygulamaya özgü tesadüfi bir durum mu, yoksa Rust/Tokio modelinin yapısal bir sınırı mı olduğunu merak ediyorum
  • Bu, withoutboats'un FuturesUnordered yazısında açıklanan deadlock'un ince bir varyasyonu gibi görünüyor
    “intra-task” eşzamanlılık kullanırken hiçbir future'ın açlık durumuna düşmemesine dikkat etmek gerekiyor
    Temelde task spawn etmek daha güvenli; timeout'u tokio::select! ile ele alırken de tüm pending future'ları onun içinde yönetmek gerekiyor
    FuturesUnordered gerçekten bütün uç durumları test etmediğiniz sürece pek önerilmez

  • Bu, öncelik terslenmesi (priority inversion) sorununa benziyor
    İşletim sistemlerinde düşük öncelikli bir thread lock'u tutarken yüksek öncelikli thread beklemek zorunda kalırsa, düşük öncelikli olan öncelik devralır ve çalıştırılır
    Tokio'da da benzer bir kavram uygulanabilir mi diye merak ediyorum — örneğin çalışamayan bir future bir Mutex tutuyorsa, o future'ı onun yerine poll etmek gibi
    Ama “çalışamayan” durumu algılamak epey ek yük doğuracak gibi duruyor

    • Bu tür bir yaklaşım Tokio'nun task düzeyinde belki mümkün olabilir
      Ama task içindeki future'lara uygulanamaz
      Çünkü async Rust'ın temel tasarımı “futures are inert” şeklinde — future yalnızca bir struct'tır ve runtime onun içini bilmez
      Runtime'ın bildiği şey yalnızca task düzeyidir; iç future'ların durumunu hiç izlemez

    • Rust'ın async modeli stackless coroutine modelidir; bu yüzden zaten çalışmakta olan bir async fonksiyonun yürütmesini keyfi biçimde sürdürmek güvenli değildir
      Stackless model, yerel durumu paylaşılan stack üzerinde tuttuğu için yalnızca LIFO sırasıyla güvenle çalıştırılabilir
      Bu yüzden coloring gerekir ve stackful coroutine'lardaki gibi serbestçe yield edemez

    • Kod bana fazla karmaşık görünüyor
      Erlang, Elixir, Go, hatta C ile yazılandan bile çok daha ayrıntılı ve uzun görünüyor

    • Bunun temel bir iki lock'lu deadlock durumuna benzediğini düşünüyorum
      Tokio'nun Mutex bekleme kuyruğu ile task zamanlaması birbirine dolanıp kilitlenme yaratıyor
      OS Mutex olsaydı başka bir bekleyen thread uyandırılarak çözülebilirdi ama async Rust'ta future'ın durum makinesi yapısı nedeniyle bunun zor olduğunu düşünüyorum
      Bekleme kuyruğundaki future'ları sırayla poll ederek çözmek mümkün olabilir ama bu da beklenmedik yan etkilere yol açabilir

  • async Rust ekosisteminde bu tür sorunları birlikte ele alma deneyimim oldu
    select! içinde referans kullanımını yasaklarsanız bu sorunlardan kaçınabilirsiniz, ama o zaman kuyruktaki yerini kaybetmeden tekrar tekrar select! çalıştırma deseni imkânsız hale gelir
    Cancellation sorunlarıyla birlikte bu tür şeyler Rust uzmanları için bile beklenmedik tuzaklar olabilir
    Yine de callback tabanlı koda kıyasla şaşırtıcı durumlar çok daha az

    • Evet, bizim ekip de bu deadlock'u analiz ettikten sonra “Bunu nasıl önleyebilirdik?” diye tartıştı ama sonunda kimsenin hatası olmadığı sonucuna vardık
      Tokio'nun tüm primitive'leri amaçlandığı gibi çalıştı, kod da doğru yazılmıştı, ama birbirleriyle etkileşimleri beklenmedik bir deadlock yarattı
      &mut future kullanımını select! içinde yasaklamak bunu engelleyebilir, ama bu da pek çok normal kodu engeller
      Sonunda acı bir şekilde bunun “sadece dikkat edilmesi gereken bir nokta” olduğu sonucuna vardık
      İlgili tartışma bu yorumda da sürüyor

    • select! seçilmeyen future'ları drop etmeden geri döndürse durum kaybı yaşanmazdı
      Ama bu kullanışsız olur ve kökten bir çözüm sayılmaz
      Asıl neden, bu başlıkta açıklandığı gibi cancellation işlemenin eksik olmasıdır

  • SSS'deki “future1 iptal edilmiyor mu?” sorusu ilginçti
    Cancellation iki aşamadan oluşur — poll'ün durması ve drop
    Bu örnekte drop geciktiği için guard elde tutulmaya devam ediyor ve yan etkiler doğuyor
    Bu iki davranışın her zaman aynı anda gerçekleşmesinin garanti edilip edilemeyeceğini merak ediyorum

  • Rust tasarımcılarına sormak isterim — neden actor modeli yerine async deseni seçildi
    Erlang kullandığınızda actor modeli çok daha temiz ve güvenli hissettiriyor
    JS dil yapısı nedeniyle async kullanmak zorundaydı ama Rust yeni bir dildi; neden bu yolu seçtiğini merak ediyorum

    • Rust'ın async tasarımında gömülü ortam desteği büyük bir etkendi
      malloc ya da thread kullanmadan da çalışabilmesi gerektiğinden actor modeli mümkün değildi
      Tokio ile actor tarzı kod yazılabilir ama çok doğal hissettirmiyor

    • Bir başka neden de performans
      Actor modeli mesaj kopyalama maliyeti taşır; Rust ise performansın önemli olduğu bir sistem dili olduğu için async state machine ile zero-cost abstraction hedefledi
      Erlang ve Go farklı tavizler veren dillerdir

    • Rust, C FFI çağrılarında ek yük kabul etmek istemediği için green thread tabanlı model dışarıda bırakıldı
      async/await durum makinesine derlendiğinden ek yük düşüktür
      Go'nun ilk dönemlerinde de preemption yoktu ve benzer açlık sorunları yaşanıyordu; sonradan scheduler bunu çözdü
      Sonuçta her dilin hedefleri ve kısıtları farklıydı

    • Oxide'ın async'i benimsemesine ben de şaşırdım
      Gömülü sistemler ya da HTTP sunucuları tarafında alışıldık ama Oxide gibi bir sistem şirketinin bunu bu kadar derin kullanacağını beklemiyordum

  • Belgeyi okurken anlamadığım nokta, neden lock'u tutan future yerine ana thread'in uyandığıydı
    Adil bir lock ise future1'in uyanması gerekirdi; runtime neden başka bir thread seçti, bunu merak ettim

  • Yazı gerçekten çok ilgi çekiciydi
    Örnek kod da açıktı ve böyle bir hatayı bulmak kâbus gibi olsa da, bulduğunuz anda yapboz parçalarının yerine oturması hissi var

    • Bizim şirkette tüm toplantılar ve debug oturumları kaydediliyor; tam o “yapbozun oturduğu an” da videoda var
      Eliza, Sean, John ve Dave'in birlikte beyin fırtınası yapıp kök nedeni bulduğu anı görmek etkileyiciydi
      Pazartesi günü bununla ilgili bir podcast bölümü yayımlayacağız
      İlgili videolar RFD 537 ve bu etkinlik bağlantısında görülebilir
  • Rust'ın tüm etkin task'lerin aynı anda ilerlemesini sağlamaması anlaması zor ve hata üretmeye yatkın bir tasarım gibi görünüyor
    Python'daki Trio gibi structured concurrency getirilse daha sezgisel olabilir
    Rust da böyle bir modeli benimseyebilir mi diye merak ediyorum

    • Rust'ta da structured concurrency mümkün ama yalnızca task düzeyinde
      Future'lar ancak poll edildiklerinde ilerleyen basit struct'lar olduğu için “etkin future” diye bir kavram yok
      Her şeyi task olarak spawn etmek çözüm gibi görünse de, bu da bazı faydalı desenleri engeller

    • task ile future arasındaki ayrım önemli
      Future poll edilmezse hiçbir şey yapmaz
      Cancellation'ı “drop edilene kadar poll edilmeyen durum” diye tanımlarsanız, bu vakadaki gibi lock tutarken duran future'lar ortaya çıkar
      Rust'ın RAII felsefesinde temizlik işleminin drop sırasında olması beklenir, ama poll durmuşsa o bile gerçekleşmez

  • Son zamanlarda Rust'ın async'inin fazla aceleyle yayımlanmış olabileceğini düşünmeye başladım

    • Geliştirilecek çok şey olduğunu ben de düşünüyorum ama temel tasarımın kendisi mükemmel bir zemin bence
      Pin ya da sözdiziminin bazı kısımları iyileştirilebilir ama alttaki yapının kökten değişmesine gerek yok
      Hâlâ “ev tamamlanmadan önce atılmış temel” aşamasındayız; aceleye getirilmiş bir sonuç değil
      Yine de genelleştirilmiş coroutine gibi daha alt seviye yapı taşlarına ihtiyaç olduğunu düşünüyorum