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函数可以划分为两个大的组:


线程管理:

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


同步:

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


链接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 tread;

        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值。

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

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;
}


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值