Наиболее распространенное использованиеBoost.Atomicдля реализации пользовательских протоколов синхронизации потоков: Цель состоит в координации доступа потоков к общим переменным, чтобы избежать «конфликтов». Программист должен знать, что компиляторы, процессоры и иерархии кэша могут, как правило, переупорядочивать ссылки на память по желанию. Как следствие, такая программа, как:
int x = 0, int y = 0;
thread1:
x = 1;
y = 1;
thread2
if (y == 1) {
assert(x == 1);
}
Это может привести к провалу, поскольку нет никакой гарантии, что чтение<x
>по потоку 2 «видит» запись по потоку 1.
Boost.Atomicиспользует концепцию синхронизации, основанную на отношении, происходящем до, для описания гарантий, при которых такие ситуации, как вышеупомянутая, не могут возникнуть.
В оставшейся части этого раздела будет обсуждаться— до— практическим способом вместо того, чтобы давать полностью формализованное определение. Читателю предлагается дополнительно взглянуть на обсуждение правильности некоторых изпримеров.
В качестве вводного примера, чтобы понять, как аргументация с использованиемпроисходит до того, какработает, рассмотрим две нити, синхронизирующиеся с использованием общего мутекса:
mutex m;
thread1:
m.lock();
...
m.unlock();
thread2:
m.lock();
...
m.unlock();
«Интуиция, основанная на блокировке», будет утверждать, что A и B не могут выполняться одновременно, поскольку кодовые пути требуют общего замка.
Однако можно прийти к такому же выводу, используя, прежде чем: Либо поток1, либо поток2 преуспеют первыми<m.lock()
>. Если это поток1, то, как следствие, поток2 не может преуспеть в<m.lock()
>до того, как поток1 выполнил<m.unlock()
>, следовательно, A.— доВ в этом случае. По симметрии, если нить 2 преуспевает на<m.lock()
>первой, мы можем заключить B.происходит доА.
Поскольку это уже исчерпывает все варианты, мы можем заключить, что либо Апроисходит — доВ, либо Впроисходит — доА должно всегда держаться. Очевидно, что они не могут указать, чтоиз двух отношений, но одного достаточно, чтобы заключить, что A и B не могут конфликтовать.
Сравните реализациюspinlock, чтобы увидеть, как концепция взаимного исключения может быть отображена на.Boost.Atomic.
Самый простой шаблон для координации потоковBoost.Atomicиспользует<release
>и<acquire
>на атомной переменной для координации: Если...
- ... нить 1 выполняет операцию А,
- ... нить 1 впоследствии записывает (или атомарно модифицирует) атомную переменную с<
release
>семантической,
- ... thread2 считывает (или атомарно считывает и модифицирует) значение этого значения из той же атомной переменной с<
acquire
>семантической и
- ...поток 2 впоследствии выполняет операцию В,
Апроисходит доВ.
Рассмотрим следующий пример
atomic<int> a(0);
thread1:
...
a.fetch_add(1, memory_order_release);
thread2:
int tmp = a.load(memory_order_acquire);
if (tmp == 1) {
...
} else {
...
}
В этом примере возможны два пути исполнения:
- <
store
>работа по потоку1 предшествует<load
>по потоку2: В этом случае поток 2 будет выполнять B, а «Aпроисходит доB» выполняется, поскольку все вышеперечисленные критерии удовлетворены.
- <
load
>работа по потоку2 предшествует<store
>по потоку1: В этом случае поток 2 будет исполнять C, но «Aпроисходит — доC» не удерживает: поток 2 не считывает значение, записанное потоком 1 через<a
>.
Следовательно, A и B не могут конфликтовать, а A и Cмогутконфликтовать.
Ограничения упорядочивания обычно указываются вместе с доступом к атомной переменной. Однако также возможно проводить операции «забора» изолированно, в этом случае забор работает в сочетании с предыдущими (для<acquire
>,<consume
>или<seq_cst
>операциями) или последующими (для<release
>или<seq_cst
>) атомными операциями.
Пример из предыдущего раздела также может быть написан следующим образом:
atomic<int> a(0);
thread1:
...
atomic_thread_fence(memory_order_release);
a.fetch_add(1, memory_order_relaxed);
thread2:
int tmp = a.load(memory_order_relaxed);
if (tmp == 1) {
atomic_thread_fence(memory_order_acquire);
...
} else {
...
}
Это обеспечивает те же гарантии заказа, что и ранее, но исключает (возможно, дорогостоящую) операцию заказа памяти в случае C.
Второй шаблон для координации потоков черезBoost.Atomicиспользует<release
>и<consume
>на атомной переменной для координации: Если...
- ... нить 1 выполняет операцию А,
- ... нить 1 впоследствии записывает (или атомарно модифицирует) атомную переменную с<
release
>семантической,
- ... thread2 считывает (или атомарно считывает и модифицирует) значение этого значения из той же атомной переменной с<
consume
>семантической и
- ...поток 2 впоследствии выполняет операцию В, котораявычислительно зависит от значения атомной переменной,
Апроисходит доВ.
Рассмотрим следующий пример
atomic<int> a(0);
complex_data_structure data[2];
thread1:
data[1] = ...;
a.store(1, memory_order_release);
thread2:
int index = a.load(memory_order_consume);
complex_data_structure tmp = data[index];
В этом примере возможны два пути исполнения:
- <
store
>работа по потоку1 предшествует<load
>по потоку2: В этом случае поток 2 будет читать<data[1]
>и «Апроисходит доВ», поскольку все вышеуказанные критерии удовлетворены.
- Работа<
load
>по потоку2 предшествует<store
>по потоку1: В этом случае поток 2 будет читать<data[0]
>, а «Aпроисходит доB» не удерживает: поток 2 не считывает значение, записанное потоком 1 - 121 .
Здесьпроисходит - доотношения помогают гарантировать, что любые доступы (предположительно записывается) к<data[1]
>по потоку1 происходят до того, как доступы (предположительно читается) к<data[1]
>по потоку2: Не имея этой связи, поток 2 может видеть устаревшие / непоследовательные данные.
Обратите внимание, что в этом примере тот факт, что операция B вычислительно зависит от атомной переменной, поэтому ошибочна будет следующая программа:
atomic<int> a(0);
complex_data_structure data[2];
thread1:
data[1] = ...;
a.store(1, memory_order_release);
thread2:
int index = a.load(memory_order_consume);
complex_data_structure tmp;
if (index == 0)
tmp = data[0];
else
tmp = data[1];
<consume
>Наиболее часто (и наиболее безопасно) См.ограничения, используемые с указателями, сравните, например,синглтон с двойной проверкой блокировки.
Третий шаблон для координации потоков черезBoost.Atomicиспользует<seq_cst
>для координации: Если...
- ... нить 1 выполняет операцию А,
- ... нить 1 впоследствии выполняет любую операцию с<
seq_cst
>,
- ... нить 1 впоследствии выполняет операцию В,
- ... резьба 2 выполняет операцию C,
- ... нить 1 впоследствии выполняет любую операцию с<
seq_cst
>,
- ...поток 2 впоследствии выполняет операцию D,
Апроисходит доD или Спроисходит доB.
При этом не имеет значения, работают ли нити 1 и нити 2 на одних и тех же или разных атомных переменных, или используют операцию «в одиночку»<atomic_thread_fence
>.