Чтение онлайн

ЖАНРЫ

QNX/UNIX: Анатомия параллелизма
Шрифт:

Заметим здесь вскользь (в дальнейшем нам представится возможность использовать эти знания), что помимо «продуктивных» потоков (компонент системы и пользовательских приложений) в системе всегда существует один «паразитный» поток, запущенный с приоритетом 0 (idle-поток). Он «выбирает» весь остаток процессорного времени в те периоды, когда все имеющиеся в системе продуктивные потоки перейдут в блокированные состояния (ожидания). Подобная практика хорошо известна и реализуется также в большинстве других операционных систем.

Отличия от POSIX

Если следовать POSIX-стандарту, то некоторые из атрибутов невозможно переопределить до фактического создания этого стандарта (их

можно изменить позже в самом коде потока, но иногда это не совсем правильное решение). Все эти возможности относятся к асинхронному завершению потока; детали функционирования этого механизма рассматриваются позже. К подобного рода атрибутам относятся:

• запретить асинхронное завершение (отмену) потока;

• установить тип завершаемости потока;

• определить, что должно происходить при доставке потоку сигналов.

QNX расширяет возможности POSIX, позволяя по условию OR установить соответствующие биты-флаги в поле

flags
атрибутной записи, прежде чем будет произведен вызов, создающий поток. Не существует функций вида
pthread_attr_set_*
, эквивалентных этим установкам. К этим флагам относятся:

PTHREAD_CANCEL_ENABLE
— запрос на завершение будет обрабатываться в соответствии с типом завершаемости, установленным для потока (значение по умолчанию);

PTHREAD_CANCEL_DISABLE
— запросы на завершение будут отложены;

PTHREAD_CANCEL_ASYNCHRONOUS
— если завершение разрешено, отложенные или текущие запросы будут выполнены немедленно;

PTHREAD_CANCEL_DEFERRED
— если завершение разрешено, запросы на завершение будут отложены до достижения точки завершаемости (значение по умолчанию);

PTHREAD_MULTISIG_ALLOW
— завершать по сигналу все потоки в процессе (POSIX-умолчание);

PTHREAD_MULTISIG_DISALLOW
— завершать по сигналу только тот поток, который принял сигнал.

После запуска потока все атрибуты, связанные с завершаемостью потока, могут быть изменены вызовами

pthread_setcancelstate
и
pthread_setcanceltype
.

Передача параметров потоку

Зачастую каждый поток из группы последовательно создаваемых потоков, выполняющих одну и ту же функцию, нужно запускать со своим индивидуальным блоком данных (параметром потока). Для этого предназначен 4-й параметр вызова

pthread_create
— указатель на блок данных типа
void*
. Характерно, что это может быть произвольная структура данных сколь угодно сложного типа, структуризацию которой вызывающий
pthread_create
код и функция потока должны понимать единообразно; никакого контроля соответствия типов на этапе вызова не производится.

Достаточно часто встречающийся на практике образец многопоточного кода — это циклическая процедура ожидания наступления некоторого условия (события), после которого порождается новый экземпляр потока, призванный обслужить наступившее событие (типичная схема всего разнообразия многопоточных сетевых серверов). В таких случаях код, порождающий потоки, выглядит подобно следующему фрагменту:

// функция потока:

void* ThreadProc(void* data) {

// ... выполняется обработка, используя структуру *(DataParam*)data

return NULL;

}

// порождающий
потоки код:

while (true) {

// инициализация области параметров

struct DataParam data(...);

if ( /* ожидаем нечто */ )

pthread_create(NULL, &attr, &ThreadProc, &data);

}

Этот простейший код крайне опасен: при быстрых циклах и, что намного важнее, непредсказуемых моментах повторных созданий экземпляров потоков из вызывающего цикла необходимо обеспечить, чтобы используемое в функции потока

ThreadProc
значение данных было адекватным. Оно может быть изменено в вызывающем коде или даже, более того, просто разрушено при выходе из локальной области видимости, как в следующем коде:

// порождающий потоки код:

while(true) {

if ( /* ожидаем нечто */ ) {

struct DataParam data(...);

pthread_create(NULL, &attr, &ThreadProc, &data);

}

// здесь может идти достаточно длительная обработка

}

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

Единственно надежный способ обеспечить требование актуальности передаваемых данных - это создание копии блока параметров непосредственно при входе в функцию потока, например так (если определена операция копирования):

// функция потока:

void* ThreadProc(void* data) {

DataParam copy = *(DataParam*)data;

// выполняется обработка, используя структуру copy

return NULL;

}

или так (если определен инициализирующий конструктор структуры данных):

// функция потока:

void* ThreadProc(void* data) {

DataParam copy(*(DataParam*)data);

// ... выполняется обработка, используя структуру copy

return NULL;

}

Но и этот код оказывается некорректен. При порождении потока нам нужно, чтобы инициализация копии переданных данных в теле функции потока произошла до того, как на очередном цикле оригинал этих данных будет разрушен или изменен. Но дисциплины диспетчеризации равнозначных потоков (в данном случае родительского и порожденного) в операционной системе никак не регламентируют (и не имеют права этого делать!) порядок их выполнения после точки ветвления —

pthread_create
.

Обеспечить актуальность копии переданных данных можно несколькими искусственными способами:

1. Передачей в качестве аргумента

pthread_create
специально сделанной ранее временной копии экземпляра данных, например:

if ( /* нечто */ ) {

// static обеспечивает неразрушаемость

static struct DataParam copy;

copy = data;

pthread_create(NULL, &attr, &ThreadProc, &copy);

Поделиться с друзьями: