PunchJAMB will now handle all HND admissions in polytechnics — NBTECNN Türk13 EYLÜL YAYIN AKIŞI: Bugün televizyonda hangi diziler, filmler, programlar var?The Jerusalem PostIDF warns of rising West Bank tensions despite low terror levels, pushes for preventive operationsInquirerMaguindanao Sur officials turn over loose guns yielded by constituentsESPNTransfer rumors, news: Liverpool look at Inter's Stankovicוואלהדיווחים ברשתות האיראניות: כלי שיט נפגע מירי בסמוך לאי קשםSouth China Morning PostBangladesh draws closer to China in ‘political chessboard’ to balance India tiesCollider‘Landman’ Star Billy Bob Thornton’s Twisted Crime Thriller Is Officially Streaming for FreeCumhuriyetMeksika’da kartellerin yeni yöntemi: Gizli kripto para çiftlikleriRappler[Tech Thoughts] The Jurassic Parkification of AI chatbotsThe Hollywood Reporter‘Look Back’ Breakouts Furi Nanase and Rokka Okada on Growing Up on a Hirokazu Koreeda SetHipertextualLa nueva IA de IBM y la NASA es de código abierto y promete revolucionar la exploración lunar
The Daily Newsstand · Free, Always
Sunday, September 13, 2026

Когда atomic действительно быстрее Mutex

Translate

Привет, Хабр! Эта статья посвящается важной теме работы в многопоточной среде. Для безопасной работы с одним ресурсом из разных горутин разработчик должен быть уверен в безопасности и обособленности действий, чтобы не создать ситуации, в которых "гонки данных" (data race) ломали бы важную логику и создавали коллизии данных.

В случаях, когда нужно обращаться к одному источнику из разных горутин в Go обычно используют sync/atomic или MutexОсновная задача текущей статьи рассказать об их отличиях и о тех случаях, когда лучше применять atomic, чем sync.Mutex. Перед тем, как перейти к различиям, необходимо рассмотреть эти способы по ближе.

sync/atomic

Атомарные операции (atomic) - это операции, которые выполняются полностью или не выполняются вообще. Подобные операции не делимы и часто применяются для выполнения какого-то одного действия. Примером таких действий может быть: чтение, запись, сложение, или сравнение. Такая операция превращается в одну инструкцию для процессора, что позволяет избежать лишних блокировок.

Ниже рассматриваются некоторые методы из пакета sync/atomic для использования атомарных операций.

atomic.Load

Функция Load используется для атомарного чтения значений из переменной. Она позволяет безопасно получать значение, которое одновременно может измениться другими горутинами.

var isReady int32 = 0


// Атомарно читаем общие данные:
ready := atomic.LoadInt32(&isReady) // LoadInt32() - для чтения именно int32 типа
fmt.Printf("%v", ready)

Для разных типов в пакете sync/atomic существуют соответствующие функции. Например, LoadInt32() используется для атомарного чтения типа int32.

atomic.Store

Функция Store используется для атомарной записи значений.

var isReady int32 = 0

// Заносим данные в переменную по адресу в памяти:
atomic.StoreInt32(&isReady, 1)
fmt.Printf("%v", isReady)

Атомарная запись значения гарантирует, что другие горутины не увидят промежуточного состояния операции. При этом Store не блокирует другие горутины и не запрещает им выполнять последующие записи в ту же переменную, вместо этого использует низкоуровневые инструкции процессора.

atomic.Add

Функция Add необходима для атомарного сложения. Она очень удобна в случаях, когда нужно повысить значение данных (к примеру того же пресловутого счётчика).

var isReady int32 = 0

atomic.AddInt32(&isReady, 1)
fmt.Printf("%v", isReady)

atomic.CompareAndSwap

Функция CompareAndSwap является очень интересной. Она сравнивает значение из переменной с тем, которое вы указываете и заменяет на новое. Если значения равны, она заменяет на новое и возвращает true, если нет, то вернёт false, что означает, что замена не состоялась.

var isReady int32 = 0

result := atomic.CompareAndSwapInt32(&isReady, 1, 1)
fmt.Printf("%v", result)

Это были основные функции из пакета sync/atomic, но там также есть различные вариации функций для побитовых операций, таких как OR.

sync.Mutex

Это ещё один инструмент для синхронизации горутин. Он гарантирует, что в один и тот же момент времени только одна горутина может производить операции над данными, блокируя доступ из других горутин.
При базовом использовании в Mutex используются два метода:

  1. Lock() - блокировка доступа.

  2. Unlock() - разблокировка доступа.

Для начала рассмотрим пример кода без мьютексов (с гонкой данных):

var Counter int32 = 0

// Гонка данных без мьютекса:
func main() {
	
	wg := sync.WaitGroup{}
	wg.Add(2)

	go operator1(&wg)
	go operator2(&wg)
	
	wg.Wait()
	fmt.Printf("Final counter: %d", Counter)
}


func operator1(wg *sync.WaitGroup) {
	defer wg.Done()
	
	for i := 0; i < 5; i++  {
		Counter ++
		fmt.Println("\n", Counter)
	}
}


func operator2(wg *sync.WaitGroup) {
	
	defer wg.Done()
	for i := 0; i < 5; i++  {
		Counter ++
		fmt.Println("\n", Counter)
	}
}

При запуске go run -race . мы увидим такой вывод:

 1

 2

 3

 4

 5
==================
WARNING: DATA RACE
Read at 0x0000006262ac by goroutine 8:
  main.operator1()
      /home/maks/Projects/test1/main.go:28 +0xb6
  main.main.gowrap1()
      /home/maks/Projects/test1/main.go:16 +0x2e

Previous write at 0x0000006262ac by goroutine 9:
  main.operator2()
      /home/maks/Projects/test1/main.go:38 +0xcc
  main.main.gowrap2()
      /home/maks/Projects/test1/main.go:17 +0x2e

Goroutine 8 (running) created at:
  main.main()
      /home/maks/Projects/test1/main.go:16 +0xbe

Goroutine 9 (finished) created at:
  main.main()
      /home/maks/Projects/test1/main.go:17 +0x124
==================

 6

 7

 8

 9

 10
Final counter: 10Found 1 data race(s)
exit status 66

Необходимо исправить проблему, сделав работу безопасной с использованием мьютексов:


var Counter int32 = 0

func main() {
	
	wg := sync.WaitGroup{}
	wg.Add(2)
	var mutex sync.Mutex

	go operator1(&wg, &mutex)
	go operator2(&wg, &mutex)
	
	wg.Wait()
	fmt.Printf("Final counter: %d", Counter)
}


func operator1(wg *sync.WaitGroup, mutex *sync.Mutex) {
	defer wg.Done()
	
	for i := 0; i < 5; i++  {
		mutex.Lock()
		Counter ++
		fmt.Println("\n", Counter)
		mutex.Unlock()
	}
}


func operator2(wg *sync.WaitGroup, mutex *sync.Mutex) {
	
	defer wg.Done()
	for i := 0; i < 5; i++  {
		mutex.Lock()
		Counter ++
		fmt.Println("\n", Counter)
		mutex.Unlock()
	}
}

Но надо также учитывать, что sync.Mutex - это не просто флаг "свободен/занят". У него есть два режима работы, которые делают этот инструмент быстрым и справедливым для каждой горутины:

  1. Нормальный режим: Работает по умолчанию. Как только мьютекс освободился, его захватить может любая из горутин, в том числе та, которая только что отработала. Это даёт большую производительность, но может привести к "голоданию".

  2. Режим голодания: если горутина в очереди ждёт дольше 1 миллисекунды, мьютекс переключается в этот режим и здесь право захвата строго передаётся следующей в очереди горутине.

sync.RWMutex

Также стоит упомянуть про sync.RWMutex, который используется для сценариев, когда данные чаще читаются, чем изменяются. Здесь есть два метода RLock() и RUnlock(), которые используются для захвата мьютекса для чтения и освобождения соответственно. Множество горутин могут одновременно держать RLock(), если никто не пишет.

Различия sync/atomic и sync.Mutex

После введения основ по каждому инструменту, можно перейти к основной задаче этой статьи. На первый взгляд можно подумать, что оба инструмента выполняю одну и ту же роль - делают одновременный доступ из разных горутин безопасным. От части с этим согласиться можно, но на деле у них больше различий, чем сходств.

sync/atomic - это пакет стандартной библиотеки, которая выполняется как одиночная операция, в то время как sync.Mutex - это конструкция рантайма, которая используется для критической секции (части кода) и полностью исключает использования конкретной части другими горутинами. Таким образом, в случае, когда необходимо сделать произвольное количество переменных, операций в конкретном обращении к общему участку памяти, лучше использовать sync.Mutex.

sync/atomic всегда быстрее sync.Mutex?

Это очень спорное заявление. Да, sync/atomic действительно может быть быстрее в случае, когда операция очень мала и её можно выразить одной атомарной операцией, но есть случаи, когда atomic действительно проигрывает Mutex. В бенчмарке ниже происходит сравнение atomic и Mutex на примере простой операции увеличения счётчика.

package main

import (
	"sync"
	"sync/atomic"
	"testing"
)

// Тест для Mutex
func BenchmarkMutex(b *testing.B) {
	var counter int64
	var mutex sync.Mutex

	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			mutex.Lock()
			counter++
			mutex.Unlock()
		}
	})
}

// Тест для atomic
func BenchmarkAtomic(b *testing.B) {
	var counter int64

	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			atomic.AddInt64(&counter, 1)
		}
	})
}

Результат:

maks@fedora ~/P/test1 [1]> go test -bench=. -cpu=1,2,4,8
goos: linux
goarch: amd64
pkg: test1
cpu: AMD Ryzen 5 7640HS w/ Radeon 760M Graphics     
BenchmarkMutex      	378627867	         3.171 ns/op
BenchmarkMutex-2    	195535148	         5.885 ns/op
BenchmarkMutex-4    	80525481	        13.01 ns/op
BenchmarkMutex-8    	46922386	        23.20 ns/op
BenchmarkAtomic     	753790170	         1.562 ns/op
BenchmarkAtomic-2   	193068660	         6.053 ns/op
BenchmarkAtomic-4   	247736799	         4.845 ns/op
BenchmarkAtomic-8   	190388100	         6.297 ns/op
PASS
ok  	test1	13.107s

Здесь видно как меняется производительность при разном уровне параллельного выполнения. Так при обычном  BenchmarkMutexиBenchmarkAtomic результаты отличаются почти в два раза ( 3.171 ns/opвBenchmarkMutexпротив1.562 ns/opвBenchmarkAtomic). Но с ростом количества потоков ситуация проявляется ещё сильнее: в BenchmarkMutex-8значение23.20 ns/opпротивBenchmarkAtomic-8 со значением6.297 ns/op. Так как Здесь демонстрируется атомарная операция (увеличение счётчика). Её вполне можно уместить в одну инструкцию процессора.

При этом высокая конкуренция не делает atomic автоматически медленнее. Несколько ядер конкурируют за одну cahce line, содержащую счётчик и это даёт дополнительные накладные расходы на поддержание согласованности кэшей. Однако Mutex в этой ситуации тоже не является бесплатным. Он сам требует синхронизации между горутинами и добавляет стоимость блокировки и разблокировки.

По этой причине, при разработке лучше не ставить вопрос сравнения общей скорости работы atomic и Mutex, а смотреть за состоянием программы, за тем, какое количество одновременных горутин будет пытаться изменить один участок памяти и тем, какое количество операций и переменных за раз необходимо обезопасить.

Что и когда использовать?

Таким образом, использовать sync/atomic рентабельно, когда:

  • Вы делаете простой счётчик, работа с флагами.

  • У вас одна переменная, которая не зависит от других.

  • Вы понимаете порядок, в котором операции чтения и записи выполняются

Использовать sync.Mutex рентабельно, когда:

  • Хотите защитить больше одной переменной.

  • Есть инварианты между полями структуры.

  • Критическая секция содержит вызовы функций, I/O, аллокации

  • Вы не уверены, что вам лучше использовать

Заключение

Надеюсь эта статья была для вас полезна и помогла лучше понять особенности использования sync/atomic и sync.Mutex в Go. После прочтения этой статьи у вас должно было появиться понимание, что atomic - это не магический инструмент, который позволяет сделать из любой многопоточной реализации "ракету". Они больше подходят для тех случаев, когда необходимо обезопасить работу с отдельным значением, но по мере усложнения программы использование atomic может стать не выгодным. В конечном счёте, если вам необходимо защитить несколько связанных переменных или выполнить несколько операций, как единое целое, лучше использовать sync.Mutex. Подобный сценарий окажется более подходящим и понятным.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.