Достаточно часто встречающийся на практике образец многопоточного кода — это циклическая процедура ожидания наступления некоторого условия (события), после которого порождается новый экземпляр потока, призванный обслужить наступившее событие (типичная схема всего разнообразия многопоточных сетевых серверов). В таких случаях код, порождающий потоки, выглядит подобно следующему фрагменту:
// функция потока:
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
специально сделанной ранее временной копии экземпляра данных, например:
), не превышающего размер указателя, очень часто в самых разнообразных источниках [1, 3] можно увидеть такой трюк, когда указателю присваивается непосредственное значение скалярной величины:
// функция потока:
void* ThreadProc(void* data) {
// ... выполняется обработка, используя значение параметра (char)data