atomic_sub() atomic_sub_value()
atomic_clr() atomic_clr_value()
atomic_set() atomic_set_value()
atomic_toggle() atomic_toggle_value()
Как используются атомарные операции? Обычно для предотвращения одновременного изменения некоторого счетчика индекса мы вынуждены создавать критическую секцию, обозначая ее, скажем, операциями над мьютексом. В частности, в следующем примере нам необходимо из различных потоков последовательно дописывать некоторые байтовые результаты в единый буфер:
// глобальные описания, доступные всем потокам
const unsigned int N = ...
uint8_t buf[N];
// индекс текущей позиции записи
unsigned int ind = 0;
// общий мьютекс, доступный каждому из потоков
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
...
// выполняется в каждом из потоков:
uint8_t res[M]; // результат некоторой операции
unsigned int how = ... // реальная длина этого результата
pthread_mutex_lock(&mutex);
memcpy((void*)buf + ind, (void*)res, how);
ind += how;
pthread_mutex_unlock(&mutex);
Используя атомарные операции, мы можем этот процесс записать так (все глобальные описания остаются неизменными):
// глобальные описания, доступные всем потокам
...
// индекс текущей позиции записи
volatile unsigned int ind = 0;
...
// выполняется в каждом из потоков:
uint8_t res[M]; // результат некоторой операции
unsigned int how = ... // реальная длина этого результата
memcpy((void*)buf + atomic_add_value(ind, how), (void*)res, how);
Или даже так:
// глобальные описания, доступные всем потокам
...
// указательтекущей позиции записи:
volatile unsigned int ind = (unsigned int)buf;
...
// выполняется в каждом из потоков:
memcpy((void*)atomic_add_value(ind, how), (void*)res, how);
В последнем случае это, конечно, трюкачество, построенное на том, что в 32-разрядной архитектуре представления указателя и переменной unsigned int
совпадают, но это «работающее трюкачество» и работающее иногда весьма эффективно.
Техника применения атомарных операций оказывается крайне удобной, например, при осуществлении вывода, часто диагностического, из различных потоков. Положим, нам нужно в каждом из многих потоков выводить диагностическую строку при достижении ими определенной точки исполнения:
cout << "Это вывод потока " << pthread_self() << endl;
Но так делать нельзя: при таком решении у нас появляются 2 проблемы, одна из которых очевидна, а другая — не очень:
1. Выводы разных потоков могут «смешиваться»; более того, за счет буферизации вывода некоторые «рваные» фрагменты мы будем наблюдать дважды. Одним словом, наш вывод окажется полностью «нечитабельным».
2. При осуществлении вывода в контексте потока почти наверняка в процессе вывода будут выполняться системные вызовы, которые потребуют новой диспетчеризации и приведут к вытеснению исходного потока. При этом порядок передачи управления от потока к потоку при наличии отладочного вывода будет отличаться от порядка при его отсутствии. А это, наверное, не совсем то, что мы ожидали. В результате при наличии отладочного вывода мы можем наблюдать совсем не ту картину, для изучения которой этот вывод, собственно, и предназначен.
Эти эффекты не связаны с какой-то конкретной формой вывода, такой как вывод в поток, показанный выше; точно так же будет вести себя и традиционный вызов printf(...)
.
Проблема очень просто решается, если вместо непосредственного вывода из потоков последовательно сбрасывать все сообщения в промежуточный буфер, который будет выводиться в те периоды времени программы, когда запись в него не производится:
const int N = ... , M = ...;
char buf[N];
volatile unsigned ind = 0;
...
// а вот это производится из каждого потока
char tbuf[M];
sprintf(tbuf, "Это вывод потока %X", pthread_self());
strcpy(buf + atomic_add_value(ind, strlen(tbuf)), tbuf);
И наконец, последнее: есть ли смысл во введении этого дополнительного механизма, если всегда существует альтернативная форма организации такой же защиты доступа посредством критической секции (например, при использовании мьютекса)? Сравним ( файл a1.cc ) временные затраты при многократном изменении значения переменной для случаев атомарных операций и критической секции на базе мьютекса (мы берем именно мьютекс, потому что из всех примитивов синхронизации он самый низкоуровневый и быстрый):
Сравнение мьютекса и двух форм вызова атомарной операции
#include
Читать дальше
Конец ознакомительного отрывка
Купить книгу