В отличие от
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)?
Выполним ряд тестов по сравнительной оценке временных затрат на создание процесса и потока. Начнем с процесса (
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);
if (pid > 0) {
waitpid(pid, NULL, WEXITED);
t = ClockCycles - t;
}
if (blk != NULL) delete blk;
cout << "Fork time " << cycle2milisec(t)
<< " msec. [" << t << " cycles]" << endl; exit(EXIT_SUCCESS);
}
Эта программа сделана так, что может иметь один численный параметр: размер (в мегабайтах) блока условных данных (в нашем случае даже неинициализированных), принадлежащего адресному пространству процесса. (Функцию преобразования процессорных циклов в соответствующий миллисекундный интервал
cycle2milisec
мы видели раньше, и поэтому в листинг она не включена.)