Первая статья будет о сравнении атомарных примитивов в Go и C++.
Атомарные операции — самые примитивные операции синхронизации. Более сложная конструкция синхронизации использует скрыто атомарные примитивы. Например, Mutex и WaitGroup.
Атомарная операция гарантирует, что только один поток будет обращаться к памяти за раз. Секрет этой операции в том, что у ассемблера есть специальные инструкции.
Давайте проверим простой пример:
var value int // global counter func operation() { for i := 0; i < 1000000; i++ { value++ } } func main() { go operation() go operation() }
Ассемблер для инкрементной части
lw $t0, counter addi $t0, $t0, 1 sw $t0, counter
Если мы будем запускать его на машине с двумя процессорами, то ожидаем именно такой порядок выполнения.
Но на самом деле в какой-то момент времени это может быть:
У меня получилось 1012998 вместо 2000000
Это большая проблема. На этот случай в ассемблере есть специальные конструкции, которые я не буду описывать. Это уровень, на котором мы не должны играть.
Go
Из пакета go sync/atomic не пишутся в go. Вместо этого они напрямую указывают на исходный код ассемблера, за исключением atomic.Value.
Go содержит следующие функции: Add, CompareAndSwap, Load, Store и Swap для типов *int32, *int64, *uint32, *uint64, *uintptr.
Добавить автоматически добавляет дельту к *addr и возвращает новое значение.
CompareAndSwapIntвыполняет операцию сравнения и замены для значения.
Загрузитьатомарно загружает *addr.
Store атомарно сохраняет val в *addr.
Swap атомарно сохраняет новое значение в *addr и возвращает предыдущее значение *addr.
Переписал наш пример с новыми функциями.
var value int32 // global counter func operation() { for i := 0; i < 1000000; i++ { atomic.AddInt32(&value, 1) } } func main() { go operation() go operation() }
Теперь результат 2000000.
Type Value обеспечивает очень удобный способ атомарного обновления любого значения. У него есть два метода: Load() (x interface{}) и Store(x interface{}). Хорошие примеры находятся в godoc.
C++
Пакет Atomic содержит классы atomic и atomic_flag.
atomic_flag обеспечивает очень удобный способ атомарного обновления глобального флага. Он имеет два метода. test_and_set, который проверяет, что флаг еще не установлен, если он не устанавливает флаг и возвращает true. Если он сидит, он возвращает false и ничего не делает. Сбросьте флаг сброса метода. "хороший пример"
std::atomic_flag lock_stream = ATOMIC_FLAG_INIT; // global
...
// inside thread
if (lock_stream.test_and_set())// set flag
lock_stream.clear();// reset flag
перейти к реализации
package main import ( "sync/atomic" "time" ) var global_flag *uint32 = new(uint32) func runner(id string) { for i := 0; i < 90000000; { i++ } // test_and_set if atomic.CompareAndSwapUint32(global_flag, 0, 1) { println(id + " won!") } } func main() { go runner("1") go runner("2") time.Sleep(time.Second) // clear flag atomic.StoreUint32(global_flag, 0) }
Класс atomic обеспечивает добавление, сравнение и замену, загрузку, сохранение и обмен для основных типов. compare_exchange_weak и compare_exchange_strong делают то же самое, но с разным скрытым эффектом. compare_exchange_weak может возвращать false, хотя значение изменилось! Иногда это может быть быстрее для некоторых алгоритмов.
Cpp также предоставляет функции макросов TYPE_LOCK_FREE и atomic_is_lock_free. Я не понял. Я понял. Но почему?!! Атомарные операции не могут быть без блокировки в C++.
Есть перечисление memory_order, которое должно быть передано в atomic_signal_fence или atomic_thread_fence.
Вывод
C++ ожидаемо намного сложнее. Так много низкоуровневых неявных вещей, которые могут быть разными в разных реализациях!
идти ожидаемо просто. Вместо одной неявной операции в C++ можно использовать две явные операции в go.