Марк Митчелл - Программирование для Linux. Профессиональный подход
Во многих случаях поток выполняет код, который нельзя просто взять и прервать. Например, поток может выделить какие-то ресурсы, поработать с ними, а затем удалить. Если отмена потока произойдет где-то посередине, освободить занятые ресурсы станет невозможно, вследствие чего они окажутся потерянными для системы. Чтобы учесть эту ситуацию, поток должен решить, где и когда он может быть отменен.
С точки зрения возможности отмены поток находится в одном из трех состояний.
■ Асинхронно отменяемый. Такой поток можно отменить в любой точке его выполнения.
■ Синхронно отменяемый. Поток можно отменить, но не везде. Запрос на отмену помещается в очередь, и поток отменяется только по достижении определенной точки.
■ Неотменяемый. Попытки отменить поток игнорируются. Первоначально поток является синхронно отменяемым.
4.2.1. Синхронные и асинхронные потоки
Асинхронно отменяемый поток "свободен" в любое время. Синхронно отменяемый поток, наоборот, бывает "свободным", только когда ему "удобно". Соответствующие места в программе называются точками отмены. Запрос на отмену помещается в очередь и находится в ней до тех пор, пока поток не достигнет следующей точки отмены.
Чтобы сделать поток асинхронно отменяемым, воспользуйтесь функцией pthread_setcanceltype(). Эта функция влияет на тот поток, в котором она была вызвана. Первый ее аргумент должен быть PTHREAP_CANCEL_ASYNCHRONOUS в случае асинхронных потоков и PTHREAD_CANCEL_DEFERRED — в случае синхронных потоков. Второй аргумент — это указатель на переменную, в которую записывается предыдущее состояние потока.
Вот как можно сделать поток асинхронным:
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
Что такое точка отмены и где она должна находиться? На этот вопрос нельзя дать прямой ответ. Точка отмены создается с помощью функции pthread_testcancel(). Все, что она делает, — это обрабатывает отложенный запрос на отмену в синхронном потоке. Ее следует периодически вызывать в потоковой функции в ходе длительных вычислений, там, где поток можно завершить без риска потери ресурсов или других побочных эффектов.
Некоторые функции неявно создают точки отмены. О них можно узнать на man-странице, посвященной функции pthread_cancel(). Учтите, что они могут вызываться в других функциях, которые, тем самым, косвенно станут точками отмены.
4.2.2. Неотменяемые потоки
Поток может вообще отказаться удаляться, вызвав функцию pthread_setcancelstate(). Как и в случае функции pthread_setcanceltype(), это оказывает влияние только на вызывающий поток. Первый аргумент функции должен быть PTHREAD_CANCEL_DISABLE, если нужно запретить отмену потока, и PTHREAD_CANCEL_ENABLE в противном случае. Второй аргумент — это указатель на переменную, в которую записывается предыдущее состояние потока.
Вот как можно запретить отмену потока:
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
Функция pthread_setcancelstate() позволяет организовывать критические секции. Критической секцией называется участок программы, который должен быть либо выполнен целиком, либо вообще не выполнен. Другими словами, если поток входит в критическую секцию, он во что бы то ни стало должен дойти до ее конца.
Предположим, к примеру, что для банковской программы требуется написать функцию, осуществляющую перевод денег с одного счета на другой. Для этого нужно добавить заданную сумму на баланс одного счета и вычесть аналогичную сумму с баланса другого счета. Если между этими двумя операциями произойдет отмена потока, выполняющего функцию, программа ложно увеличит суммарный депозит банка вследствие незавершенной транзакции. Чтобы этого не случилось, обе операции должны выполняться в критической секции.
В листинге 4.6 показан пример функции process_transaction(), осуществляющей данную задумку. Функция запрещает отмену потока до тех пор, пока баланс обоих счетов не будет изменен.
Листинг 4.6. (critical_section.c) Защита банковской транзакции с помощью критической секции#include <pthread.h>
#include <stdio.h>
#include <string.h>
/* Массив балансов счетов, упорядоченный по номеру счета. */
float* account_balances;
/* перевод денежной суммы, равной параметру DOLLARS, со счета
FROM_ACCT на счет TO_ACCT. Возвращается 0, если транзакция
завершена успешно, или 1, если баланс счета FROM_ACCT
слишком мал. */
int process_transaction(int from_acct, int to_acct,
float dollars) {
int old_cancel_state;
/* Проверяем баланс на счету FROM_ACCT. */
if (account_balances(from_acct) < dollars)
return 1;
/* Начало критической секции. */
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancel_state);
/* переводим деньги. */
account_balances[to_acct] += dollars;
account_balances[from_acct] -= dollars;
/* Конец критической секции. */
pthread_setcancelstate(old_cancel_state, NULL);
return 0;
}
Обратите внимание на то, что по окончании критической секции восстанавливается предыдущее состояние потока, а не режим PTHREAD_CANCEL_ENABLE. Это позволит безопасно вызывать функцию process_transaction() из другой критической секции.
4.2.3. Когда необходимо отменять поток
В общем случае не рекомендуется отменять поток, если его можно просто завершить. Лучше всего каким-то образом просигнализировать потоку о том, что он должен прекратить работу, а затем дождаться его завершения. Подробнее о способах взаимодействия с потоками речь пойдет ниже в этой главе.
4.3. Потоковые данные
В отличие от процессов, все потоки программы делят общее адресное пространство. Это означает, что если один поток модифицирует ячейку памяти (например, глобальную переменную), то это изменение отразится на всех остальных потоках. Таким образом, потоки могут работать с одними и теми же данными, не используя механизмы межзадачного взаимодействия (рассматриваются в главе 5, "Взаимодействие процессов").
Тем не менее у каждого потока — свой собственный стек вызова. Это позволяет всем потокам выполнять разный код, а также вызывать функции традиционным способом. При каждом вызове функции в любом потоке создается отдельный набор локальных переменных, которые сохраняются в стеке этого потока.
Иногда все же требуется продублировать определенную переменную, чтобы у каждого потока была ее собственная копия. С этой целью операционная система Linux предоставляет потокам область потоковых данных. Переменные, сохраняемые в этой области, дублируются для каждого потока, что позволяет потокам свободно работать с ними, не мешая друг другу. Доступ к потоковым данным нельзя получить с помощью ссылок на обычные переменные, ведь у потоков общее адресное пространство. В Linux имеются специальные функции для чтения и записи значений, хранящихся в области потоковых данных.
Можно создать сколько угодно потоковых переменных, при этом все они должны иметь тип void*. Ссылка на каждую переменную осуществляется по ключу. Для создания нового ключа, т.е. новой переменной, предназначена функция pthread_key_create(). Первым ее аргументом является указатель на переменную типа pthread_key_t. В нее будет записано значение ключа, посредством которого любой поток сможет обращаться к своей копии данных. Второй аргумент — это указатель на функцию очистки ключа. Она будет автоматически вызываться при уничтожении потока; ей передается значение ключа, соответствующее данному потоку. Это очень удобно, так как функция очистки вызывается даже в случае отмены потока в произвольной точке. Если потоковая переменная равна NULL, функция очистки не вызывается. Если же такая функция не нужна, задайте в качестве второго параметра функции pthread_key_create() значение NULL.
После того как ключ создан, каждый поток может назначать ему собственное значение, вызывая функцию pthread_setspecific(). Ее первый аргумент — это ключ, а второй — требуемое значение типа void*. Для чтения потоковых переменных предназначена функция pthread_getspecific(), единственным аргументом которой является ключ.
Предположим, имеется приложение, распределяющее задачу между несколькими потоками. В целях аудита за каждым потоком закреплен отдельный журнальный файл, куда записываются сообщения о ходе выполнения поставленной задачи. Область потоковых данных — удобное место для хранения указателя на журнальный файл каждого потока.
В листинге 4.7 показано, как осуществить задуманное. Для хранения файлового указателя в функции main() создается ключ, запоминаемый в переменной thread_log_key. Эта переменная является глобальной, поэтому она доступна всем потокам. Когда поток начинает выполнять свою потоковую функцию, он открывает журнальный файл и сохраняет указатель на него в своем ключе. Позднее любой поток может вызвать функцию write_to_thread_log(), чтобы записать сообщение в свой журнальный файл. Эта функция извлекает из области потоковых данных указатель на журнальный файл и помещает в файл требуемое сообщение.