Я предлагаю:
void* ThreadFunc(void* arg) {
struct TContext* ctxt = arg;
volatile int* counter = ctxt->Counter;
fprintf(stderr, "This is %s thread\n", ctxt->Name);
while (1)
{
int count ;
count = *counter ; // NB: volatile*
if (count >= MAX)
break ;
if ((count % 2) == ctxt->Mod)
{
printf("%d ", count) ;
*counter = count + 1 ;
} ;
} ;
pthread_exit(0);
}
Что, по крайней мере для x86 / x86_64, даст тот эффект, который, я думаю, вы искали, а именно, что два потока по очереди увеличивают счетчик.
Действительно интересный вопрос: почему это работает: -)
Постскриптум
Приведенный выше код критически зависит от четырех вещей:
между потоками используется только одно значение - счетчик,
счетчик одновременно управляет данными и - ls бит счетчика сигнализирует, какой поток должен продолжаться.
чтение и запись счетчика должны быть атомами c - поэтому каждое чтение счетчика считывает последнее записанное значение (а не некоторые комбинация предыдущей и текущей записи).
компилятор должен выдать код для фактического чтения / записи счетчика из / в память внутри l oop.
Теперь (1) и (2) определяют c для этой конкретной проблемы. (3) обычно верно для int
(хотя может потребоваться правильное выравнивание). (4) достигается путем определения счетчика как volatile
.
Итак, я изначально сказал, что это будет работать "по крайней мере для x86 / x86_64", потому что я знаю (3) верно для этих устройств, но Я также считаю, что это справедливо для многих (большинства?) Распространенных устройств.
Более чистая реализация будет определять счетчик _Atomic
следующим образом:
#include <stdatomic.h>
void* ThreadFunc(void* arg) {
struct TContext* ctxt = arg;
atomic_int* counter = ctxt->Counter;
fprintf(stderr, "This is %s thread\n", ctxt->Name);
while (1)
{
int count ;
count = atomic_load_explicit(counter, memory_order_relaxed) ;
if (count > MAX) // printing up to and including MAX
break ;
if ((count % 2) == ctxt->Mod)
{
printf("%d ", count) ;
atomic_store_explicit(counter, count + 1, memory_order_relaxed) ;
} ;
} ;
pthread_exit(0);
}
Что делает (3) и (4) явный. Но обратите внимание, что (1) и (2) по-прежнему означают, что нам не нужно упорядочивать память. Каждый раз, когда каждый поток читает счетчик, бит0 сообщает ему, «владеет» ли он счетчиком. Если он не владеет счетчиком, поток зацикливается, чтобы прочитать его снова. Если он владеет счетчиком, он использует значение, а затем записывает новое значение - и поскольку он передает «владение», он возвращается к чтению l oop (он не может ничего делать со счетчиком, пока не «владеет» им снова). После того, как MAX + 1 будет записано в счетчик, ни один из потоков не будет его использовать или изменять, так что это тоже безопасно.
Брат Занятый русский правильный, здесь есть «гонка данных», но это разрешается зависимостью данных, особенно в этом случае.
В целом
Приведенный выше код не очень полезен, если у вас нет других приложений с одним общим значением. Но это можно обобщить, используя операции memory_order_acquire
и memory_order_acquire
atomi c.
Предположим, у нас есть некоторый struct shared
, который содержит некоторое (нетривиальное) количество данных, которые будет генерировать один поток, и другой будет потреблять. Предположим, мы снова используем atomic_uint counter
(изначально ноль) для управления доступом к данному struct shared parcel
. Теперь у нас есть поток производителя, который:
void* ThreadProducerFunc(void* arg)
{
atomic_uint counter = &count ; // somehow
....
while (1)
{
uint count ;
do
count = atomic_load_explicit(counter, memory_order_acquire) ;
while ((count & 1) == 1) ;
... fill the struct shared parcel, somehow ...
atomic_store_explicit(counter, count + 1, memory_order_release) ;
} ;
....
}
и поток потребителя, который:
void* ThreadConsumerFunc(void* arg)
{
atomic_uint counter = &count ; // somehow
....
while (1)
{
uint count ;
do
count = atomic_load_explicit(counter, memory_order_acquire) ;
while ((count & 1) == 0) ;
... empty the struct shared parcel, somehow ...
atomic_store_explicit(counter, count + 1, memory_order_release) ;
} ;
....
}
Операции получения нагрузки синхронизируются с операциями освобождения хранилища, поэтому:
в производителе: заполнение участка не начнется до тех пор, пока производитель не получит «владение» (как указано выше), а затем «завершится» (записи станут видимыми для другого потока) до счет обновляется (и новое значение становится видимым для другого потока).
у потребителя: опорожнение посылки не начнется, пока потребитель не получит «владение» (как указано выше) и затем «завершится» (все чтения будут считаны из памяти) до счетчик обновляется (и новое значение становится видимым для другого потока).
Очевидно, что два потока заняты, ожидая друг друга. Но с двумя или более посылками и счетчиками потоки могут развиваться со скоростью медленнее.
Наконец - x86 / x86_64 и получение / выпуск
С x86 / x86_64, все чтения и записи в память - это неявное чтение-чтение и запись-отпускание. Это означает, что накладные расходы равны нулю в atomic_load_explicit(..., memory_order_acquire)
и atomic_store_explicit(..., memory_order_release)
.
И наоборот, все операции чтения-изменения-записи (и операции memory_order_seq_cst) несут накладные расходы в течение нескольких десятков часов - 30 ?, 50 ?, больше, если операция разрешена (зависит от устройства).
Итак, где производительность имеет решающее значение, возможно, стоит понять, что возможно (а что нет).