POSIX 线程小结

POSIX 在IEEE Std 1003.1c-1995 (也称为POSIX 1995 或 POSIX.1c) 对线程库进行了标准化。开发人员称之为 POSIX线程,或简称为 Pthreads。Pthreads 是 UNIX 系统上 C 和 C++ 语言的主要线程解决方案。

Pthreads API

Pthreads API 定义了构建一个多线程程序需要的方方面面——虽然是在很底层做的。Pthreads API提供了100多个接口,因此还是很庞大的。由于Pthreads API过于庞大和丑陋。Pthreads也有不少骂声。但是,它依然是UNIX系统上核心的线程库,即使使用不同的线程机制,Pthreads还是值得一学的,因为很多都是构建在Pthreads上的。

Pthreads API在文件<pthread.h>中定义的。API中的每个函数前缀都是pthread_。举个例子,创建线程的函数称为pthread_create()。pthread函数可以划分为两个大的组:

线程管理:

完成创建、销毁、连接和 detach 线程的函数。

同步:

管理线程的同步的函数,包括互斥、条件变量和障碍。

 

链接Pthreads

虽然Pthreads是由glibc提供的,但它在独立库libpthread中,因此需要显式链接.有了gcc,可以通过 -pthread 标志位来自自动完成,它确保链接到可执行文件的是正确的库:

         gcc -Wall -Werror -pthread beard.c -o beard 

如果通过多次调用gcc编译和链接二进制文件,需要给它们提供 -pthread 选项:标志位还会影响预处理器,通过设置某个预处理器可以定义控制线程的安全。

创建线程

当程序第一次运行并执行 main() 函数时,它是单线程。实际上,编译器支持一些线程安全选项,链接器把它连接到Pthreads库中,你的进程和其他进程没有什么区别。在初始化线程中,有时称为默认线程或主线程,必须创建一个或多个线程,才能实现多线程机制。

Pthreads提供了函数 pthread_create() 定义和启动新的线程:

  #include <pthread.h>
        int pthread_create (pthread_t *thread,
                                       const pthread_attr_t *attr,
                                       void *(*start_routine) (void *),
                                       void *arg);

调用成功时,会创建新的线程,开始执行 start_routine 提供的函数,可以给该函数传递一个参数 arg。函数会保存线程 ID,用于表示新的线程,在由 thread 指向的 pthread_t 结构体中,如果不是 NULL 的话。

由attr指向的pthread_attr_t 对象是用于改变新创建线程的默认线程属性。绝大多数 pthread_create() 调用会传递NULL给attr,采用默认属性。线程属性支持程序改变线程的各个方面,比如 栈大小、调度参数以及初始分离(detach)状态。 

start_routine必须包含以下特征:

                       void *start_thread (void *arg);

因此,线程执行函数,接收void指针作为参数,返回值也是个void指针。和 fork() 类似,新的线程会继承绝大多数属性、功能以及父线程的状态。和fork() 不同的是,线程会共享父进程资源,而不是接收一份拷贝。当然,最重要的共享资源是进程地址空间,但是线程也共享(通过接收拷贝)信号处理函数和打开的文件。

使用该函数的代码应该传递 -pthread 给 gcc。这适用于所有的Pthread函数,后面不会再提这一点。

出错时,pthread_create() 会直接返回非零错误码(不使用 errno),线程的内容是未定义的。可能的错误码包含:

EAGAIN   调用进程缺乏足够的资源来创建新的线程。通常这是由于进程触碰了某个用户或系统级的线程限制。

EINVAL    attr 指向的 pthread_attr_t 对象包含无效属性。

EPERM      attr 指向的 pthread_attr_t 对象包含调用进程没有权限操作的属性。

示例代码如下:       

pthread_t thread;

        int ret;

        ret = pthread_create (&thread, NULL, start_routine, NULL);

        if (ret) {

              errno = ret;

              perror ("pthread_create");

              return -1;

        }

        /* a new thread is created and running start_routine concurrently ... */

线程 ID

线程 ID (TID)类似于进程 ID(PID)。但是,PID 是由 Linux 内核分配的,而 TID 是由 Pthread 库分配的。TID 是由模糊类型 pthread_t 表示的,POSIX 不要求它是算术类型。正如我们所看到的,新线程的TID是在成功调用 pthread_create() 时,通过 thread 参数提供的。线程可以在运行时调用 pthread_self() 函数来获取自己的 TID:

     #include <pthread.h>
         pthread_t pthread_self (void);

使用方式很简单,因为函数本身不会失败:

 const pthread_t me = pthread_self ();

比较线程 ID

因为Pthread标准不需要 pthread_t 是个算术类型,因此不能确保等号可以正常工作。因此,为了比较线程 ID,Pthread库需要提供一些特定接口:

 #include <pthread.h>
         int pthread_equal (pthread_t t1, pthread_t t2);

如果提供的两个线程 ID一样, pthread_equal() 函数会返回非零值。如果提供的线程 ID 不同,返回 0. 该函数不会失败。

终止线程

和创建线程相对应的是终止线程。终止线程和进程终止很类似,差别在于当线程终止时,进程中的其他线程会继续执行。在一些线程模式中,比如“每个连接一个线程”,线程会被频繁的创建和销毁。

线程可能会在某些情况下终止,所有这些情况都和进程终止类似:

1. 如果线程在启动时返回,该线程就结束。这和 main() 函数结束有点类似。

2. 如果线程调用了 pthread_exit() 函数,它就会终止。这和调用 exit() 返回类似。

3. 如果线程是被另一个线程通过 pthread_cancel() 函数取消,它就会终止。这和通过 kill() 发送 SIGKILL 信号类似。

这三个示例都只会杀死有问题的线程。

在以下场景中,进程中的所有线程都被杀死,因此整个进程都被杀死。

1. 进程从 main() 函数中返回。

2. 进程如果 exit() 函数终止。

3. 进程通过 execve() 函数执行新的二进制镜像。

信号可以杀死一个进程或单个线程,这取决于如何发送。Pthreads 使得信号处理变得复杂,在多线程程序中,最好最小化信号的使用方式。

线程自杀

最简单的线程自杀方式是在启动时就结束掉。在通常情况下,你可能想要结束函数调用栈中的某个线程,而不是在启动时。在这种情况下,Pthreads 提供了 pthread_exit() 函数,该函数等价于 exit() 函数:   

#include <pthread.h>
         void pthread_exit (void *retval);

调用该函数时,调用线程结束。retval 是提供给需要等待结束线程的终止状态的线程,还是和 exit() 功能类似。不会出现出错情况。

使用方式:

  pthread_exit(NULL);

终止其他线程

线程通过其他线程终止来调用结束线程。它提供了 pthread_cancel() 函数来实现这一点:

 #include <pthread.h>
        int pthread_cancel (pthread_t thread);

成功调用 pthread_cancel() 会给由线程 ID 表示的线程发送取消请求。线程是否可以取消以及如何取消分别取决于取消状态和取消类型。成功时, pthread_cancel() 会返回 0。注意,返回成功只是表示成功执行取消请求。实际的取消操作是异步的。出错时,pthread_cancel() 会返回 ESRCH,表示thread 是非法的。

线程是否可取消以及何时取消有些复杂。线程的取消状态只有“允许(enable)”和“不允许(disable)”两种。对于新的线程,默认是允许。如果线程不允许取消,请求会入队列,直到允许取消。在其他情况下,取消类型会声明什么时候取消请求。线程可以通过 pthread_setcancelstate() 来改变其状态。

   #include <pthread.h>
       int pthread_setcancelstate (int state, int *oldstate);

成功时,调用线程的取消状态会被设置成 state,老的状态保存到 oldstate中。 state值可以是 PTHREAD_CANCEL_ENABLE 或 PTHREAD_CANCEL_DISABLE,分别表示支持取消和不支持取消。

出错时,pthread_setcancelstate() 会返回 EINVAL,表示 state 值无效。

线程的取消类型可以是异步的或延迟的(deferred),默认是后者。对于异步取消请求操作,当发出取消请求后,线程可能会在任何点被杀死。对于延迟的取消请求,线程只会在特定的取消点(cancellation points)被杀死,它是 Pthread或C库,表示要终止调用方的安全点。异步取消操作只有在某些特定的场景下才有用,因为它使得进程处于未知状态。举个例子,如果取消的线程是处于临界区的中央,会发生什么情况?对于合理的程序行为,异步取消操作只应该用于那些永远都不会使用共享资源的线程,而且只调用信号安全的函数。线程可以通过 pthread_setcanceltype() 改变状态。

  #include <pthread.h>
        int pthread_setcanceltype (int type, int *oldtype);

成功时,调用线程的取消类型会设置成 type,老的类型会保存在 oldtype 中。type 可以是 PTHREAD_CANCEL_ASYNCHRONOUS 或 PTHREAD_CANCEL_DEFERRED 类型,分别使用异步或延迟的取消方式。

出错时,pthread_setcanceltype() 会返回 EINVAL, 表示非法的type值。

下面我们来考虑一个线程终止另一个线程的示例。首先,要终止的线程支持取消,并把类型设置成 deferred(这些是默认设置,因此以下只是个示例);

int unused;

int ret;

ret = pthread_setcancelstate (PTHREAD_CANCEL_ENABLE, &unused);

if (ret) 

{

     errno = ret;

     perror ("pthread_setcancelstate");

     return -1;

}

ret = pthread_setcanceltype (PTHREAD_CANCEL_DEFERRED, &unused);

if (ret) {

     errno = ret;

     perror ("pthread_setcanceltype");

     return -1;

}

然后,另一个线程发送取消请求:

int ret;

/* 'thread' is the thread ID of the to-termnate thread */

ret = pthread_cancel (thread);

if (ret) {

     errno = ret;

     perror("pthread_cancel");

     return -1;

}

 

join (加入)线程和 detach (分离)线程

由于线程创建和销毁很容易,必须有对线程进行同步的机制,避免被其他线程终止——对应的线程函数即 wait() 。实际上,即 join(加入) 线程。

join 线程

join 线程支持一个线程阻塞,等待另一个线程终止:

  #include <pthread.h>
         int pthread_join (pthread_t thread, void **retval);

成功调用时,调用线程会被阻塞,直到由 thread 指定的线程终止(如果线程已经终止,pthread_join() 会立即返回)。一旦线程终止,调用线程就会醒来,如果retval值不为NULL,被等待线程传递给 pthread_exit() 函数的值或其运行函数退出时的返回值会被放到 retval中。通过这种方式,我们称线程已经被“joined“了。join线程支持线程和其他线程同步执行。Pthread 中的所有线程都是对等节点,任何一个线程都可以 join 对方。一个线程 join 多个线程(实际上,正如我们所看到的,这往往是主线程等待其创建的线程的方式),但是应该只有一个线程尝试 join 某个特殊线程,多个线程不应该尝试随便 join 任何一个线程。

出错时,pthread_join() 会返回以下非 0 错误码值之一:

EDEADLK   检测到死锁:线程已经等待 join 调用方,或者线程本身就是调用方。

EINVAL       由thread指定的线程不能 join。

ESRCH       由thread指定的线程是无效的。

使用示例:
       

 int ret;

         /* join with 'thread' and we don't care about its return value */

         ret = pthread_join (thread, NULL);

         if (ret) {

              errno = ret;

              perror ("pthread_join");

              return -1;

          }

detach 线程

默认情况下,线程是创建成可 join 的。但是,线程也可以 detach(分离),使得线程不可 join。因为线程在被 join 之前占有的系统资源不会被释放,正如进程消耗系统资源那样,直到其父进程调用 wait(),不想 join 的线程应该调用 pthread_detach() 进行 detach。

      #include <pthread.h>
         int pthread_detach (pthread_t thread)

成功时,pthread_detach() 会分离由 thread 指定的线程,并返回 0。如果在一个已经分离的线程上调用 pthread_detach(),结果是未知的。出错时,函数返回 ESRCH,表示thread值非法。

pthread_join() 或 pthread_detach() 都应该在进程中的每个线程上调用,这样当线程终止时,也会释放系统资源。(当然,如果整个进程退出,所有的线程资源也都会释放掉,但是显式加入或分离所有线程是个良好的编程习惯。)

猜你喜欢

转载自blog.csdn.net/weixin_40039738/article/details/81143956
今日推荐