Go'da, bir kanalda yapılan bir gönderim işlemi, o kanaldan karşılık gelen alım tamamlanmadan önce gerçekleşir. Bu garanti, zamanlama tarafından hafif senkronizasyon öncülleri kullanılarak uygulanır; genellikle kanalda içsel hchan yapısında atomik işlemler veya mutex'ler kullanılır. Bir goroutine bir gönderim yaptığında, zamanlama tüm bellek yazımlarının gönderim talimatından önce tamamlandığının ve bu değer başarılı bir şekilde alındığında tüm goroutine'lere görünür olduğunun garanti edilir.
Tersine, alım bir edinim işlemi işlevi görür, bu da alıcı goroutine'in tüm yan etkilerini, gönderimden önce gözlemlemesini sağlar. Bu senkronizasyon, derleyici ve CPU'nun bu sınırın ötesinde yükleme ve depolama işlemlerini yeniden sıralamasını engelleyen katı bir gerçekleşen-önce kenarını oluşturur. Mekanizma, Go'nun eşzamanlılık güvenliği için temeldir ve goroutine'lerin veri aktarımı için açık kilitler olmadan iletişim kurmasına olanak tanırken, aktarılan verilerin ardışık tutarlılığını korur.
Yüksek verimli bir günlüğü toplayıcı uygulamak zorundaydık; burada birden fazla üretici goroutine, günlük girişlerini biçimlendirir ve bunları diske yazmak için birikim yapan tek bir tüketiciye gönderir. Günlük giriş yapıları büyük bayt dilimlerine işaretçi alanları içeriyordu ve tüketicinin işaretçiyi görebildiği ancak dilim başlığından eski veriyi okuduğu, uygun bellek görünürlüğünün eksikliğini gösteren seyrek bozulmalar gözlemledik.
Çözüm 1: Manuel Mutex Senkronizasyonu
Her günlük girişinin değişim ve erişimini sync.Mutex ile sarmalamayı düşündük. Bu, girişi değiştirmeden önce açıkça kilitleyerek ve gönderimden sonra bu kilidi açarak görünürlük garanti edecekti, ardından alıcıda yeniden kilitlenerek. Ancak bu yaklaşım önemli bir içe dönmeye neden oldu; çünkü mutex hem kanal işlemini hem de veri hazırlığını seri hale getiriyordu, bu da goroutine eşzamanlılığının yararlarını ortadan kaldırıp kodu kilit yönetimi ile karmaşık hale getiriyordu.
Çözüm 2: Atomik İşaretçi Değişimi
Başka bir yaklaşım ise günlük girişlerini atomik işaretçiler içinde saklamak ve devretme sırasında değiştirmekti. Bu, kilit olmadan ilerleme sağlasa da, ABA problemlerinden kaçınmak için dikkatli bellek yönetimi gerektiriyordu ve tüketicinin tüm alan erişimlerinin atomik işlemleri kullanmasını zorunlu kılıyordu. Bu, karmaşık yapıların pratik olmasını zorlaştırır ve Go'nun bileşik veri türleri için alışılmış uygulamalarını ihlal ederek kodu hataya açık ve bakımını zor hale getiriyordu.
Seçilen Çözüm: Kanalın Gerçekleşen-Önce Garantisi
Sonuç olarak, Go'nun önbelleksiz kanallarının içsel gerçekleşen-önce garantisine güvendik. Üreticinin tüm alan değişimlerini gönderim ifadesinden önce tamamladığından ve tüketicinin yalnızca alım ifadesi geri döndükten sonra girişi eriştiğinden emin olarak, Go zamanlaması gerekli bellek bariyerini otomatik olarak oluşturdu. Bu, ek senkronizasyon öncüllerine olan ihtiyacı ortadan kaldırdı, kod karmaşıklığını azalttı ve tam olarak başlatılmış veri yapılarının her zaman tüketici tarafından gözlemlendiğini garanti ederken sıfır atama devretmelerini sağladı.
Sonuç:
Sistem, veri yarışları veya bozulmalar olmadan, yarış algılayıcı ile geniş kapsamlı testlerle doğrulandığı üzere, saniyede 100.000'den fazla günlük girişini başarıyla işledi. Kod, manuel senkronizasyonu tanıtmadan, Go'nun yerleşik eşzamanlılık öncüllerinden yararlanarak temiz ve alışılmış hale geldi. Bu yaklaşım, günlüğü yönetme alt sistemini sürdüren geliştiricilerin bilişsel yükünü önemli ölçüde azalttı.
Gerçekleşen-önce garantisi, birden fazla eleman içeren tamponlu kanallar için geçerli midir?
Evet, ancak önemli bir ayrım ile. Garanti, tampon kapasitesinden bağımsız olarak, belirli bir gönderim ile karşılık gelen alım arasında geçerlidir. Ancak, tamponlu kanallar kullanıldığında, bir gönderim, alım gerçekleşmeden önce tamamlanabilir (çünkü değer tamponda bekler). Gerçekleşen-önce kenarı hâlâ o belirli değeri alan gönderim işlemi ile sonraki alım arasında oluşturulur; herhangi bir rastgele alım işlemi arasında değildir. Adaylar sık sık tamponlu kanalların bellek modelini zayıflattığını düşünerek, ancak senkronizasyonun her bir eleman için sürdüğünü belirtiyorlar; gönderici, verisini tüketen belirli alıcı ile senkronizedir, diğer goroutine'ler araya girse bile.
Bir kanalı kapatmanın gerçekleşen-önce ilişkisini gönderimle karşılaştırırken etkisi nedir?
Bir kanalı kapatmak, kapanışın sonucu olarak sıfır değeri başarıyla alan tüm alıcılarla bir gerçekleşen-önce ilişkisi oluşturur, sadece biriyle değil. Bir kanal kapatıldığında, ondan (sıfır değeri ve ok == false gösterimi alarak) alan herhangi bir goroutine, kapanış işlemi öncesinde gerçekleşen tüm bellek yazılarını görme garantisine sahiptir. Bu, kapanışı sona erdirmeye işaret eden etkili bir yayın mekanizması haline getirir. Adaylar genellikle bunun, kanalın bir şekilde "sıfırlığı" düşüncesi ile veya kapalı bir kanaldan okumanın senkronize edilmediği fikri ile karıştırıyor; gerçekte, kapatma işlemi tüm gözlemcilerin tespit edebileceği senkronize bir yazma işlemi olarak işlev görür.
Derleyici optimizasyonları, gönderilen değer doğrudan etkilenmiyorsa kanal işlemleri arasında talimatları yeniden sıralayabilir mi?
Hayır, bu tehlikeli bir yanılgıdır. Go'nun bellek modeli, kanal işlemlerini, böyle yeniden sıralamaları yasaklayan senkronizasyon işlemleri olarak ele alır. Derleyici, bellek yazımlarını bir gönderimden sonra alım işleminden önce taşıyamaz, ayrıca alım işleminden önceki okuma işlemlerini alım işleminden sonra hareket ettiremez; bu, dahil olan değişkenler gönderilen değerin parçası olmasa bile. Bunun nedeni, kanal işleminin kendisinin, programdaki tüm bellek işlemlerinin yeniden sıralamasını kısıtlayan bir gerçekleşen-önce kenarını kurmasıdır, sadece kanalın yükünü etkileyen işlemler değildir. Bunu anlamamak, geliştiricilerin paylaşılan durumu algılanan kritik bölümün dışından erişmeye çalıştıkları gizli hatalara yol açar, görünürlük garantilerini kırar.