Sorunun tarihi
C++20'den önce, C++ standardı, paylaşılan akışlara biçimlendirilmiş çıktı için el ile senkronizasyon olmadan thread-güvenli bir olanak sunmuyordu. std::cout, sync_with_stdio aracılığıyla C akışlarıyla senkronize olacağına dair bir garanti sağlarken, bu, tamponlamayı engelledi ve ciddi performans kaybına yol açtı. Geliştiriciler genellikle her ekleme etrafında bir std::mutex sarmaladı fakat bu, thread'leri tamamen sıraladı ve eğer kilit bir kod yolunda unutulursa iç içe geçmeye karşı koruma sağlamadı. Yüksek verimlilikte çok iş parçacıklı günlükleme için atomik olarak çıktıyı toplu olarak toplayan daha yüksek düzeyde bir soyutlamaya ihtiyaç duymak kritik hale geldi.
Sorun
Standart akış ekleme işlemleri atomik değildir. Karmaşık bir tür için bir operator<< çağrısı, temel std::streambuf'a birden fazla sputc veya sputn çağrısı tetikleyebilir ve eş zamanlı thread'lerin karakterleri bu ince düzeyde iç içe geçebilir. std::stringstream gibi mevcut çözümler, kilitlenmiş çıktıyı gerektiriyor ve tüm tamponun ekstra bir kopyasını almak gerekiyordu. Manuel kilitleme, gereksiz kod ekleyip, olası kilitlenmelere neden olabiliyordu, eğer bir istisna, mutex açılmadan önce gerçekleşirse.
Çözüm
C++20, tüm biçimlendirilmiş çıktıyı yerel olarak biriken bir std::basic_syncbuf kapsayan std::basic_osyncstream (karakter için std::osyncstream takma adıyla) tanıttı. emit() çağrıldığında - ya açıkça ya da yok edici tarafından - sarıldığı std::streambuf ile ilişkili bir mutex edinir ve tam birikmiş içeriği tek bir sürekli yazma işlemi ile aktarır. Bu, ince düzeyde karakter kilitlemeyi kaba düzeyde mesaj düzeyinde kilitlemeye dönüştürerek, başka hiçbir thread'in karakterleri yayım sırasında iç içe geçmesini sağlamaz.
#include <syncstream> #include <iostream> #include <thread> #include <vector> void worker(int id) { std::osyncstream synced_out(std::cout); synced_out << "Thread " << id << " işlem verisi işliyor "; // emit() burada otomatik olarak çağrılıyor, tam satırı atomik olarak yazıyor } int main() { std::vector<std::jthread> threads; for (int i = 0; i < 10; ++i) { threads.emplace_back(worker, i); } }
Dağıtık bir veritabanı sistemi, çoklu işlem thread'lerinden merkezi bir denetim günlüğüne JSON taahhüt kayıtları yazması gerekiyordu. Her kayıt, işlem kimliklerini, zaman damgalarını ve durum bayraklarını içeriyordu. Atomik yayım olmadan, farklı thread'lerden gelen süslü parantezler ve tırnaklar karışıyordu; bu, aşağı akış analitik hatlarının ayrıştıramayacağı bozulmuş JSON üretiyordu ve gecelik toplu işler başarısız oluyordu.
Düşünülen çözümlerden biri, eş zamanlı okumalar ancak özel yazmalarına izin veren global std::shared_mutex kullanmaktı. Artıları: Aşina senkronizasyon düzeni. Eksileri: Yazarlar, JSON biçimlendirme süresince tamamen sıralanıyordu; taahhüt fırtınaları sırasında yüksek rekabet, gecikme piklerine neden oluyordu; bir thread'in kilidi açmadan önce bir istisna fırlatması durumunda kilitlenme riski vardı.
Diğer bir yaklaşım, arka plandaki bir thread tarafından birleştirilen per-thread günlük dosyalarıydı. Artıları: Yazma yolunda sıfır rekabet; işlem işleme sırasında kilitlenme gereği yoktu. Eksileri: Karmaşık günlük döngüsü ve dosya yönetimi; thread'ler arasında zamansal sıralamanın kaybı; çoklu dosya tutacaklarından kaynaklanan artan disk G/Ç; eğer birleştirme thread'i çökerse kayıtların kaybolma potansiyeli.
Ekip, std::osyncstream'i benimsedi. Her işlem kapsamı, paylaşılan denetim std::ofstream etrafında bir yerel std::osyncstream oluşturur. JSON, iç tamponda oluşturulur ve kapsam çıkışında atomik olarak yayılır. Bu, kilit tutma süresini milisaniyeden (JSON biçimlendirme süresi) mikro saniyeye (tampon kopyası) azalttı, tamamen bozulmayı ortadan kaldırdı ve emit() çıkışı sırasında alt akışa erişimi sıralayarak kronolojik sıralamanın korunmasını sağladı.
Sonuç: Sistem, 100,000'in üzerinde taahhütü bozulmadan sürdürdü ve hata ayıklama, kayıtların sağlam ve sıralı kalmasından dolayı mümkün hale geldi.
Neden std::osyncstream'in yok edicisi, emit()'i koşulsuz olarak çağırıyor ve altında yatan akış fırlatırsa istisna güvenliği sonuçları nelerdir?
Yok edici, tamponlanmış verinin kaybolmadığını garanti altına almak için emit()'i çağırır. emit() fırlatırsa (örneğin, disk dolu), ve yok ediciler kendiliğinden noexcept olduğundan, std::terminate hemen çağrılır. Adaylar genellikle istisnanın yayıldığını veya tamponun sessizce yok sayıldığını düşünür. Doğru detay, std::basic_syncbuf'un erişilebilir bir emit_on_flush bayrağı ve hata durumu sağladığıdır; ancak yok edici, sessiz veri kaybı veya yok edicilerden istisna sızmasına karşı programın sonlandırılmasını önceliklendirir.
Açık boşaltma işlemleri (std::flush veya std::endl) ve std::osyncstream'in iç tamponlaması nasıl etkileşir ve neden kötüye kullanımı performansı mutex düzeyine geri döndürür?
Birçok aday, std::flush'ın hemen altında yatan cihaza yazıldığını düşünür. std::osyncstream'de, std::flush, iç syncbuf'ın yayım için hazırlık yapmasını sağlar ama kendisi emit()'i çağırmaz; yalnızca emit_on_flush durumunu ayarlar. Eğer bir kullanıcı her eklemeden sonra manuel olarak emit() çağrarsa (birim tamponlama taklit ederek), her mesaj için kilitlemeyi zorlar, grup optimizasyonunu bozar. Verimlilik, birçok eklemeyi kapsam çıkışında tek bir emit() çağrısı haline getirme üzerine dayanır.
Sarılı std::streambuf'un yaşam döngüsü kısıtlaması nedir ve sarılan tampon emit()'ten önce yok olursa ne tür belirsiz davranışlar oluşur?
std::osyncstream, sarılan std::streambuf'a bir işaretçi tutar, mülkiyet değil. Geçici bir std::ostringstream oluşturursanız, onun rdbuf()'ını std::osyncstream'e geçirirseniz ve ostringstream kapsamdan çıktığında önce osyncstream, sonraki emit() çağrıları bir sallantı işaretçisini derefere eder. Adaylar genellikle referans sayma veya osyncstream'in tamponu kopyaladığını varsayar. Standart, programcının, sarılan streambuf'un syncstream'den daha uzun yaşamını sağlamalı olduğunu belirtir, tıpkı std::string_view'in temel dize deposundan bağımlı olduğu gibi.