29.2 线程创建
pthread_t
类型
在 Linux 中是个整数(unsigned long
),NPTL 将其强制转换成指针。将其解释为整数或指针是不可移植的,在其他平台上,此类型可能是某个结构体。
Pthreads 线程的终止如何影响进程的终止?
在 Linux 中,要让一个进程终止,需要让它的所有(包括 detach 的)线程都终止。怎么理解包括 detach 的线程也要终止呢?如果一个线程被 detach,但是它还在运行过程中,使用了进程的资源,因此进程就不能终止。这和 C++ 的 std::thread
API 的 detach 含义有点不同,后文会解释。
使用 exit()
退出程序就会终止所有的线程(从 main()
返回则 C 语言运行时会调用 exit()
)。这其实给进程保持运行增加了一个隐含的条件:主线程不能从 main()
函数中返回。Pthreads API 的确有绕开这一条件的方法,用 pthread_exit(NULL)
来退出主线程,则进程不会直接退出,而是会等待其他线程都结束、或任意线程调用 exit()
。
Note
注意返回和退出的区别。
Pthreads API 提供了 pthread_detach()
和 pthread_join()
两个函数。前者使得一个线程被标识为 detached,其信息不会被保留、不可 join,而且在终止后会自动被系统回收(有点像设置 SIGCHLD 的处理方式为 SIG_IGN)。后者则是等待一个线程结束并获取其信息(有点像用于进程等待的 waitpid()
系统调用)。两者都只能在 joinable pthreads 上调用,调用一次之后 pthreads 就不再 joinable,再次调用以上两个函数之一是未定义行为,比如同样的线程标识符可能被其他线程重用,导致操作的线程变了。
Caution
固然可以在线程调用 pthread_exit()
前通过 pthread_detach(pthread_self())
将自己设置为 detached 来保证资源的正常回收,但是这样会使得等待这个线程的行为变得未定义。所以最好不要用这种模棱两可的做法,在创建线程的时候就要确定好这个线程的用途,以及它是要 detach 还是要被 join。
验证已经 detach 的线程还在运行时,也会阻止主进程退出
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *f(void *arg) {
printf("from another thread\n");
fflush(stdout);
for (;;) {
pause();
}
printf("from another thread but shouldn't have reached here\n");
fflush(stdout);
return NULL;
}
int main() {
pthread_t thread1;
pthread_create(&thread1, NULL, f, NULL);
pthread_detach(thread1);
pthread_exit(NULL);
}
以上代码编译运行后,只有 from another thread\n
输出。主线程因为 pthread_exit()
而结束,没有机会调用 exit()
,由于还有一个线程在运行(尽管其已经被标记为 detached),进程并没有结束。
和 C++ 的 std::thread
/std::jthread
比较
C++ 线程的 join()
操作类似于 pthread_join()
。detach()
操作也类似于 pthread_detach()
。但是 C++ 中 detach 的线程不会影响进程的退出。这和 Pthreads API 不同,为什么?
C++ 没有暴露对线程发起 pthread_exit()
操作的 API,只能靠从线程启动函数中返回来结束单个线程,因此主线程要结束就只能退出 main()
(无论是正常返回还是抛出异常),接着 exit()
函数就会被调用,导致所有线程被终止。看上去只要主线程结束,所有线程都会结束。
如果对一个线程既不 detach 又不 join:
- 在 Pthreads API 中,这样的线程终止后会成为僵尸(如果进程没有结束)。
- 在 C++ 中,
std::thread
析构时会抛出异常、std::jthread
将会 join。
和 Java 的 Thread
比较
Java 中的 Thread
默认是非守护线程,在 main()
函数退出之后 JVM 还会等待所有的非守护线程都结束。JVM 对“守护”的解释更容易让人理解。
public class Main {
public static void main(String[] args) {
var thread = new Thread(() -> {
System.out.println("from another thread");
for (;;) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO: handle exception
}
}
});
thread.setDaemon(true); // 必须在 thread.start() 之前修改线程属性
thread.start();
}
}
僵尸线程
如果有线程结束,但是没有用 pthread_join()
获取信息,则该线程会成为僵尸线程,直到进程结束才会被回收。
用 kill
发送信号给特定线程的尝试
Caution
一般建议在多线程程序中不要使用信号。
对于其过程,见
29.3 用 kill
发送信号给特定线程的尝试。对这样做是否可行的讨论,参见
33.1 能不能用 kill(1) 给特定线程发送信号呢?。
其中有个重要发现是:如果主线程使用 pthread_exit()
退出,而其他线程正在运行,则主线程会成为僵尸线程,且无法通过提前 detach 或者被其他线程 join 的方式回收。
让线程 join 自己,结果是什么?
书上有这样的习题,好像在暗示 join 自己会造成死锁。
若一线程执行了如下代码,可能会产生什么结果?
pthread_join(pthread_self(), NULL)
;
但我本地测试时发现没有任何效果,无论对主线程还是创建的其他线程都是这样。根据网上的提示,pthread_join()
在此时会失败,并返回 EDEADLK
。
// 头文件略
int main() {
int ret;
if ((ret = pthread_join(pthread_self(), NULL)) != 0) {
perror("pthread_join");
return ret;
}
}
输出 pthread_join: Success
,返回值是 35。这也说明 pthread_join()
失败时并不会设置 errno
,而且发生错误时返回值是正的。之前我写的代码没有对返回值进行检查,而以为程序没有崩溃就是调用成功,这种依赖异常机制使用 API 的方式在系统编程中是不对的。