QNX/UNIX: Анатомия параллелизма
QNX/UNIX: Анатомия параллелизма читать книгу онлайн
Книга адресована программистам, работающим в самых разнообразных ОС UNIX. Авторы предлагают шире взглянуть на возможности параллельной организации вычислительного процесса в традиционном программировании. Особый акцент делается на потоках (threads), а именно на тех возможностях и сложностях, которые были привнесены в технику параллельных вычислений этой относительно новой парадигмой программирования. На примерах реальных кодов показываются приемы и преимущества параллельной организации вычислительного процесса. Некоторые из результатов испытаний тестовых примеров будут большим сюрпризом даже для самых бывалых программистов. Тем не менее излагаемые техники вполне доступны и начинающим программистам: для изучения материала требуется базовое знание языка программирования C/C++ и некоторое понимание «устройства» современных многозадачных ОС UNIX.
В качестве «испытательной площадки» для тестовых фрагментов выбрана ОСРВ QNX, что позволило с единой точки зрения взглянуть как на специфические механизмы микроядерной архитектуры QNX, так и на универсальные механизмы POSIX. В этом качестве книга может быть интересна и тем, кто не использует (и не планирует никогда использовать) ОС QNX: программистам в Linux, FreeBSD, NetBSD, Solaris и других традиционных ОС UNIX.
Внимание! Книга может содержать контент только для совершеннолетних. Для несовершеннолетних чтение данного контента СТРОГО ЗАПРЕЩЕНО! Если в книге присутствует наличие пропаганды ЛГБТ и другого, запрещенного контента - просьба написать на почту [email protected] для удаления материала
int main() {
// переопределение реакции ^C в старой манере
signal(SIGINT, endhandler);
// маска блокирования-разблокирования
sigemptyset(&sig);
sigaddset(&sig, SIGNUM);
// блокировка в главном потоке приложения
sigprocmask(SIG_BLOCK, &sig, NULL);
cout << "Process " << getpid() << ", waiting for signal " << SIGNUM << endl;
// установка обработчика (для дочерних потоков)
struct sigaction act;
act.sa_mask = sig;
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
if (sigaction(SIGNUM, &act, NULL) < 0) perror("set signal handler: ");
const int thrnum = 3;
for (int i = 0; i < thrnum; i++) {
threcord threc = { 0, false };
pthread_create(&threc.tid, NULL, threadfunc, (void*)i);
tharray.push_back(three);
}
pause();
// сюда мы попадаем после ^C для завершающих операций...
tharray.erase(tharray.begin(), tharray.end());
cout << "Clean vector" << endl;
}
Это приложение, в отличие от предыдущих, построено уже с использованием специфики С++, в нем используется контейнерный класс
vector
Показанное приложение в значительной степени искусственно и неэффективно. Мы приводим его здесь не как образец того, «как нужно делать», а только как иллюстрацию гибкости возможностей, предоставляемых в области параллельного программирования. При некоторой изобретательности можно заставить программу вести себя согласно вашим капризам, какими бы изощренными они ни оказались.
Запускаем полученное приложение:
# s10
Process 2089006, waiting for signal 41
После чего с другого терминала пошлем приложению ожидаемый им сигнал, например командой:
# kill -41 2089006
Посылаем этот сигнал несколько раз (в данном случае 3) и получаем вывод от приложения:
SIG = 41; TID = 4
SIG = 41; TID = 2
SIG = 41; TID = 3
SIG = 41; TID = 3
SIG = 41; TID = 4
SIG = 41; TID = 2
SIG = 41; TID = 2
SIG = 41; TID = 3
SIG = 41; TID = 4
^C
Clean vector
Видно, что реакция на каждый сигнал возбуждается несколько раз (по числу потоков), каждый раз выполняясь в контексте разного потока (TID). Интересно и изменение порядка активизации потоков от сигнала к сигналу, то есть потоки в очереди ожидающих «перетасовываются» при поступлении каждого сигнала.
В приложение добавлена реакция на ^C (сигнал
SIGINT
• начиная с некоторой сложности приложений, их завершению должна обязательно предшествовать некоторая последовательность действий; в данном случае мы условно показываем очистку вектора состояний потоков;
• реакция на
SIGINT
SIGRTMIN
Как мы уже видели, тот факт, что обработчик сигнала выполняется в контексте потока, который разблокировал реакцию на этот сигнал (независимо от того, в момент выполнения какого потока приходит сигнал), позволяет реализовать в обработчике сигнала обработку любой сложности в интересах этого потока. Для этого лишь требуется разместить все области данных, запрашиваемые в этой обработке, не в стеке потока (объявленные как локальные переменные потоковой функции), а в области собственных данных потока, которые мы детально рассмотрели ранее. Схематично это можно показать в коде так:
• Положим, нам нужно уведомлять о некоторых событиях N потоков.
Будем использовать для этого сигналы
SIGRTMIN
SIGRTMIN + (N - 1)
for (int i = SIGRTMIN, i < SIGRTMIN + N; i++) {
pthread_create(NULL, NULL, threadfunc, (void*)(i));
}
• При запуске
N
class DataBlock {
~DataBlock(void) {...}
};
static pthread_key_t key;
static pthread_once_t once = PTHREAD_ONCE_INIT;
static void destructor(void* db) { delete (DataBlock*)db; }
static void once_creator(void) {
pthread_key_create(&key, destructor);
}
void* threadfunc(void* data) {
// надлежащим образом маскируем сигналы
// ...
// это произойдет только в первом потоке из N