• выполняются все процедуры завершения, занесенные ранее в стек завершения вызовами pthread_cleanup_push();
• выполняются деструкторы собственных данных потока;
• отменяемый поток завершается;
• процесс отмены — асинхронный с точки зрения вызывающего pthread_cancel() кода, поэтому вызывающий отмену поток должен дождаться завершения потока на вызове pthread_join().
Прежде всего, поток может вообще отказаться выполнять любые отмены, вызвав из своей функции потока:
int pthread_setcancelstate(int state, int* oldstate);
где state и oldstate — устанавливаемое и установленное ранее (возвращаемое вызовом) состояния отмены потока, которые могут принимать значения PTHREAD_CANCEL_DISABLE либо PTHREAD_CANCEL_ENABLE. (Естественно, как и во многих функциях с подобным прототипом, значением oldstate может быть NULL, и тогда нам не нужно возвращать ранее установленное состояние.)
Далее, даже если для потока установлено состояние завершаемости (также называемое «состоянием отмены») PTHREAD_CANCEL_ENABLE (это значение по умолчанию при создании потока), поток может переопределить еще и тип отмены, вызвав:
int pthread_setcanceltype(int type, int* oldtype);
где type и oldtype — как и в предыдущем случае, новое и ранее установленное значения типа отмены потока, которые могут принимать значения PTHREAD_CANCEL_ASYNCHRONOUS (асинхронный по отмене поток) либо PTHREAD_CANCEL_DEFERRED (синхронный по отмене поток). Значением по умолчанию, устанавливаемым при создании потока, является PTHREAD_CANCEL_DEFERRED, хотя предписываемым POSIX умолчанием является PTHREAD_CANCEL_ASYNCHRONOUS.
Обе рассмотренные функции установок[23] параметров отмены при успешном выполнении возвращают значение EOK.
Итак, действия потока на запрос его завершения будут определяться текущей комбинацией двух установленных для него параметров: состоянием и типом отмены.
Теперь о том, чем же отличается отмена асинхронно и синхронно завершаемых потоков. Поток с асинхронным типом отмены (установленный с PTHREAD_CANCEL_ASYNCHRONOUS) может быть отменен в любой произвольный момент времени, то есть он всегда «свободен» для отмены и отмена производится немедленно. Поток с синхронным типом отмены (установленный с PTHREAD_CANCEL_DEFERRED) может быть остановлен только в тех точках выполнения потока, когда ему «удобно», и соответствующие места в программе называются точками отмены. При поступлении запроса на отмену такого потока (после выполнения извне pthread_cancel()) запрос помещается в очередь, а процесс отмены активизируется только после того, как отменяемый поток в ходе своего выполнения достигнет очередной точки отмены. Как определяются (создаются) точки отмены в коде потока? Для этого служит функция:
void pthread_testcancel(void);
Каждый вызов pthread_testcancel() тестирует очередь поступивших запросов на отмену на предмет наличия запросов, и если таковой запрос есть, процесс отмены активизируется. Если в коде отсутствуют вызовы pthread_testcancel(), то в нем практически отсутствуют точки отмены и поток становится неотменяемым (подобно установке его состояния отмены в PTHREAD_CANCEL_DISABLE). Поэтому при выполнении длительных вычислений функцию pthread_testcancel() следует периодически вызывать в потоковой функции в тех точках, где потенциальная отмена потока не опасна.
Примечание
(Очень важно!) Достаточно много библиотечных функций могут сами устанавливать точки отмены. Более того, такие функции могут косвенно вызываться из других функций в программе и тем самым неявно устанавливать точки отмены. Информацию о таких функциях следует искать в справочной man-странице по функции pthread_testcancel(). В результате этого эффекта можно получить отмену потока не в той точке, которую вы считаете безопасной и которую явно отмечаете вызовом pthread_testcancel(), а ранее этой точки — когда будет вызвана одна из таких функций. А это, очевидно, вовсе не то, на что вы рассчитывали!
Если состояние отмены потока, как это описывалось ранее, установлено в PTHREAD_CANCEL_DISABLE, то никакая расстановка точек отмены не имеет эффекта и поток остается неотменяемым.
Покажем, как могут быть использованы все эти предосторожности в коде функции потока, чтобы сделать код безопасным с позиции возможной асинхронной отмены потока извне:
void* function(void* data) {
int state;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &state);
// ... здесь выполняется инициализация ...
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
pthread_setcancelstate(&state, NULL);
while (true) {
struct blockdata *blk = new blockdata;
// ... обработка блока данных blk ...
delete blk;
pthread_testcancel();
}
}
...
pthread_t tid;
...
pthread_create(&tid, NULL, function, NULL);
...
pthread_cancel(tid); // отмена потока
void* res;
pthread_join(tid, &res); // ожидание отмены
if (res != PTHREAD_CANCELED)
cout << "Что-то не так!" << endl;
Наконец, в QNX (но не в POSIX) существует вызов, подобный pthread_cancel(), принудительно отменяющий поток независимо от его установок («желания»):
int pthread_abort(pthread_t thread);
В отличие от pthread_cancel(), этот вызов принудительно и немедленно отменяет поток. Кроме того, никакие процедуры завершения и деструкторы собственных данных потока не выполняются. Очевидно, что в результате такого «завершения» состояния объектов процесса будут просто неопределенными, поэтому такой вызов крайне опасен. При таком способе отмены в программный код, ожидающий завершения на pthread_join(), в качестве результата завершения возвращается константа (тип void*) PTHREAD_ABORTED (аналогично возвращается константа PTHREAD_CANCELED при выполнении pthread_cancel()).
Но и этих мер безопасности недостаточно на все случаи жизни, поэтому механизм потоков предусматривает еще один уровень (механизм) страховки.
Для поддержания корректности состояния объектов процесса каждый поток может помещать (добавлять) в стек процедур завершения (thread's cancellation-cleanup stack) функции, которые при завершении (pthread_exit() или return) или отмене (по pthread_cancel()) выполняются в порядке, обратном помещению. Для манипуляции со стеком процедур завершения предоставляются вызовы (оба вызова реализуются макроопределениями, но это не суть важно[24]):
void pthread_cleanup_push(void (routine)(void*), void* arg);
где routine — адрес функции завершения, помещаемой в стек; arg — указатель блока данных, который будет передан routine при ее вызове.
Функции завершения (начиная с вершины стека) вызываются со своими блоками данных в случаях, когда:
• поток завершается, выполняя pthread_exit();
• активизируется действие отмены потока, ранее запрошенное по вызову pthread_cancel();
• выполняется второй (комплементарный к pthread_cleanup_push()) вызов с ненулевым значением аргумента:
void pthread_cleanup_pop(int execute);
Этот вызов выталкивает из стека последнюю помещенную туда pthread_cleanup_push() функцию завершения и, если значение execute ненулевое, выполняет ее.
Вот как может выглядеть в этой технике безопасный (с позиции возможной асинхронной отмены потока) захват мьютекса:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void cleanup(void* arg) { pthread_mutex_unlock(&mutex); }
void* thread_function(void* arg) {
while (true) {
pthread_mutex_lock(&mutex);
pthread_cleanup_push(&cleanup, NULL);
{
// все точки отмены должны быть расставлены в этом блоке!
}
pthread_testcancel();
pthread_cleanup_pop(1);
}
}
Вот теперь, завершив краткий экскурс использования процессов и потоков, можно вернуться к вопросу, который вскользь уже звучал по ходу рассмотрения: почему и в каком смысле потоки часто называют «легкими процессами» (LWP — lightweight process)?
Выполним ряд тестов по сравнительной оценке временных затрат на создание процесса и потока. Начнем с процесса (файл p2-1.cc):
Затраты на порождение нового процесса
struct mbyte { // мегабайтный блок данных
#pragma pack(1)
uint8_t data[1024 * 1024];
#pragma pack(4)
};
int main(int argc, char *argv[]) {
mbyte *blk = NULL;
if (argc > 1 && atoi(argv[1]) > 0) {
blk = new mbyte[atoi(argv[1])];
}
uint64_t t = ClockCycles();
pid_t pid = fork();
if (pid == -1) perror("fork"), exit(EXIT_FAILURE);
if (pid == 0) exit(EXIT_SUCCESS);