Цилюрик Олег Владимирович - QNX/UNIX: Анатомия параллелизма стр 18.

Шрифт
Фон

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) отчетливо ориентирована на семантику классического С, в то время как все свое изложение мы ориентируем и иллюстрируем на С++. При создании экземпляра собственных данных полностью разрушается контроль типизации: разные экземпляры потоков вполне могли бы присвоить своим указателям данные (типа void*), ассоциированные с одним значением 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<throwndata*> 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. И то и другое верно, ибо компилятор достаточно умен, чтобы при указании имени функции взять ее адрес.

Собственные данные потоков - это настолько гибкий механизм, что он может таить в себе и другие, еще не используемые техники применения.

Безопасность вызовов в потоковой среде

Рассмотрев "в первом приближении" технику собственных данных потоков, мы теперь готовы ответить на вопрос: "В чем же главное предназначение такой в общем-то достаточно громоздкой техники? И зачем для ее введения потребовалось специально расширять стандарты POSIX?" Самое прямое ее предназначение, помимо других "попутных" применений, которые были обсуждены ранее, - это общий механизм превращения существующей функции для однопотокового исполнения в функцию, безопасную (thread safe) в многопоточном окружении. Этот механизм предлагает единую (в смысле "единообразную", а не "единственно возможную") технологию для разработчиков библиотечных модулей.

Примечание

Ваша оценка очень важна

0
Шрифт
Фон

Помогите Вашим друзьям узнать о библиотеке