Олег Цилюрик - QNX/UNIX: Анатомия параллелизма
- Название:QNX/UNIX: Анатомия параллелизма
- Автор:
- Жанр:
- Издательство:Символ-Плюс
- Год:2006
- Город:Санкт-Петербург
- ISBN:5-93286-088-Х
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
Олег Цилюрик - QNX/UNIX: Анатомия параллелизма краткое содержание
Книга адресована программистам, работающим в самых разнообразных ОС UNIX. Авторы предлагают шире взглянуть на возможности параллельной организации вычислительного процесса в традиционном программировании. Особый акцент делается на потоках (threads), а именно на тех возможностях и сложностях, которые были привнесены в технику параллельных вычислений этой относительно новой парадигмой программирования. На примерах реальных кодов показываются приемы и преимущества параллельной организации вычислительного процесса. Некоторые из результатов испытаний тестовых примеров будут большим сюрпризом даже для самых бывалых программистов. Тем не менее излагаемые техники вполне доступны и начинающим программистам: для изучения материала требуется базовое знание языка программирования C/C++ и некоторое понимание «устройства» современных многозадачных ОС UNIX.
В качестве «испытательной площадки» для тестовых фрагментов выбрана ОСРВ QNX, что позволило с единой точки зрения взглянуть как на специфические механизмы микроядерной архитектуры QNX, так и на универсальные механизмы POSIX. В этом качестве книга может быть интересна и тем, кто не использует (и не планирует никогда использовать) ОС QNX: программистам в Linux, FreeBSD, NetBSD, Solaris и других традиционных ОС UNIX.
QNX/UNIX: Анатомия параллелизма - читать онлайн бесплатно ознакомительный отрывок
Интервал:
Закладка:
2. Неприятность здесь состоит в том, что запросить значение ключа для DataBlock
должен только первый пришедший к этому месту поток (когда ключ еще не распределен). Последующие потоки, достигшие этого места, должны только воспользоваться ранее распределенным значением ключа для типа DataBlock
. Для разрешения этой сложности в систему функций собственных данных введена функция pthread_once()
.
3. После этого каждый поток (как создавший ключ, так и использующий его) должен запросить по pthread_getspecific()
адрес блока данных и, убедившись, что это NULL
, динамически распределить область памяти для своего экземпляра данных, а также зафиксировать по pthread_setspecific()
этот адрес в массиве экземпляров для дальнейшего использования.
4. Дальше поток может работать с собственным экземпляром данных (отдельный экземпляр на каждый поток), используя для доступа к нему pthread_getspecific()
.
5. При завершении любого потока система уничтожит и его экземпляр данных, вызвав для него деструктор, который был установлен вызовом pthread_key_create()
, единым для всех экземпляров данных, ассоциированных с этим значением ключа.
Теперь запишем это в коде, заодно трансформировав в новую функцию ThreadProc()
код ранее созданной версии этой же функции SingleProc()
для исполнения в одном потоке, не являющийся реентерабельным и безопасным в многопоточной среде. (О вопросах реентерабельности мы обязательно поговорим позже.)
void* SingleProc(void *data) {
static DataBlock db( ... );
// ... операции с полями DataBlock
return NULL;
}
To, что типы параметров и возвращаемое значение SingleProc()
«подогнаны» под синтаксис ее более позднего эквивалента ThreadProc()
, не является принципиальным ограничением - входную и выходную трансформации форматов данных реально осуществляют именно в многопоточном эквиваленте. Нам здесь важно принципиально рассмотреть общую формальную технику трансформации нереентерабельного кода в реентерабельный.
Далее следует код SingleProc()
, преобразованный в многопоточный вид:
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) {
// создается единый на процесс ключ для данных DataBlock:
pthread_key_create(&key, destructor);
}
void* ThreadProc(void *data) {
// гарантия того, что ключ инициализируется только 1 раз на процесс!
pthread_once(&once, once_creator);
if (pthread_getspecific(key) == NULL)
pthread_setspecific(key, new DataBlock(...));
// Теперь каждый раз в теле этой функции или функций, вызываемых
// из нее, мы всегда можем получить доступ к экземпляру данных
DataBlock* pdb = pthread_getspecific(key);
// ... все те же операции с полями pdb->(DataBlock)
return NULL;
}
Обратите внимание, что вся описанная техника преобразования потоковых функций в реентерабельные (как и все программные интерфейсы POSIX) отчетливо ориентирована на семантику классического С, в то время как все свое изложение мы ориентируем и иллюстрируем на С++. При создании экземпляра собственных данных полностью разрушается контроль типизации: разные экземпляры потоков вполне могли бы присвоить своим указателям данные (типа v oid*
), ассоциированные с одним значением key
. Это совершенно различные типы данных, скажем DataBlock_1*
и DataBlock_2*
. Но проявилось бы это несоответствие только при завершении функции потока и уничтожении экземпляров данных, когда к объектам совершенно разного типа был бы применен один деструктор, определенный при выделении ключа. Ошибки такого рода крайне сложны в локализации.
Особая область, в которой собственные данные потока могут найти применение и где локальные (стековые) переменные потока не могут быть использованы, — это асинхронное выполнение фрагмента кода в контексте потока, например при получении потоком сигнала.
Еще одно совсем не очевидное применение собственных данных потока (мы не встречали в литературе упоминаний о нем), которое особо органично вписывается в использование именно С++, — это еще один способ возврата в родительский поток результатов работы дочерних. При этом неважно, как были определены дочерние потоки - как присоединенные или как отсоединенные (мы обсуждали это ранее); такое использование в заметной мере нивелирует их разницу. Эта техника состоит в том, что:
• Если при создании ключа не определять деструктор экземпляра данных потока pthread_key_create(..., NULL)
, то при завершении потока над экземпляром его данных не будут выполняться никакие деструктивные действия и созданные потоками экземпляры данных будут существовать и после завершения потоков.
• Если к этим экземплярам данных созданы альтернативные пути доступа (а они должны быть в любом случае созданы, так как области этих данных в конечном итоге нужно освободить), то благодаря этому доступу порождающий потоки код может использовать данные, «оставшиеся» как результат выполнения потоков.
В коде (что гораздо нагляднее) это может выглядеть так (код с заметными упрощениями взят из реального завершенного проекта):
// описание экземпляра данных потока
struct throwndata {
...
};
static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t key;
void createkey(void) { pthread_key_create(&key, NULL); }
// STL-очередь, например указателей на экземпляры данных
queue result;
// функция потока
void* GetBlock(void*) {
pthread_once(&once, createkey);
throwndata *td;
if ((td = (throwndata*)pthread_getspecific(key)) == NULL) {
td = new throwndata();
pthread_setspecific(key, (void*)td);
// вот он - альтернативный путь доступа:
result.push(td);
}
// далее идет плодотворная работа над блоком данных *td
// . . . . . . . . .
}
int main(int argc, char **argv) {
// . . . . . .
for (int i = 0; i < N; i++)
pthread_create(NULL, NULL, GetBlock, NULL);
// . . . . . . к этому времени потоки завершились;
// ни в коем случае нельзя помещать result.size()
// непосредственно в параметр цикла!
int n = result.size();
for (int i = 0; i < n; i++) {
throwndata *d = result.front();
// обработка очередного блока *d ...
result pop();
delete d;
}
return EXIT_SUCCESS;
}
В предыдущих примерах кода мы указывали третий параметр pthread_create()
в виде &GetBlock
(адреса функции потока), но в текущем примере мы сознательно записали GetBlock
. И то и другое верно, ибо компилятор достаточно умен, чтобы при указании имени функции взять ее адрес.
Собственные данные потоков — это настолько гибкий механизм, что он может таить в себе и другие, еще не используемые техники применения.
Читать дальшеИнтервал:
Закладка: