У меня есть рабочие потоки, выполняющие критичную по времени обработку с регулярными интервалами (примерно 1 кГц). В каждом цикле рабочих будят для выполнения работы, каждая из которых должна (в среднем) завершаться до начала следующего цикла. Они работают с одним и тем же объектом, который может иногда изменяться основным потоком.
Чтобы предотвратить скачки, но разрешить модификацию объекта до следующего цикла, я использовал спин-блокировку вместе с атомарным счетчиком, чтобы записать, сколько потоков все еще работают:
class Foo {
public:
void Modify();
void DoWork( SomeContext& );
private:
std::atomic_flag locked = ATOMIC_FLAG_INIT;
std::atomic<int> workers_busy = 0;
};
void Foo::Modify()
{
while( locked.test_and_set( std::memory_order_acquire ) ) ; // spin
while( workers_busy.load() != 0 ) ; // spin
// Modifications happen here ....
locked.clear( std::memory_order_release );
}
void Foo::DoWork( SomeContext& )
{
while( locked.test_and_set( std::memory_order_acquire ) ) ; // spin
++workers_busy;
locked.clear( std::memory_order_release );
// Processing happens here ....
--workers_busy;
}
Это позволяет немедленно завершить всю оставшуюся работу при условии, что хотя бы один поток запущен, и всегда будет блокироваться до того, как другой рабочий сможет начать работу для следующего цикла.
Доступ к atomic_flag
осуществляется с помощью заказов на получение и освобождение памяти, что, по-видимому, является приемлемым способом реализации спин-блокировок в C ++ 11. Согласно документации на cppreference.com:
memory_order_acquire
: операция загрузки с этим порядком памяти выполняет операцию получения в затронутой области памяти: никакие обращения к памяти в текущем потоке не могут быть переупорядочены до этой загрузки. Это гарантирует, что все записи в других потоках, которые выпускают ту же атомарную переменную, будут видны в текущем потоке.
memory_order_release
: операция сохранения с этим порядком памяти выполняет операцию освобождения: никакие обращения к памяти в текущем потоке не могут быть переупорядочены после этого сохранения. Это гарантирует, что все записи в текущем потоке будут видны в других потоках, которые получают ту же атомарную переменную, а записи, которые несут зависимость в атомарной переменной, станут видимыми в других потоках, которые используют ту же атомарную переменную.
Насколько я понимаю выше, этого достаточно для синхронизации защищенного доступа между потоками, чтобы обеспечить поведение мьютекса, без чрезмерной консервативности в отношении порядка памяти.
Я хочу знать, можно ли еще больше ослабить упорядочение памяти, потому что побочным эффектом этого шаблона является то, что я использую мьютекс с блокировкой вращения для синхронизации другой атомарной переменной.
Все вызовы ++workers_busy
, --workers_busy
и workers_busy.load()
в настоящее время имеют порядок памяти по умолчанию, memory_order_seq_cst
. Учитывая, что единственное интересное использование этого атома - это разблокировать Modify()
с помощью --workers_busy
(который не синхронизируется мьютексом спин-блокировки), можно ли использовать тот же порядок получения-освобождения памяти с этой переменной, используя расслабленное приращение? т.е.
void Foo::Modify()
{
while( locked.test_and_set( std::memory_order_acquire ) ) ;
while( workers_busy.load( std::memory_order_acquire ) != 0 ) ; // <--
// ....
locked.clear( std::memory_order_release );
}
void Foo::DoWork( SomeContext& )
{
while( locked.test_and_set( std::memory_order_acquire ) ) ;
workers_busy.fetch_add( 1, std::memory_order_relaxed ); // <--
locked.clear( std::memory_order_release );
// ....
workers_busy.fetch_sub( 1, std::memory_order_release ); // <--
}
Это верно? Возможно ли дальнейшее ослабление любого из этих порядков памяти? И какое это имеет значение?
fetch_sub
за пределами блокировки вращения не должно быть не менееmemory_order_acq_rel
, чтобы гарантировать, что он видит записи других потоков в счетчик, и, чтобы другие потоки видели запись, которую он выполняет? Может, что-то не замечает. - person ShadowRanger   schedule 02.02.2016memory_order
константы не используют специальных аппаратных функций, не означает, что они ничего не делают. Даже на x86 (который имеет строго упорядоченную семантику памяти) выборmemory_order
изменяет ограничения оптимизации / переупорядочения компилятора; двеmemory_order_relaxed
операции подряд могут быть заменены компилятором, поэтому вторая операция выполняется первой. Аналогично,relaxed
илиrelease
store
s обычно несут нулевые накладные расходы, но сmemory_order_seq_cst
компилятор добавляет явные, дорогостоящие (~ 100 циклов задержки)mfence
инструкции. - person ShadowRanger   schedule 02.02.2016lock
в некоторых случаях (при условии, что операция может быть завершена за один цикл), тогда как большинство компиляторов выдадут префиксlock
без него, это не большая задержка ~ 100-500 циклов в зависимости от чипа, но все же ... это НАМНОГО дороже, чем обычное приращение. - person Mgetz   schedule 18.02.2016lock
в операции? - person curiousguy   schedule 29.11.2019