Первая статья будет о сравнении атомарных примитивов в 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.