QNX/UNIX: Анатомия параллелизма
QNX/UNIX: Анатомия параллелизма читать книгу онлайн
Книга адресована программистам, работающим в самых разнообразных ОС UNIX. Авторы предлагают шире взглянуть на возможности параллельной организации вычислительного процесса в традиционном программировании. Особый акцент делается на потоках (threads), а именно на тех возможностях и сложностях, которые были привнесены в технику параллельных вычислений этой относительно новой парадигмой программирования. На примерах реальных кодов показываются приемы и преимущества параллельной организации вычислительного процесса. Некоторые из результатов испытаний тестовых примеров будут большим сюрпризом даже для самых бывалых программистов. Тем не менее излагаемые техники вполне доступны и начинающим программистам: для изучения материала требуется базовое знание языка программирования C/C++ и некоторое понимание «устройства» современных многозадачных ОС UNIX.
В качестве «испытательной площадки» для тестовых фрагментов выбрана ОСРВ QNX, что позволило с единой точки зрения взглянуть как на специфические механизмы микроядерной архитектуры QNX, так и на универсальные механизмы POSIX. В этом качестве книга может быть интересна и тем, кто не использует (и не планирует никогда использовать) ОС QNX: программистам в Linux, FreeBSD, NetBSD, Solaris и других традиционных ОС UNIX.
Внимание! Книга может содержать контент только для совершеннолетних. Для несовершеннолетних чтение данного контента СТРОГО ЗАПРЕЩЕНО! Если в книге присутствует наличие пропаганды ЛГБТ и другого, запрещенного контента - просьба написать на почту [email protected] для удаления материала
friend bool operator==(const X& f, const X& s) { ... }
// оператор присваивания мы не переопределяем, используется
// присваивание по умолчанию - побайтовое копирование
};
...
X A;
...
X B(А); // потенциальная ошибка
...
B = A; // потенциальная ошибка
if (А == В) { ... } // потенциальная ошибка
Обратите внимание, что все объекты данных, для которых могут наблюдаться обсуждаемые эффекты, должны быть доступны вне потока, то есть быть глобальными с точки зрения видимости в потоке.
Именно для безопасного манипулирования данными в параллельной среде QNX API и вводятся атомарные операции. Десять атомарных функций делятся на две симметричные группы по виду своего именования и логике функционирования. Все атомарные операции осуществляются только над одним типом данных
unsigned int
unsigned int
volatile
Помимо атомарных операций над этой переменной могут выполняться любые другие действия, которые можно считать безопасными в многопоточной среде: инициализация, присваивание значений, сравнения. Более того, при выходе программы за область возможного многопоточного доступа к этой переменной она может далее использоваться любым традиционным и привычным образом.
Важно также отметить, что термин «атомарность» относится не к особым свойствам некоторого объекта данных, а к ограниченному ряду операций, которые можно безопасно выполнять над этим объектом в многопоточной среде.
Общий вид прототипов каждой из двух групп атомарных операций следующий:
void atomic_*(volatile unsigned *D, unsigned S);
unsigned atomic_*_value(volatile unsigned *D, unsigned S);
где вместо
*
add
sub
clr
*D) &= ~S
set
*D) |= S
toggle
*D) ^= S
D
S
Две формы атомарных функций для каждой операции отличаются тем, что первая из них выполняет операцию без возврата значения, а вторая возвращает значение, которое операнд
D
++D
--D
D++
D--
Зачем нужны две формы для операции? Техническая документация QNX утверждает, что вторая форма может выполняться дольше. Справедливость этого утверждения и насколько дольше выполняется вторая форма, мы скоро увидим на примерах.
Итак, у нас есть 10 функций для выполнения пяти атомарных операций:
atomic_add() atomic_add_value()
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);
Или даже так:
// глобальные описания, доступные всем потокам
...
// <b>указатель</b>текущей позиции записи:
volatile unsigned int ind = (unsigned int)buf;
...
// выполняется в каждом из потоков: