调用 join() 之前取消线程会报错

Thread cancellation before calling join() gives an error

POSIX Standard 表示

The lifetime of a thread ID ends after the thread terminates if it was created with the detachstate attribute set to PTHREAD_CREATE_DETACHED or if pthread_detach() or pthread_join() has been called for that thread.

在下面的程序中创建了一个线程。该线程执行 thread_task() 例程。例程完成后,线程退出,但是,因为它的 detachstate 属性是 PTHREAD_CREATE_JOINABLE(默认情况下),我希望在这个线程上调用 pthread_cancel()安全且不会 return 任何错误。 由于大量的错误检查,它有点冗长

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int counter=0;

void free_buffer(void* buff)
{
    printf("freeing buffer\n");
    free(buff);
}

void* thread_task(void* arg)
{
    void* buffer = malloc(1000);
    pthread_cleanup_push(free_buffer, buffer);

    for(int i = 0; i < 100000; i++) { // 'counter' is a global variable
        for(counter = 0; counter < 10000; counter++);
        pthread_testcancel();
    }

    pthread_cleanup_pop(1);
    printf("Thread exiting\n");
    return NULL;
}

int main()
{
    pthread_t tid;
    int errnum = pthread_create(&tid, NULL, thread_task, NULL);
    if(errnum != 0) {
        fprintf(stderr, "pthread_create(): %s\n", strerror(errnum));
        exit(EXIT_FAILURE);
    }    

    getchar();

    errnum = pthread_cancel(tid);
    if(errnum != 0) {
        fprintf(stderr, "pthread_cancel(): %s [%d]\n", strerror(errnum), errnum);
        exit(EXIT_FAILURE);
    } 

    void* ret;
    errnum = pthread_join(tid, &ret);
    if(errnum != 0) {
        fprintf(stderr, "pthread_join(): %s [%d]\n", strerror(errnum), errnum);
        exit(EXIT_FAILURE);
    } 

    if(ret == PTHREAD_CANCELED) {
        printf("Thread was canceled\n");
    }

    printf("counter = %d\n", counter);
}

但这并没有发生。当我 运行 程序时,我看到的消息是:

// wait for the thread routine to finish...
freeing buffer
Thread exiting
// press any key
pthread_cancel(): No such process [3]

这似乎暗示线程退出后,其TID不再有效。这不违反标准吗?这是怎么回事?

问题来自这样一个事实,即如果您的速度不够快,线程会在您在键盘上键入 RETURN 之前自行完成(消耗所有循环)。因此,pthread_cancel() 以错误结束,因为您试图取消已终止的线程。但是下面的pthread_join()成功收获线程。使用 strace,您可以了解会发生什么:

$ strace -f ./pcancel
execve("./pcancel", ["./pcancel"], 0x7ffd11e1ad58 /* 28 vars */) = 0
brk(NULL)                               = 0x55cf92027000
[...]

#### CREATION OF THE THREAD ==> Linux task id: 10679

clone(child_stack=0x7fe663b19fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fe663b1a9d0, tls=0x7fe663b1a700, child_tidptr=0x7fe663b1a9d0) = 10679
strace: Process 10679 attached

[pid 10678] fstat(0,  <unfinished ...>
[pid 10679] set_robust_list(0x7fe663b1a9e0, 24 <unfinished ...>
[pid 10678] <... fstat resumed> {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 13), ...}) = 0
[pid 10679] <... set_robust_list resumed> ) = 0

#### Main thread is waiting for a char on the keyboard (getchar() call)

[pid 10678] read(0,  <unfinished ...>

#### Meanwhile the thread continues its execution...

[pid 10679] mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x7fe65b31a000
[pid 10679] munmap(0x7fe65b31a000, 13524992) = 0
[pid 10679] munmap(0x7fe660000000, 53583872) = 0
[pid 10679] mprotect(0x7fe65c000000, 135168, PROT_READ|PROT_WRITE) = 0
[pid 10679] fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 13), ...}) = 0
[pid 10679] write(1, "freeing buffer\n", 15freeing buffer
) = 15
[pid 10679] write(1, "Thread exiting\n", 15Thread exiting
) = 15
[pid 10679] madvise(0x7fe66331a000, 8368128, MADV_DONTNEED) = 0

#### The thread finishes here...

[pid 10679] exit(0)                     = ?
[pid 10679] +++ exited with 0 +++

#### Main thread reads the char on the keyboard

<... read resumed> "\n", 1024)          = 1

#### The call to pthread_cancel() fails because the thread is already finished

write(2, "pthread_cancel(): No such proces"..., 38pthread_cancel(): No such process [3]
) = 38
exit_group(1)                           = ?
+++ exited with 1 +++

如果您在启动程序后快速键入两次 RETURN,pthread_cancel() 将有机会在辅助线程完成之前被主线程调用:

$ ./pcancel [RETURN typed twice very quickly]

freeing buffer
Thread was canceled
counter = 10000

我不知道 IEEE 标准,但 IMO,手册页“pthreads(7)," and "pthread_cancel(3)”含糊不清。

pthread_cancel 手册页只给出了一种可能的错误代码 ESRCH,它的意思是“找不到 ID 为 thread 的线程。”但是请注意,它说的是“找不到线程......”它并没有说“不存在这样的 ID”。

pthreads(7) 手册页 gua运行tees 非分离线程的 ID 保持有效且唯一,直到该 ID 为 join()ed,但它没有说明线程本身是否继续“存在”(在 pthread_cancel() 关心的意义上)只是因为它的 ID 继续存在。

我 运行 OP 的代码在不同的平台上,并且 pthread_cancel() 对我来说 不是 return 一个错误,甚至在很久之后该线程已从 thread_task() 函数 return 编辑。 IMO,在某些情况下,OP 的构建工具链和我的构建工具链在“符合手册页”的意义上是“正确的”。


I would expect calling pthread_cancel() on this thread to be safe and not return any error.

“安全”是什么意思?对我来说,pthread_cancel() 将是“安全的”,如果可以创建一个使用它的 gua运行teed 可靠程序。如果您不得不假设任何一种行为都是可能的,那会使事情复杂化,但我认为这不会使任务变得不可能 IMO 最糟糕的是限制了您可以获取的信息类型如果您的程序懒得记录错误,请从阅读错误中获益。

This doesn't happen however. When I run the program the messages I see are:

// wait for the thread routine to finish...
freeing buffer
Thread exiting
// press any key
pthread_cancel(): No such process [3]

在我的 Linux 机器上,我可以观察到这种行为,但如果我足够快,我也可以观察到:

freeing buffer
Thread was canceled
counter = 10000

我能够看到的一种方法是将 /dev/null 重定向到程序的标准输入。

This seems to suggest that after the thread exits, its TID is no longer valid.

没那么快。你只知道 pthread_cancel() 失败了,它选择了 ESRCH 来表征失败的原因。 POSIX 确实建议 return 值,以防 TID 在其(TID)生命周期结束后传递给 pthread_cancel(),但您似乎对此读得太多了。 POSIX 对函数可能失败的原因或错误代码应该 return 没有任何要求,特别是它不会为 TID 保留特定的错误代码是无效的。仅从错误代码不能得出 TID 无效或其生命周期已结束。

事实上,如果我在 pthread_cancel() 失败的情况下删除 exit() 调用,我可以观察到 pthread_join() 使用相同的 TID 成功,这强烈表明 TID 是在连接点仍然有效。

Doesn't this go against the standard? What's going on here?

如果 TID 的生命周期实际上在它标识的线程加入之前就结束了,那么这将违反规范,但我认为没有理由认为会发生这种情况。似乎正在发生的事情是您的 pthread_cancel() 实施对于已经终止的线程失败,无论它们是否已加入。规范没有直接说明已终止但未加入的情况,但这种行为对我来说似乎是合理的:线程无法对取消请求采取行动,因为它不再是 运行。这并不排除其他一些实现在相同情况下可能会成功——并不是每个行为细节都被指定或跨实现是一致的。

I would expect calling pthread_cancel() on this thread to be safe and not return any error.

我不明白为什么。首先,“安全”和“[不会] return 任何错误”根本不是一回事。他们甚至没有很密切的关系。 pthread_cancel() 安全,通常不应该使用它,但这与其语义有关,与它是否可能失败无关。许多更安全的功能在某些情况下会失败。事实上,他们在失败时向您报告是使他们安全的因素之一(r)。