# 64 goroutine, dört çekirdekte Mutex ile kanal aynı hızda, ama Mutex'in p99'u beş kat yüksek

> sync.Mutex ve kanal 845 hücrede ölçüldü: 64 goroutine, 4 çekirdekte aynı hız ama Mutex'in p99'u 5 kat yüksek; 8 çekirdekte Mutex 2,4 kat hızlı.

- Tür: Ölçüm
- Soru: Aynı paylaşılan sayaca G goroutine ve P çekirdekle saldırıldığında sync.Mutex, sahip-goroutine kanalı ve tamponlu kanal saniyede kaç işlem yapıyor, beklemenin kuyruk gecikmesi ve adilliği nasıl değişiyor?
- Bulgu: 64 goroutine ve dört çekirdekte sync.Mutex ile tek yönlü kanal aynı hızda (7,69 ve 7,94 milyon işlem/sn) ama Mutex'in p99 beklemesi 5 kat yüksek. Sekiz çekirdekte Mutex 2,4 ve 4,0 kat hızlı (15,96 milyon işlem/sn; kanallar 6,65 ve 3,97 milyon); oranlar bayraksız hücrelerden.
- Yöntem: go-sync-bench ile üç desen ölçüldü: S1 tek sayaç (sync.Mutex, sahip-goroutine ile tek yönlü kanal, istek-cevaplı kanal, atomic), S2 okuma oranı (Mutex, RWMutex, sahip-goroutine kanalı), S3 üretici-tüketici kuyruğu (kanal tamponları 0, 1, 64, 1024 ve sync.Cond'lu kuyruk). Her hücre, G goroutine ve P çekirdek grubu (cpuset ile 1, 2, 4 ve 8) için 5 round koştu; tekrar başına 0,5 sn ısınma, 1,5 sn throughput ve 2,0 sn gecikme penceresi. 845 hücre, 4.225 geçerli tekrar. Her round × çekirdek grubu container'ından sonra 60 sn soğuma, ağsız ve read-only bir Docker konteyneri, 4 GiB bellek. Hücre medyanları 5 tekrarın hepsinden alındı, hiçbir tekrar atılmadı. Bayrak eşikleri tam aralık üzerinden throughput için %10, p99 için %25, saat kayması için %5. Yeniden koşu adayı seçilirken en uçtaki tekrar atılıp kalan aralığa bakıldı (throughput %10, p99 %50); ilk tasarımın tetikleyicisi 294 aday seçince 84 hücrelik bütçe aşıldığı için bu kural kullanıldı ve 88 hücre yeniden koşuldu (2026-09-30 01:38-02:31 UTC). 845 hücrenin 518'i bayraklı olduğu için confidence low. Gecikme yüzdelikleri log kovalarının üst sınırıdır ve 1/16 örneklenmiştir; taban, saat çözünürlüğünün 4 katı olan 164 ns.
- Metrikler: G=64 P=4 · p99 bekleme, Mutex → tek yönlü kanal: 491.519 → 98.303 ns · G=64 P=8 · Mutex: 15,96 milyon işlem/sn · G=64 P=8 · tek yönlü kanal: 6,65 milyon işlem/sn · Kalite bayrağı taşıyan hücre: 518 / 845
- Ölçüm tarihi: 2026-09-29
- Güven: Düşük güven
- Durum: Yürürlükte
- Program: Dil & çalışma zamanı
- Ortam: Go 1.27.1 · linux/arm64 · Sanallaştırma Docker Engine 29.8.0 · 12 vCPU · aarch64 · Donanım Apple M4 Pro · 12 CPU · Çekirdek grupları P1 = 4 · P2 = 4-5 · P4 = 4-7 · P8 = 4-11 (cpuset) · Konteyner ağsız · read-only · 4 GiB bellek · Image ilk geçiş sha256:640a046f219a… · yeniden koşu sha256:eabc1573265a… · Protokol 5 round · 845 hücre · 0,5 sn ısınma + 1,5 sn throughput + 2,0 sn gecikme · 60 sn soğuma (container başına) · Seed 932269622
- Teknolojiler: Go, Docker
- Tekrarlamak için: ./bench/verify.sh && STAMP=$(date -u +%F) ./bench/run.sh --all
- Kaynak kodu: https://github.com/muhammetsafak/go-sync-bench
- Ham veri: https://github.com/muhammetsafak/go-sync-bench/tree/main/results/2026-09-29
- Ham veri lisansı: https://github.com/muhammetsafak/go-sync-bench/blob/main/LICENSE
- Yayın: 2026-09-30
- Kaynak: https://muhammetsafak.com/tr/research/go-mutex-ve-channel-cekisme-altinda/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
Dört çekirdekte 64 goroutine'i aynı sayaca saldığımda `sync.Mutex` ile kanal
saniyede aynı sayıda işlem yaptı: 7,69 ve 7,94 milyon. Aynı hücrede p99
bekleme 491.519 ns'ye karşı 98.303 ns'ydi. "Hangisi daha hızlı" sorusunun
tek bir cevabı olmadığını bu ikilide gördüm; bu kayıt o cevabın neye bağlı
olduğunu ölçtüğüm kadarıyla yazıyor. Kodun ve ham koşuların hepsi
[go-sync-bench](https://github.com/muhammetsafak/go-sync-bench) deposunda.

Goroutine ve kanal tarafının pratik anlatımı için [Go'da eşzamanlılık:
goroutine ve channel
pratiği](/tr/blog/goda-eszamanlilik-goroutine-ve-channel-pratigi/) yazısına
bakabilirsiniz. Burada yalnız ölçüm var.

## Ne ölçtüm

Ana desen (S1) şu: G goroutine, tek bir paylaşılan sayacı artırıyor. Sayaca
erişimin dört yolu var.

| Model | Erişim biçimi |
| --- | --- |
| Mutex | sync.Mutex ile korunan sayaç |
| Tek yönlü kanal | Sayacı tek bir sahip-goroutine tutuyor, diğerleri kanala istek yolluyor, cevap beklemiyor |
| İstek-cevaplı kanal | Aynı sahip-goroutine, ama her istek cevap kanalı taşıyor ve cevabı bekliyor |
| atomic | atomic artırma; yalnız bağlam için, başlık iddiasına girmiyor |

İstek kanalı tamponsuz. Tamponlu bir istek kanalı sonucu değiştirebilir; bunu ölçmedim.

İki desen daha koştu: S2'de okuma oranı değişiyor (yüzde 50, 90 ve 99 okuma),
S3'te bir üretici-tüketici kuyruğu farklı tamponlarla (kanal 0, 1, 64, 1024 ve
`sync.Cond`'lu kuyruk) deneniyor. Çekirdek sayısı P, konteynere cpuset ile
verilen çekirdek grubunun büyüklüğü: 1, 2, 4 ve 8.

Bu kayıtta "işlem/sn" bir throughput penceresinin medyanı, "bekleme" ise
gecikme penceresinde bir işlemin sayaca ulaşması için geçen süre. Gecikme
yüzdelikleri log kovalarının üst sınırı olarak raporlanıyor; bu yüzden
491.519 gibi sayılar bir kovanın tavanı, ölçülmüş bir tam değer değil.

## 64 goroutine, dört çekirdek: aynı hız, farklı kuyruk

| Model | İşlem/sn | p50 | p99 | p99.9 |
| --- | --- | --- | --- | --- |
| Mutex | 7.688.545 | 639 ns | 491.519 ns | 1.441.791 ns |
| Tek yönlü kanal | 7.944.446 | 175 ns | 98.303 ns | 114.687 ns |

S1, G=64, P=4, iş yükü W=0. İki hücre de bayraksız. p99 oranı 491.519 / 98.303 = 5,00.

Throughput farkı yüzde 3,3 ve kanalın lehine, yani gürültü içinde. Bekleme
farkı gürültü değil: p50'de 639 ile 175, p99'da 491.519 ile 98.303, p99.9'da
1.441.791 ile 114.687 ns. Mutex aynı toplamı daha geniş bir dağılımla
üretiyor. Ortalamaya bakan bir benchmark bu iki modeli eşit görürdü.

Neden böyle olduğunu ölçmedim. Mutex'in bekleyen goroutine'leri nasıl
uyandırdığına dair bir açıklama kurabilirim, ama bu deneyin ürettiği bir
kanıt olmazdı; yazıya koymuyorum.

## Sekiz çekirdekte tablo döndü

**G=64: dört ve sekiz çekirdekte throughput**

Mutex sekiz çekirdekte iki katına yakın çıkarken tek yönlü kanal geriledi. İstek-cevaplı kanalın P=4 hücresi kalite bayrağı taşıdığı için grafiğe girmedi; P=8 değeri aşağıdaki tabloda.

Kaynak: go-sync-bench, results/2026-09-29/merged/summary.json, S1 hücreleri

|  | P=4 | P=8 |
| --- | --- | --- |
| Mutex | 7.69 M | 15.96 M |
| Tek yönlü kanal | 7.94 M | 6.65 M |

| Model | İşlem/sn (P=8) | Mutex oranı | p99 |
| --- | --- | --- | --- |
| Mutex | 15.962.148 | 1,00 | 114.687 ns |
| Tek yönlü kanal | 6.648.606 | 2,40 | 122.879 ns |
| İstek-cevaplı kanal | 3.970.457 | 4,02 | 131.071 ns |

S1, G=64, P=8. Üç hücre de bayraksız. Oran sütunu Mutex'in ilgili modele karşı katı: 15.962.148 / 6.648.606 = 2,40 ve 15.962.148 / 3.970.457 = 4,02.

Mutex P=4'ten P=8'e 2,08 kat hızlandı. Tek yönlü kanal 0,84 katına indi:
sayacı tek bir sahip-goroutine tutuyor; çekirdek eklemenin ona ne kattığını
bilmiyorum. Nedenini deney ölçmedi; bir hipotez bile öne sürmüyorum.

P=8'de p99 üç modelde de birbirine yakın: 114.687, 122.879 ve 131.071 ns.
Dört çekirdekteki beş katlık fark burada görünmüyor. Hangi modelin
kuyruğunun "daha iyi" olduğu sorusunun tek bir cevabı yok; P'ye bağlı.

Kapsam dışı bırakmam gerekenler: P=1'deki Mutex hücresi (63,10 milyon)
`spread_persistent` ve `observer_effect` bayrağı taşıyor, P=2'deki (13,22
milyon) bayraklı. İkisini de iddiaya almadım. Mutex eğrisinin P'ye göre
monoton olup olmadığını da söyleyemiyorum, çünkü P=1 ve P=2 hücreleri
bayraklı.

## 10.000 goroutine: Mutex hızlı, adaletsizliği açıklanamıyor

| Model | İşlem/sn | Jain | p99 | Bayrak |
| --- | --- | --- | --- | --- |
| Mutex | 14.878.958 | 0,8237 | 27.262.975 ns | fairness_unexplained |
| Tek yönlü kanal | 2.052.681 | 0,9999 | 5.767.167 ns | yok |
| İstek-cevaplı kanal | 1.778.745 | 1,0000 | 6.291.455 ns | yok |

S1, G=10.000, P=8. Mutex/tek yönlü kanal = 7,25; Mutex/istek-cevaplı kanal = 8,36. Jain 1'e yaklaştıkça goroutine'ler eşit pay alıyor.

Mutex burada kanalların 7,25 ve 8,36 katı hızlı. Ama Jain adillik indeksi
0,8237 ve bu değer işaretli: `fairness_unexplained`. Harness adaletsizliği iki
kaynaktan yalnız birine bağlayabiliyor: park davranışına (`sync`). Kural şu: G
≤ P ise, goroutine başına düşen zaman dilimi sınırı b ≥ 1 ise ya da örneklenen
edinimlerin en az yarısı park eşiğini aşıyorsa kaynak `sync`, değilse
`unexplained`. Bu hücrede G=10.000 > P=8, b = 0,12 ve yavaş edinim payı yalnız
0,0176; üç koşul da tutmadığı için adaletsizlik park davranışına bağlanamadı.
Kural Jain değerine bakmıyor. Harness'ın b'den hesapladığı beklenen Jain 0,12
(`expected_quantum_jain`), ölçülen 0,8237; bu hesabın dayandığı kuantum
modelini doğrulamadım, o yüzden zamanlayıcıya atıf yapmıyorum. Mutex'in
adaletsizliğini bir nedene yazamıyorum. Tasarım kuralım gereği bu hücreler
modeller arası adillik kıyasına girmiyor; tabloda "Mutex daha adaletsiz"
demiyorum, "adaletsizliği açıklanamıyor" diyorum.

Aynı tablonun bağlamı: dört çekirdekte Mutex'in Jain değeri G=1.024'te
0,4064, G=10.000'de 0,1892, ikisi de `fairness_unexplained`. Kanal hücrelerinde
G=10.000 ve P=8'deki Jain 0,9999 ve 1; aynı G'de P=4'te tek yönlü kanalın
Jain'i 0,7995 (yeniden koşudan), yani kanalların adilliği de P'ye bağlı.

## Okuma oranı ve kuyruk tamponu

S2'de Mutex, okuma oranı arttıkça hızlanıyor; sahip-goroutine kanalı
neredeyse hiç kıpırdamıyor.

**S2, G=64, P=4: okuma oranına göre throughput**

Mutex bayraksız. Kanalın yüzde 90 okuma hücresi yeniden koşudan geliyor, yüzde 99 okuma hücresi spread_throughput bayrağı taşıyor. RWMutex hücreleri observer_effect bayraklı, grafikte yok.

Kaynak: go-sync-bench, results/2026-09-29/merged/summary.json, S2 hücreleri

|  | %50 okuma | %90 okuma | %99 okuma |
| --- | --- | --- | --- |
| Mutex | 9.86 M | 12.23 M | 13.12 M |
| Sahip-goroutine kanalı | 4.42 M | 4.58 M | 4.5 M |

Yüzde 50 okumada Mutex kanalın 2,23 katı, yüzde 90'da 2,67 katı. Yüzde 99
hücresinin oranını bayraklı olduğu için yazmıyorum. Mutex'in
yüzde 99'daki hızı yüzde 50'ninkinin 1,33 katı.

S3'te G=64, P=4: tamponsuz kanal 5.414.190, tamponu bir olan kanal
6.912.398, tamponu 1.024 olan `sync.Cond`'lu kuyruk 4.461.277 işlem/sn. Bu
üç hücre bayraksız: tampon bire çıkınca kanal 1,28 kat, `sync.Cond`'lu
kuyruğa göre 1,55 kat hızlı. 64 ve 1.024 tamponlu kanal ile 64 tamponlu
`sync.Cond` hücreleri bayraklı; 18,72 ve 21,98 milyon gibi sayılar yalnız
yön gösterir, oran çıkarmadım.

## Sonuç

> **Sonuç**
>
> G=64 ve P=4'te Mutex ile tek yönlü kanal aynı hızda (7,69 ve 7,94 milyon
> işlem/sn) ama Mutex'in p99'u 5 kat yüksek. P=8'de Mutex 2,4 ve 4,0 kat hızlı.
> G=10.000 ve P=8'de Mutex 7,25 ve 8,36 kat hızlı, fakat adaletsizliği bir
> nedene bağlanamıyor. "Hangisi daha hızlı?" sorusunun cevabı G ve P'ye bağlı.

## Nasıl ölçtüm

Deney 845 hücre koştu; her hücre 5 round, her round tekrar başına 0,5 sn
ısınma, 1,5 sn throughput ve 2,0 sn gecikme penceresi. Toplam 4.225 geçerli
tekrar. Tekrarların medyan süresi 4,38 sn; sekiz tekrar bütçeyi aştı (örneğin
`s1-atomic-g10000-p4-w0` 55 sn). Her round × çekirdek grubu container'ından
sonra 60 sn soğuma var; container içindeki hücreler arasında bekleme yok.

Konteyner ağsız ve read-only, 4 GiB belleğe sahip. İlk geçişte uyku kapısı
yoktu; ana makine bir kez uyudu: 11:45-11:59 UTC aralığında
`s1-chan_oneway-g64-p1-w0` hücresinin 3. round'u bundan etkilendi ve bu hücre
iddiaya girmiyor. Yeniden koşudan önce `run.sh`'a `sleep 0` şartı eklendi.

Sonuçlar iki geçişten geliyor. İlk geçiş `sha256:640a046f219a…` imajıyla
2026-09-29T08:34:54Z'de başladı. İlk tasarımın yeniden koşu tetikleyicisi 294
aday seçti (290'ı sapma kapısına, 11'i ikinci bir kapıya takılmıştı; bir hücre
ikisine de takılabilir). Bu, 84 hücrelik bütçeyi aştı. Tetikleyici kırpılmış
aralığa çevrildi: en uçtaki tekrar atılıp kalan aralığa bakılıyor (throughput
için %10, p99 için %50) ve bu yalnız hangi hücrenin yeniden koşulacağını
seçiyor. Bütçe 84'ten 88'e çıkarıldı; 88 hücre `sha256:eabc1573265a…` imajıyla
2026-09-30 01:38-02:31 UTC'de yeniden koşuldu. Birleşik tabloda kaynak `rerun`
görünüyor; 36'sı yeniden koşudan sonra da eşik üstünde kaldı. Hücre medyanı
yine 5 tekrarın hepsinden alınıyor. Bayrak ise ayrı bir ölçüt: tam aralık
üzerinden throughput için %10, p99 için %25, saat kayması için %5.

Kalite bayrakları:

| Bayrak | Hücre sayısı | Anlamı |
| --- | --- | --- |
| observer_effect | 171 | Gecikme penceresindeki işlem hızı, throughput penceresindekinin 0,9 katının altında; ölçüm kendini etkiliyor |
| fairness_unexplained | 235 | Adaletsizlik park davranışına bağlanamıyor: G > P, b < 1 ve örneklenen edinimlerin yarısından azı park eşiğinin üstünde; Jain değerine bakılmaz |
| spread_p99 | 189 | Tekrarlar arası p99 sapması 0,25 üstü |
| spread_throughput | 125 | Tekrarlar arası throughput sapması 0,10 üstü |
| calib_drift | 72 | Saat kalibrasyonu 0,05 üstünde kaydı |
| spread_persistent | 36 | Yeniden koşudan sonra da sapma sürüyor |

Bir hücre birden çok bayrak taşıyabilir; toplam 518 bayraklı hücre, yani 845'in %61,3'ü. Oran %15'i aştığı için ölçümün önerdiği güven düzeyi düşük (confidence: low).

Gürültü tabanı: P=2 için atomic G=2'de Jain 0,9999, P=4 için G=4'te 0,9998,
P=8 için G=4'te 0,9997. Yani ölçüm düzeneği kendi başına adaleti bozmuyor.
Kendi kendini sınayan koşunun 40 denemesinin 40'ı geçti. Yayın komutu
`reproduce` alanında; ham veri ve lisans (MIT) depoda.

> **Tan:** Bu kayıt hangisinin daha hızlı olduğunu söylemiyor, hangisinin ne zaman hızlı olduğunu söylüyor. Bence en değerli satır, Mutex'in adaletsizliğinin bir nedene bağlanamadığı hücre. Nedenini bilmediğiniz bir davranışı üretimde varsaymayın, kendi P'nizde ölçün.

## Sınırlar ve dürüstlük notları

1. **Bayraklı hücrelerin çoğunluğu.** 845 hücrenin 518'i bayraklı. Başlıktaki
   throughput ve p99 oranlarını bu bayrakları taşımayan hücrelerden aldım;
   G=10.000, P=8 Mutex hücresi yalnız adillik bayrağı taşıyor. Bayraklı
   hücreler yön göstermek için anıldıysa öyle etiketlendi.
2. **VM içi vCPU.** Ölçüm bir Docker sanal makinesinde koştu; P/E çekirdek
   ayrımı VM'den görünmez ve küme saati meşgul çekirdek sayısına bağlı. P
   değerleri fiziksel çekirdek değil, cpuset ile verilmiş vCPU grupları.
3. **P=2 gürültülü.** Nedenini doğrulayamadım.
4. **Adillik bayrağının kuralı.** Bayrak G, P, b ve park payına bakıyor;
   zamanlayıcı kaynaklı adaletsizliği ayırt edemiyor. Kuantum modelini
   (`expected_quantum_jain`) doğrulamadım, açıklamalarda kullanmadım.
5. **Gecikme çözünürlüğü.** Gecikme yüzdelikleri log kovalarının üst sınırıdır
   ve 1/16 oranında örneklenmiştir. `< 164 ns` saat çözünürlüğünün 4 katı olan
   tabandır. `starving_fraction` alanını örnek sayısı küçük olduğu için
   kullanmadım.
6. **İstek kanalı tamponsuz.** Tamponlu istek kanalı kanal modellerinin
   sonucunu değiştirebilir.
7. **G=1'de iki goroutine.** Sahip-goroutine modelleri G=1'de 2 goroutine
   koşuyor.
8. **Kapsam dışı.** `sync.Map` hiç ölçülmedi; S2'de atomic de ölçülmedi.
   Üretim iş yükü değil, sentetik bir sayaç ölçüldü.

Kendi sayınızı üretmek için
[go-sync-bench](https://github.com/muhammetsafak/go-sync-bench) deposunu kendi
donanımınızda koşturun; P'yi kendi hedefinize göre seçin, çünkü bu kaydın
en açık sonucu, cevabın P'ye bağlı olduğu.
