await_suspend'den std::coroutine_handle döndürmek, garantili kuyruk çağrı optimizasyonu (TCO) biçimi olan simetrik transfer'i sağlar. await_suspend void döndürdüğünde, korutin çalışma zamanı bir sonraki korutine devam etmeden önce çağıranına dönmelidir, bu da zincir uzunluğu ile lineer olarak büyüyen iç içe bir çağrı yığını yaratır. Bir handle döndürerek, derleyici hedef korutinin yeniden başlatma noktasına doğrudan bir atlama (jmp komutu) gönderir, mevcut etkin kayıt kaydını yeniden kullanır ve zincir uzunluğuna bakılmaksızın sabit O(1) yığın derinliğini korur.
struct SimetrikTransfer { std::coroutine_handle<> next; // Kuyruk çağrı optimize edilmiş: yığın büyümesi yok std::coroutine_handle<> await_suspend(std::coroutine_handle<>) { return next; } void await_resume() {} bool await_ready() { return false; } };
Profesyonel müzik prodüksiyon yazılımı için gerçek zamanlı bir ses işleme motoru geliştirdik. Sistem, 500'den fazla dijital sinyal işleme (DSP) efekti (filtreler, kompresörler, yankı) iç içe geçmiş bir boru hattını temsil etmek için C++20 korutinlerini kullandı. Stres testleri sırasında, uygulama karmaşık efekt raflarını yüklerken yığın taşması ile çökmüş, her bir bireysel korutinin minimal yerel durumu olmasına rağmen.
Çözüm 1: Doğrudan yeniden başlatma ile void döndüren await_suspend Başlangıçta yapılan uygulama, void await_suspend(std::coroutine_handle<>) kullandı ve dahili olarak next.resume() çağrısı yaptı. Bu yaklaşım sezgisel, sıralı bir kod akışı ve standart yığın izleri ile kolay hata ayıklama sundu. Ancak, her resume() çağrısı önceki korutinin askıya alma mantığı içinde iç içe geçmiş, her aşama için yaklaşık 16KB tüketerek 500 aşamadan sonra 8MB'lık iş parçacığı yığınını tüketmiştir.
Çözüm 2: Asenkron zamanlamalı iş kuyruğu Doğrudan zinciri merkezi bir görev kuyruğuyla değiştirmeyi düşündük; burada her korutin bir sonraki aşamayı bir iş öğesi olarak gönderiyor ve hemen askıya alıyordu. Bu, yığın kullanımını sabit hale getirerek rekursiyonu iterasyona dönüştürdü. Dezavantajı, önemli bir performans düşüşüydü: kuyruk düğümleri için dinamik tahsisler, iş parçacığı çatışmasından dolayı önbellek karmaşası ve boru hattı aşamaları arasında önbellek yerelliğinin kaybı, alt milisaniye gecikme gereksinimlerimizi ihlal etti.
Çözüm 3: coroutine_handle aracılığıyla simetrik transfer await_suspend'i bir sonraki aşamanın std::coroutine_handle'ini doğrudan döndürecek şekilde yeniden yapılandırdık. Bu, derleyiciye TCO'yu gerçekleştirmesi için sinyal gönderdi ve yığın çerçevelerini birleştirdi. Çözüm, korutinlerin sıfır maliyetli soyutlamasını korurken, O(1) bellek kullanımını sağladı. Ana risk yaşam döngüsü yönetimi ile ilgiliydi: handle döndürüldükten sonra, mevcut korutin askıya alındı ve geri dönüş noktasından sonra this veya yerel değişkenlere erişim tanımsız davranışa neden oldu.
Seçilen Çözüm ve Sonuç Çözüm 3'ü benimsedik. Yeniden yapılandırmadan sonra, boru hattı yalnızca 4KB yığın alanı kullanarak, ardışık 512 efekti başarıyla işledi, çökmeleri ortadan kaldırdı ve belirleyici gerçek zamanlı performansı sürdürdü. Bu değişiklik, await_suspend'te geri dönüş sonrası mantık olmadığından emin olmak için dikkatli kod incelemeleri gerektirdi, ancak sağlam, ölçeklenebilir bir mimari sağladı.
Neden simetrik transferin, await_suspend içinde bir sonraki korutine co_await kullanmak yerine std::coroutine_handle döndürmesini gerektirir? await_suspend içinde co_await kullanmak, beklemekte olan korutinin öncelikle tamamen askıya alınmasını, ardından daha sonra yeniden başlatılmasını gerektirir; bu da doğal olarak çalışma zamanına dönüş yapmayı ve yığının büyümesini içerir. Handle'ı doğrudan döndürmek, derleyicinin yeniden başlatmayı bir kuyruk çağrısı olarak ele almasını sağlar, oysa co_await asimetrik bir askıya alma noktası oluşturur; bu da çağıranın çerçevesinin korunması gerektiğini gerektirir.
Eğer yeniden başlatılan korutin, nihai askıya alma noktasına ulaşmadan önce atma yaparsa simetrik transfer, istisna güvenliğini nasıl etkiler?
Eğer simetrik olarak transfer edilen korutin atma yaparsa, istisna kavramsal olarak await_suspend çerçevesinde çözülür; ancak orijinal korutin zaten askıya alınmış olarak işaretlendiği için çerçevesinin yığından çözümlenmesi sırasında yok edilmesi gerekir. Bu, derleyicinin askıya alınmış korutinin promise ve yakalanmış parametrelerini yok eden karmaşık istisna işleme tabloları oluşturmasını gerektirir. Adaylar genellikle özel promise_type tahsis edicilerinin kısmi yapıyı düzgün bir şekilde ele alması gerektiğini gözden kaçırır, aksi takdirde istisna çözülmesi sırasında çift yok etme hataları ile karşılaşabilirler.
Bir üretici uygularken simetrik transferin kullanılmasını ne engeller?
Üreticiler, durumlarını koruyarak çağırana kontrol iade etmek için co_yield kullanır. Simetrik transfer, koşulsuz olarak kontrolü bir başka korotine geçirir ve tüm zincir tamamlanana kadar orijinal çağırana dönmez. Bu nedenle, üreticilerin, tüketicinin döndürülen değeri almasına ve üreticiyi daha sonra yeniden başlatma olasılığına izin vermek için asimetrik askıya alma kullanmaları gerekir; aksi takdirde, farklı bir korutine geri dönülmez bir transfer zorlanır.