Гарантированно ли в следующем коде значение x_value равно 42?
std::atomic<int> x;
Thread A:
x.store(42, std::memory_order_seq_cst);
wakeThreadB();
Thread B:
int x_value = x.load(std::memory_order_relaxed);
assert(x_value == 42);
Я попытался проверить это, и мне показалось, что поток B всегда считывает правильное значение. Но я не уверен, что это гарантировано.
@emptysamurai: проблема с синхронизацией в том, что все детали имеют значение. Не зная, как именно реализовано пробуждение, я не думаю, что на ваш вопрос можно ответить.
На самом деле у меня есть тихий сложный код, но в конечном итоге Thread B разблокируется с помощью condional_variable или существует memory_order_seq_cst, хранимая потоком B в переменной состояния, что гарантирует, что он не перейдет в спящий режим и будет считывать значение из x
std::condition_variable
использует мьютекс для синхронизации (вам нужно вызвать блокировку/разблокировку в потоке A, чтобы он работал правильно). Таким образом, два потока будут синхронизировать свои данные. Это означает, что вы можете отказаться от атомарного и использовать вместо него обычное целое число, и оно гарантированно будет равно 42.
Детали имеют значение, но сама по себе эта расслабленная нагрузка гарантированно будет только атомарной (en.cppreference.com/w/cpp/atomic/memory_order).
Более строго упорядоченное хранилище не становится видимым раньше и не заставляет другие потоки ждать, пока они его не увидят. Это только заставляет другие операции в том же потоке ждать, чтобы получить требуемый порядок. Единственное, что может синхронизироваться между A и B, — это wakeThreadB()
, но вы не показали, как это реализовано.
Расслабленная загрузка не синхронизируется ни с какой другой загрузкой/сохранением до или после нее.
Также обратите внимание, что семантика порядка памяти связана с видимостью другой работы, выполненной в отношении переменной синхронизации.
Так, например. следующее будет неправильным:
std::atomic<int> x;
int y;
void func_A()
{
y = 1;
x.store(42, std::memory_order_seq_cst); // "releases" y = 1
}
void func_B()
{
while (x.load(std::memory_order_relaxed) != 42) {} // does NOT "acquire" anything
assert(y == 1); // not guaranteed
}
int main()
{
std::thread a(&func_A), b(&func_B);
a.join();
b.join();
}
Обязательное примечание здесь: «это всегда работает на моей машине» не делает это правильным; без синхронизации это гонка данных, форма неопределенного поведения.
Но в вашем конкретном случае, если под wakeThreadB()
вы подразумеваете создание std::thread
экземпляра с кодом потока B в качестве функции потока, тогда код на самом деле правильный - std::thread
создание является событием синхронизации (см. [thread.thread.constr]/5), поэтому любая загрузка в потоке B гарантированно увидит все, что было сделано до того, как поток B был запущен.
Это означает, что атомарное хранилище x
вообще не имеет значения, код будет правильным даже с неатомарным int
:
void func_B();
int x;
std::thread t;
void func_A()
{
x = 42;
t = std::thread(&func_B); // "releases" x = 42
}
void func_B() // "acquires" x = 42
{
assert(x == 42); // guaranteed success
}
int main()
{
func_A();
t.join();
}
Точно так же std::condition_variable
использует мьютекс внутри, а освобождение/блокировка мьютекса является событием синхронизации (см. [thread.mutex.requirements.mutex]/25), поэтому уведомление другого потока через condition_variable
также будет работать правильно без необходимости любые атомы.
Зависит от механизма, который вы используете для пробуждения потока B. Если это какое-то атомарное расслабленное чтение/запись, то нет гарантии, что
x_value
будет 42. При этом не уверен, на какой архитектуре он может выйти из строя.