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

Posted

技术标签:

【中文标题】调用 join() 之前取消线程会报错【英文标题】:Thread cancellation before calling join() gives an error 【发布时间】:2022-01-16 06:38:14 【问题描述】:

POSIX Standard 读到

如果线程 ID 的生命周期在线程终止后结束,则如果它是使用设置为 PTHREAD_CREATE_DETACHED 的 detachstate 属性创建的,或者已为该线程调用了 pthread_detach() 或 pthread_join()。

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

#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 不再有效。这不违反标准吗?这是怎么回事?

【问题讨论】:

FWIW:在添加四行 #include ...int counter 的全局声明后,我能够运行您的程序。它的行为方式与您描述的不同。我的环境:Apple clang 13.0.0,在基于 ARM 的 Mac 上,运行 macOS 11.6。无论取消调用是在线程到达thread_task() 的末尾之前还是之后发生,我都没有看到任何错误消息。 使用什么构建工具链?你在什么平台上运行它? 我首先在 WSL Ubuntu 20.04(使用 GCC 9.3.0 编译)上运行它。我第二次在 Arch Linux 上运行它(使用 GCC 11.1.0 编译)。每次都得到相同的输出。 【参考方案1】:

我不了解 IEEE 标准,但 IMO,手册页“pthreads(7)”和“pthread_cancel(3)”含糊不清。

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

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

我在不同的平台上运行了 OP 的代码,pthread_cancel() 没有 为我返回错误,即使在线程从 thread_task() 函数返回很久之后也是如此。 IMO,在“符合手册页”的意义上,OP 的构建工具链和我的构建工具链都是“正确的”。


我希望在这个线程上调用 pthread_cancel() 是安全的并且不会返回任何错误。

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

【讨论】:

【参考方案2】:

问题在于,如果您不够快,线程会在您在键盘上键入 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

【讨论】:

【参考方案3】:

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

// 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 重定向到程序的标准输入中。

这似乎表明线程退出后,它的 TID 不再有效。

没那么快。你只知道pthread_cancel() 失败了,它选择ESRCH 来描述失败的原因。 POSIX 确实建议在 TID 的(TID 的)生命周期结束后将其传递给 pthread_cancel() 的情况下返回值,但您似乎对此阅读过多。 POSIX 对函数可能失败的原因或如果失败应该返回什么错误代码没有任何要求,尤其是在 TID 无效的情况下,它不会保留该特定错误代码。仅从错误代码不能得出 TID 无效或其生命周期已结束。

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

这不违反标准吗?这是怎么回事?

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

我希望在此线程上调用 pthread_cancel() 是安全的,不会返回任何错误。

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

【讨论】:

Re,“线程无法处理取消请求,因为它不再运行。”同意,这符合规范,但如果它不返回错误,IMO 会更好。毕竟,我“取消”一个线程的原因是因为我希望它终止。如果它在我发送请求之前自行终止,那可以说是成功的。我希望线程终止,它被终止了。 @SolomonSlow,我理解你的观点。老实说,我不确定我更喜欢哪种方法。我很少考虑它,部分原因是我认为它是由 pthreads 开发的一个重要原则提出的:永远不要调用pthread_cancel() 触摸!我应该已经看到了,我应该问 OP“为什么要取消?”

以上是关于调用 join() 之前取消线程会报错的主要内容,如果未能解决你的问题,请参考以下文章

您需要加入已取消的线程吗? (线程)

线程 属性取消线程多线程

FutureTaskFork/Join BlockingQueue

(C语言)为啥我这样调用线程里的结构体参数会报错

Print不同版本调用不同

Java并发-取消与关闭