Sorunun Tarihi
Erken korutin uygulamaları yığınlıydı ve her bağlam değiştirme için megabaytlarca sabit yığın alanı tahsis ediyordu, bu da eşzamanlılığı binlerce görevle sınırlıyordu. C++20, yığınsız korutinler tanıttı ve çerçeveleri yığında tahsis etti, ancak naif özyinelemeli bileşim hâlâ yığın taşması riski taşıyordu çünkü asimetrik transfer – await_suspend'den void veya bool döndürmek – yenileyicinin resume() çağrısında bulunmasını zorunlu kılıyor, bu da O(N) yerel çağrı yığın çerçevelerini inşa ediyordu. Simetrik transfer, korutin A'nın doğrudan korutin B'yi yenileyebilmesi için standartlaştırıldı ve A'nın yığın çerçevesini zorunlu kuyruk çağrısı optimizasyonu aracılığıyla devretmesini sağladı.
Problem
Korutin A, korutin B üzerinde co_await gerçekleştirdiğinde ve B, C'yi beklediğinde, asimetrik transfer her resume() çağrısının daha derine inmeden önce çağıranına geri dönmesini gerektiriyor. N derinlikteki özyineleme (örneğin, 50.000+ ağaç düğümüne geçiş) yerel yığını tüketiyor, her korutin çerçevesi yığındayken, bu SIGSEGV veya STATUS_STACK_OVERFLOW hatasına neden oluyor.
Çözüm
await_suspend'in std::coroutine_handle<Promise> (veya std::coroutine_handle<>) döndürmesi gerekir. Derleyici bunu bir kuyruk çağrısı olarak ele alır: mevcut aktivasyon kaydını yok eder ve doğrudan hedef handle'ın yenileme noktasına zıplar, çağrı yığınını büyütmeden. Bu mekanizma, mantıksal korutin iç içe geçiş derinliğinden bağımsız olarak sabit yığın derinliği ile çalışmayı garanti eder.
struct Task { struct promise_type { Task get_return_object() { return Task{std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; std::coroutine_handle<> h; }; struct SymmetricAwaiter { std::coroutine_handle<> target; bool await_ready() const noexcept { return false; } // Asimetrik (kötü): void await_suspend(std::coroutine_handle<>) { target.resume(); } // Simetrik (iyi): Kuyruk çağrısı optimizasyonu std::coroutine_handle<> await_suspend(std::coroutine_handle<>) noexcept { return target; } void await_resume() noexcept {} };
Problem Tanımı
Yüksek frekanslı bir ticaret motoru geliştirirken, karmaşık türev fiyatlama ağaçlarını modellemek için geri çağırma tabanlı asenkron G/Ç'den C++20 korutinlerine geçiş yaptık. Derinlemesine iç içe geçmiş sentetik opsiyonlar (50.000+ seviyeler) içeren portföylerle stres testleri sırasında, yığın tahsis edilen korutin çerçevelerine rağmen sistem yığın taşmalarında çöktü. Suçlu, await_suspend'in void döndürdüğü ilk uygulamaydı, bu da yerel yığın boyutunun fiyatlama modelinin derinliğiyle orantılı olarak büyümesine neden oldu.
Değerlendirilen Farklı Çözümler
Çözüm 1: Yerel yığın boyutunu ulimit -s veya bağlayıcı bayraklar aracılığıyla artırmak.
Artıları, kod değişikliği gerektirmemesi ve testler sırasında anlık rahatlama sağlamasıdır. Eksileri, her iş parçacığı başına gigabaytlarca sanal bellek israfı yapmak, sınırsız özyineleme senaryolarını ele almada başarısız olmak ve yığın tahsis mekanizmalarının önemli ölçüde farklı olduğu Linux ve Windows arasında taşınabilirlik kabusları yaratmaktır.
Çözüm 2: Asla özyineleme yapmayan bir trampolin yürütücüsü döngüsü uygulamak.
Artıları, korutin sözdizimini bozmadan yığın yönetimini merkezi bir olay döngüsüne taşımasıdır. Eksileri, sanal dağıtım nedeniyle bağlam değişiminin her birinde (yüzlerce nanosecond) önemli gecikmeler, zamanlayıcıda artırılan kod karmaşıklığı ve askıya alma noktalarında kayıt tahsisi için derleyici optimizasyonlarının kaybıydı.
Çözüm 3: await_suspend'den std::coroutine_handle döndürerek simetrik transferi benimsemek.
Artıları, sıfır üst yük soyutlama sağlamak (el yazması durum makineleri ile aynı montaj) ve yığın büyümesi olmadan sınırsız özyinelemeyi doğal olarak yönetmek, ayrıca okunabilir korutin sözdizimini korumaktı. Eksileri, C++20 derleyici desteği gerektirmekteydi (başlangıçta bazı gömülü platformlarda sınırlı) ve kuyruk çağrısı optimizasyonu nedeniyle yığın izlerinin kesilmiş görünmesiyle karmaşık hata ayıklamaya yol açıyordu.
Hangi çözüm seçildi ve neden?
Mali modellerin, teorik fiyatlama hesaplamaları için doğasında sınırsız özyineleme derinliği gerektirdiğinden, Çözüm 3'ü seçtik. Mikro saniye gecikme bütçesi trampolinin yükünü kaldıramazdı ve bellek kısıtlamaları büyük yığın tahsisini engelliyordu. Simetrik transfer, hem doğru hem de verimli olan tek sıfır maliyetli çözümdü.
Sonuç
Motor, 100.000+ iç içe geçmiş seviyeleri olan portföyleri sorunsuz bir şekilde işledi. Gecikme benchmarkları, el ile optimize edilmiş C durum makineleri ile aynı performansı gösterdi ve bellek kullanımı özyineleme derinliğinden bağımsız olarak düz kaldı. Sistem, yığınla ilgili sıfır çökme ile 18 aydır üretimde çalışıyor.
await_suspend'in void döndürmesinin, korutin çerçevesinin askıya alma zamanlaması açısından true döndürmesinden neden farklı olduğunu ve bunun thread güvenliği açısından neden önemli olduğunu açıklayınız?
Birçok aday, void'ın kontrolün hemen askıya alındığı ve transfer edildiğini varsayar. Aslında, void döndürmek mevcut korutini askıya alır, ancak kontrol resume()'in çağıranına geri döner; bu da bir sonraki yürütme adımını kimin belirleyeceği anlamına gelir. true döndürmek de askıya alır, ancak kritik olarak, void korutininin askıya alındığını garantiler. await_suspend void döndürdüğünde (örneğin, başka bir iş parçacığından) yerel korutin değişkenlerine erişim, askıya alma noktasına ulaşıldıktan sonra güvenlidir. Simetrik transfer ile (handle döndürme) yığın çerçevesi hemen dönüşte yok edilir, bu da yerel değişkenlerin anında erişilemez hâle gelmesine yol açar – adaylar genellikle simetrik transfer başlatıldıktan sonra yakalanan değişkenlere erişerek veri yarışları tanıtırlar.
Simetrik transfer, hedef korutin bir istisna attığında istisna yönetimiyle nasıl etkileşir ve bu, vaadini türündeki unhandled_exception'ı neden karmaşıklaştırır?
Adaylar genellikle, simetrik transferin, bekleyen korutin üzerinden normal yığın çözümlemeyi atladığını gözden kaçırır. Korutin A, B'ye simetrik transfer gerçekleştirdiğinde ve B bir istisna attığında, istisna B'nin unhandled_exception'ına yayılır. Ancak A'nın yığın çerçevesi kuyruk çağrısı optimizasyonu yoluyla zaten değiştirilmiştir; bu, A'nın co_await ifadesinin etrafında try/catch kullanarak B'den istisnaları yakalayamayacağı anlamına gelir. İstisna, A'nın orijinal çağıranına (yenileyiciye) yayılır ve A'nın temizleme kodunu atlayabilir, eğer A'nın vaadindeki unhandled_exception durumu yalnızca yığında tahsis edilen çerçeve aracılığıyla yönetmiyorsa. Acemi adaylar, A'da RAII yığın koruyucularının tetikleneceğini varsayar ve simetrik zincirlerde istisnalar gerçekleştiğinde kaynak sızıntılarına yol açar.
Simetrik transfer zincirlerinde std::noop_coroutine()'nin önemi nedir ve tamamlanmayı belirtmek için neden varsayılan olarak oluşturulmuş bir handle yerine döndürülmelidir?
Varsayılan olarak oluşturulmuş bir std::coroutine_handle, yeniden başlatıldığında tanımsız davranış sergileyen bir null handle'dır. await_suspend'den döndürmek "şimdi hiç kimseyi yenile" anlamına gelir ve mevcut korutini bir ardılı olmadan askıya alır; zamanlayıcı geçerli bir devam noktası bekliyorsa sistemin asılı kalmasına neden olabilir. std::noop_coroutine(), yeniden başlatıldığında hemen çağırana geri dönen özel bir singleton handle döndürür. Bu, bir yaprak korutini bittiğinde ve manuel yenileme olmadan üstüne kontrol döndürmek istediğinde, std::noop_coroutine()'yi döndürmesi için kritik öneme sahiptir. Bu, ebeveynin await_suspend'inin (çocuğa simetrik aktarım gerçekleştirmiş olan) geçerli bir "devam" almasını sağlar ve bu da zincir güvenli bir şekilde sona erer. Adaylar null handle'ları noop handle'ları ile karıştırır ve korutin sisteminin null bir yenileyici hedefinde sonsuza dek beklemesine neden olan ince ölümlere yol açar.