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 的方式在系统编程中是不对的。