← На главную

pthreads от So обгоняет Go, но только без парковки

13.07.2026 15:56 · hackernews

Разработчик Solod (So) — строгого подмножества Go, которое транслируется в чистый C без рантайма и сборщика мусора — добавил в язык конкурентность. В итоге он выяснил: на pthreads можно сделать многое из того, что есть в Go, но с честными компромиссами.

В основе всей конкурентности So лежат два примитива POSIX: мьютекс (pthread_mutex_t) и условная переменная (pthread_cond_t). sync.Mutex и sync.Cond — это тонкие обёртки, которые при ошибке вызывают панику. Атомики (sync/atomic) мапятся прямо на __atomic-встроенные функции компилятора C — они работают с той же скоростью, что и в Go: Load, Store и CompareAndSwap выполняются за одинаковое время.

Для создания потоков используется conc.Go — это обёртка над pthread_create. Но вызывать её в цикле для каждой мелкой задачи нельзя: OS-потоки дорогие. Вместо этого есть conc.Pool — фиксированный набор воркеров, которые тянут задачи из общей кольцевой очереди. Реализация занимает около 200 строк и представляет собой классическую связку producer-consumer на мьютексе и трёх условных переменных: notEmpty, notFull и allDone. Воркеры запускают задачи вне блокировки, так что они выполняются параллельно.

Каналы (conc.Chan) работают в двух режимах. Буферизированный — это мьютекс с кольцевым буфером и парой условных переменных. Небуферизированный — rendezvous: отправитель публикует значение и ждёт, пока получатель скопирует данные прямо из его стека (без промежуточного буфера), после чего получатель будит отправителя. Безопасность обеспечивается тем, что отправитель не уходит, пока не получит подтверждение.

Производительность — главный компромисс. Когда никому не нужно парковаться, So обгоняет Go: uncontended mutex — 9 ns против 14 ns, contended spin — 27 ns против 75 ns. Но как только поток паркуется (условная переменная, блокировка на канале), So проигрывает в 7–10 раз — из-за системных вызовов. Небуферизированный канал в 23 раза медленнее Go (3 µs против 130 ns). Буферизированный с ёмкостью 100 — уже только в 2 раза (70 ns против 33 ns). Когда задачи крупные (40 мкс CPU или 1 мс IO), разница сходит на нет — So проигрывает всего 10%.

Разработчик намеренно отказался от userspace-планировщика (файберов) в пользу простоты: pthreads знают все C-программисты, а потери на парковке приемлемы для грубозернистых задач. Вся конкурентность вынесена в стандартную библиотеку, а не в язык, чтобы следовать правилу «никаких скрытых аллокаций» — память под каналы и пулы выделяется явно через переданный аллокатор.

Вывод: подход не подходит для тысяч лёгких горутин, но для нескольких воркер-потоков, обрабатывающих крупные задачи, он работает почти так же хорошо, как Go, и в сто раз проще.

Читать оригинал →