问题聚焦:
在简单地介绍线程的基本知识之后,主要讨论三个方面的内容:
1 创建线程和结束线程;
2 读取和设置线程属性;
3 线程同步方式:POSIX信号量,互斥锁和条件变量。
1 创建线程和结束线程;
2 读取和设置线程属性;
3 线程同步方式:POSIX信号量,互斥锁和条件变量。
Linux线程概述
线程模型
程序中完成一个独立任务的完整执行序列,即一个可调度的实体。
分为内核线程和用户线程
当进程的一个内核线程获得CPU的使用权时,它就加载并运行一个用户线程,可见,内核线程相当于用户线程运行的“容器”。
一个进程可以拥有M个内核线程和N个用户线程, M<=N。
线程实现
完全在用户空间实现线程的特点:
- 创建和调度线程都无须内核的干预,因此速度相当快。
- 不占用额外的内核资源,很多线程不会对系统性能造成明显影响。
- (缺点)一个进程的多个线程无法运行在不同的CPU上
完全在内核空间实现线程的优缺点则和上面的实现相反,优缺点也互换。
双层调度模式是前两种实现模式的混合体
- 内核调度M个内核线程,线程库调度N个用户线程
- 不会过度消耗内核资源,又可以充分利用多处理器的优势
创建线程和结束线程
基础API
创建pthread_create
定义:
#include <pthread.h> int pthread_create ( pthread_t* thread, const pthread_attr_t* attr, void * (*start_routine)(void*) , void* arg);
参数说明:
thread:新线程的标识符, 实际是一个整型,并且,Linux上几乎所有的资源标识符都是一个整型数,比如socket。
attr:用于设置新线程的属性,传递NULL表示使用默认线程属性
start_routing和arg:分别指定新线程将运行的函数及其参数。
返回:
成功时返回0,失败时返回错误码。一个用户可以打开的线程数量不能超过RLIMIT_NPROC软资源限制。此外,系统上所有能创建的线程总数也不能超过/proc/sys/kernel/threads-max 内核参数所定义的值。
退出线程pthread_exit
定义:
#include <pthread.h> void pthread_exit ( void* retval );
pthread_exit函数通过retval参数向线程的回收者传递其退出信息。它执行完之后不会返回到调用者,而且永远不会失败。
回收其他线程pthread_join
定义:
#include <pthread.h> int pthread_join( pthread_t thread, void** retval );
一个进程中的所有线程都可以调用pthread_join函数来回收其他线程,即等待其他线程结束,这类似于回收进程的wait和waitpid系统调用。
参数说明:
thread:目标线程的标识符
retval:目标线程返回的退出信息
效果:该函数会一直阻塞,直到被回收的线程结束为止
返回:成功时返回0,失败时返回错误码。
取消线程pthread_cancel
功能:
有时候我们希望异常终止一个线程,即取消线程。
定义:
#include <pthread.h> int pthread_cancel ( pthread_t thread );接收到取消请求的目标线程可以决定是否允许被取消以及如何取消。这分别由如下两个函数完成:
#include <pthread.h> int pthread_setcancelstate( int state, int *oldstate ); int pthread_setcanceltype ( int type, int *oldtype );
这两个函数的第一个参数分别用于设置线程的取消状态(是否允许取消)和取消类型(如何取消)。
第二个参数分别记录线程原来的取消状态和取消类型。
state参数有两个可选值:
- PTHREAD_CANCEL_ENABLE:允许线程被取消。它是线程被创建时默认取消状态。
- PTHREAD_CANCEL_DISABLE:禁止线程被取消。这种情况下,如果一个线程收到取消请求,则它会将请求挂起,直到该线程允许被取消。
type参数也有两个可选值:
- PTHREAD_CANCEL_ASYNCHRONOUS:线程随时都可以被取消。它将使得接收到取消请求的目标线程立即采取行动。
- PTHREAD_CANCEL_DEFERRED:允许目标线程推迟行动,直到它调用了下面几个所谓的取消点函数中的一个:pthread_join、pthread_testcancel、pthread_cond_wait、pthread_cond_timewait、sem_wait和sigwait。
pthread_setcancelstate和pthread_setcanceltype 成功时返回0,失败则返回错误码。
线程属性
pthread_attr_t结构体,完整的线程属性。
定义:
#include <bits/pthreadtypes.h> #define __SIZEOF_PTHREAD_ATTR_T 36 typedef union { char __size[__SIZEOF_PTHREAD_ATTR_T]; long int __align; } pthread_attr_t;
各种线程属性全部包含在一个字符数组中,并且线程库定义了一系列函数操作pthread_attr_t类型的变量,以方便我们获取和设置线程属性。这些函数包括:
#include <pthread.h> /*初始化线程属性对象*/ int pthread_attr_init( pthread_attr_t* attr ); /*销毁线程属性对象,被销毁的线程属性对象只有再次初始化之后才能继续使用*/ int pthread_attr_destroy( pthread_attr_t* attr); /*下面这些函数用于获取和设置线程属性对象的某个属性*/ int pthread_attr_getdetachstate( const pthread_attr_t* attr ,int* detachstate ); int pthread_attr_setdetachstate( pthread_attr_t* attr,int detachstate ); int pthread_attr_getstackaddr( const pthread_attr_t* attr,void **stackaddr ); int pthread_attr_setstackaddr( pthread_attr_t* attr,void* stackaddr ); int pthread_attr_getstacksize( const pthread_attr_t* attr,size_t* stacksize ); int pthread_attr_setstacksize( pthread_attr_t* attr,size_t stackszie ); int pthread_attr_getstack( const pthread_attr_t* attr,void** stackaddr,size_t* stacksize ); int pthread_attr_setstack( pthread_attr_t* attr,void* stackaddr,size_t stacksize ); int pthread_attr_getguardsize( const pthread_attr_t* attr,size_t* guarsize ); int pthread_attr_setguardsize( pthread_attr_t* attr,size_t guarsize ); int pthread_attr_getschedparam( const pthread_attr_t* attr,struct sched_param* param ); int pthread_attr_setschedparam( pthread_attr_t* attr,const struct sched_param* param ); int pthread_attr_getschedpolicy( const pthread_attr_t* attr,int* policy ); int pthread_attr_setschedpolicy( pthread_attr_t* attr,int policy ); int pthread_attr_getinheritsched( const pthread_attr_t* attr,int* inherit ); int pthread_attr_setinheritsched( pthread_attr_t* attr,int inherit ); int pthread_attr_getscope( const pthread_attr_t* attr,int* scope ); int pthread_attr_setscope( pthread_attr_t* attr,int scope );
下面我们详细讨论每个线程属性的含义:
- detachstate:线程的脱离状态。它有PTHREAD_CREATE_JOINABLE和PTHREAD_CREATE_DETACH两个可选值。前者指定线程是可以被回收的,后者使调用线程脱离与进程中其他线程的同步。脱离了与其他线程同步的线程称为“脱离线程”。脱离线程在退出时将自行释放其占用的系统资源。线程创建时该属性的默认值是PTHREAD_CREATE_JOINABLE。
- stackaddr和stacksize:线程堆栈的起始地址和大小。一般来说,我们不需要自己来管理线程堆栈,因为Linux默认为每个线程分配了足够的堆栈空间(一般是8 MB)。我们可以使用ulimt -s 命令查看或修改这个默认值。
- guardsize:保护区域大小。如果guardsize大于0,则系统创建线程的时候会在其堆栈的尾部额外分配guardsize字节的空间,作为保护堆栈不被错误的覆盖的区域。如果guardsize等于0,则系统不为新创建的线程设置堆栈保护区。如果使用者通过pthread_attr_setstackaddr或pthread_attr_setstack函数手动设置线程的堆栈,则guardsize属性将被忽略。
- schedparam:线程调度参数。其类型时sched_param结构体。该结构体面前还只有一个整型类型的成员——sched_priority,该成员表示线程的运行优先级。
- schedpolicy:线程调度策略。该属性有SCHED_FIFO、SCHED_RR和SCHED_OTHER三个可选值,其中SCHED_OTHER是默认值。SCHED_RR表示采用轮转算法调度,SCHED_FIFO表示使用先进先出的方法调度,这两种调度方法都具备实时调度功能,但只能用于以超级用户身份运行的进程。
- inheritsched:是否继承调用线程的调度属性。该属性有PTHREAD_INHERIT_SCHED和PTHREAD_EXPLICIT_SCHED 两个可选值。前者表示新线程沿用其创建者的线程调度参数,这种情况下再设置新线程的调度参数将没有任何效果。后者表示调用者要明确的指定新线程的调度参数。
- scope:线程间竞争CPU的范围,即线程优先级的有效范围。POSIX标准定义了该属性的PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS两个可选值,前者表示目标线程与系统中所有线程一起竞争CPU,后者表示目标线程仅与其他隶属于同一进程的线程竞争CPU的使用。面前Linux 只支持PTHREAD_SCOPE_SYSTEM这一种取值。
接下来我们讨论3种专门用于线程同步的机制:POSIX信号量,互斥量和条件变量
POSIX信号量
常用API
#include <semaphore.h> int sem_init ( sem_t* sem, int pshared, unsigned int value ); // 初始化一个未命名的信号量 int sem_destroy ( sem_t* sem ); // 用于销毁信号量,以释放其占用的内核资源 int sem_wait ( sem_t*sem ); // 以原子操作的方式将信号量减1,如果信号量的值为0,则阻塞,直到该值不为0. int sem_trywait ( sem_t* sem ); // sem_wait的非阻塞版本 int sem_post ( sem_t* sem ); // 以原子操作的方式将信号量的值加1
- sem_init函数用于初始化一个未命名的信号量。pshared参数指定信号量的类型。如果其值是0,就表示这个信号量是当前进程的局部信号量,否则该信号量就可以在多个进程之间共享。value参数指定信号量的初始值。此外,初始化一个已经被初始化的信号量将导致不可预期的结果。
- sem_destroy函数用于销毁信号量,以释放其占用的内核资源。如果销毁一个正被其他线程等待的信号量,则将导致不可预期的结果。
- sem_wait函数以原子操作的方式将信号量的值减1.如果信号量的值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。
- sem_trywait与sem_wait函数相似,不过它始终立即返回,而不论被操作的信号量是否具有非0值,相当于sem_wait的非阻塞版本。当信号量的值大于0值时,sem_trywait对信号量执行减1操作。当信号量的值为0时,它将返回-1,并设置errno为EAGAIN。
- sem_post函数以原子操作的方式将信号量的值加1.当信号量的值大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。
这些函数成功时返回0,失败时返回-1并设置errno。
pthread 还提供了下面的方法,使得我们可以明确的将一个信号发送给指定的线程:
互斥锁
作用:用于保护关键代码段,以确保其独占式访问。
基础API:
定义:
#include <pthread.h> int pthread_mutex_init ( pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr ); int pthread_mutex_destroy ( pthread_mutex_t* mutex ); int pthread_mutex_lock ( pthread_mutex_t* mutex ); int pthread_mutex_trylock ( pthread_mutex_t* mutex ); int pthread_mutex_unlock ( pthread_mutex_t* mutex );
pthread_mutex_init函数用于初始化互斥锁。mutexattr参数指定互斥锁的属性。如果将它设置为NULL,则表示使用默认属性。除了pthread_mutex_init函数,我们还可以如下初始化一个互斥锁:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;宏PTHREAD_MUTEX_INITIALIZER实际上只是把互斥锁的各个字段都初始化为0.
pthread_mutex_destroy函数用于销毁互斥锁,以释放其占用的内核资源。销毁一个已经加锁的互斥锁将导致不可预期的后果。
如果互斥锁已经被加锁,pthread_mutex_trylock将返回错误码EBUSY。
互斥锁属性:
pthread_mutexattr_t结构体中定义了一套完整的互斥锁属性
这里我们列出其中一些主要的函数
#include <pthread.h> /* 初始化互斥锁属性对象 */ int pthread_mutexattr_init ( pthread_mutexattr_t* attr ); /* 销毁互斥锁属性对象 */ int pthread_mutexattr_destroy ( pthread_mutexattr_t* attr ); /* 获取和设置互斥锁的pshared属性 */ int pthread_mutexattr_getpshared ( const pthread_mutexattr_t* attr, int * pshared ); int pthread_mutexattr_setpshared ( pthread_mutexattr_t* attr, int pthread ); /* 获取和设置互斥锁的type属性 */ int pthread_mutexattr_gettype ( const pthread_mutexattr_t* atr, int * type ); int pthread_mutexattr_settype ( pthread_mutexattr_t* attr, int type );
这里提到了两种常用属性:pshared 和type
pshared指定是否允许跨进程共享互斥锁,其可选值有两个:
- PTHREAD_PROCESS_SHARED:互斥锁可以被跨进程共享。
- PTHREAD_PROCESS_PRIVATE:互斥锁只能被和锁的初始化线程隶属于同一个进程的线程共享。
type指定互斥锁的类型,Linux支持如下4种类型的互斥锁:
- PTHREAD_MUTEX_DEFAULT:默认锁(缺省的互斥锁类型属性)。如果一个线程试图对一个默认锁重复锁定或者试图解锁一个由别的线程锁定的默认锁或者试图解锁已经被解锁的默认锁会引发不可预料的结果。
- PTHREAD_MUTEX_NORMAL:普通锁。当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个等待队列,并在该锁解锁后按优先级获得锁。这种锁类型保证了资源分配的公平性。但这种锁也很容易引发问题:(1)一个线程如果对一个已经加锁的普通锁再次加锁,将引发死锁。(2)对一个已经被其他线程加锁的普通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。
- PTHREAD_MUTEX_ERRORCHECK:检错锁。 如果一个线程试图对一个互斥锁重复锁定,将会返回一个错误码EDEADLK。 如果试图解锁一个由别的线程锁定的互斥锁或者试图解锁已经被解锁的互斥锁,则解锁操作返回EPERM。
- PTHREAD_MUTEX_RECURSIVE:嵌套锁。如果一个线程对这种类型的互斥锁重复上锁,不会引起死锁,不过一个线程对这类互斥锁的多次重复上锁必须由这个线程来重复相同数量的解
锁,这样才能解开这个互斥锁,别的线程才能得到这个互斥锁。如果对一个已经被其他线程加锁的嵌套锁解锁,或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM。这种类型的互斥锁只能是进程私有的(作用域属性为PTHREAD_PROCESS_PRIVATE)。
死锁:
结果:导致一个或多个线程被挂起而无法继续执行
原因:
对一个已经加锁的普通锁再次枷锁,将导致死锁,这种情况可能出现在不够仔细的递归函数中。
如果两个线程按照不同顺序申请两个互斥锁,也容易产生死锁。
举例:
这段代码或许总能成功的运行(不加入sleep函数刻意导致死锁),但是会为程序留下一个潜在的bug。
#include <pthread.h> #include <unistd.h> #include <stdio.h> int a = 0; int b = 0; pthread_mutex_t mutex_a; pthread_mutex_t mutex_b; void* another( void* arg ) { pthread_mutex_lock( &mutex_b ); printf( "in child thread, got mutex b, waiting for mutex a " ); sleep( 5 ); ++b; pthread_mutex_lock( &mutex_a ); b += a++; pthread_mutex_unlock( &mutex_a ); pthread_mutex_unlock( &mutex_b ); pthread_exit( NULL ); } int main() { pthread_t id; pthread_mutex_init( &mutex_a, NULL ); pthread_mutex_init( &mutex_b, NULL ); pthread_create( &id, NULL, another, NULL ); pthread_mutex_lock( &mutex_a ); printf( "in parent thread, got mutex a, waiting for mutex b " ); sleep( 5 ); ++a; pthread_mutex_lock( &mutex_b ); a += b++; pthread_mutex_unlock( &mutex_b ); pthread_mutex_unlock( &mutex_a ); pthread_join( id, NULL ); pthread_mutex_destroy( &mutex_a ); pthread_mutex_destroy( &mutex_b ); return 0; }
条件变量
互斥锁的作用:用于同步线程对共享数据的访问。
条件变量:用于在线程之间同步同享数据的值。
作用:提供了一种线程间通知的机制,当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程。
相关函数:
#include <pthread.h> int pthread_cond_init (pthread_cond_t* cond, const pthread_condattr_t* cond_attr); int pthread_cond_destroy ( pthread_cond_t* cond ); int pthread_cond_broadcast ( pthread_cond_t* cond ); //以广播的形式唤醒一个等待目标条件变量的线程 int pthread_cond_signal ( pthread_cond_t* cond ); //唤醒一个等待目标条件变量的线程 int pthread_cond_wait ( pthread_cond_t* cond, pthread_mutex_t* mutex ); // 等待目标条件变量,mutex参数保证对条件变量及其等待队列的操作原子性。
- pthread_cond_init函数用于初始化条件变量。cond_attr参数指定条件变量的属性。如果将它设置为NULL,则表示使用默认属性。除了pthread_cond_init函数外,还可以如下初始化一个条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;宏PTHREAD_COND_INITIALIZER实际上只是把条件变量的各个字段都初始化为0.
- pthread_cond_destroy 用于销毁条件变量,以释放其占用的内核资源。销毁一个正在被等待的条件变量将失败并返回EBUSY。
- pthread_cond_broadcast 函数以广播的方式唤醒所有等待目标条件变量的线程。
- pthread_cond_signal 函数用于唤醒一个等待目标条件变量的线程。至于哪个线程将被唤醒,则取决于线程的优先级和调度策略。有时候我们可能想唤醒一个指定线程,但pthread没有对该需求提供解决方案。不过我们可以间接实现该需求:定义一个能够唯一表示目标线程的全局变量,在唤醒等待条件变量的线程前先设置该变量为目标线程,然后采用广播方式唤醒所有等待条件变量的线程,这些线程被唤醒后都检查该变量以判断被唤醒的是否是自己,如果是就开始执行后续代码,如果不是则返回继续等待。
pthread_cond_wait 函数用于等待目标条件变量。mutex参数是用于保护条件变量的互斥锁,以确保pthread_cond_wait 操作的原子性。在调用pthread_cond_wait 前,必须确保互斥锁mutex已经加锁,否则将导致不可预期的结果。pthread_cond_wait 函数执行时,首先把调用线程放入条件变量的等待队列中,然后将互斥锁mutex解锁。 可见,从pthread_cond_wait 开始执行到其调用线程被放入条件变量的等待队列之间的这段时间内,pthread_cond_signal 和pthread_cond_broadcast 等函数不会修改条件变量。换言之,pthread_cond_wait 函数不会错过目标条件变量的任何变化。当pthread_cond_wait 函数成功返回时,互斥锁mutex将再次被锁上。
对三种同步机制的封装:
这是原书的代码,我也没有加注释,上面的api都了解了,这里也不会有什么问题。
#ifndef LOCKER_H #define LOCKER_H #include <exception> #include <pthread.h> #include <semaphore.h> class sem { public: sem() { if( sem_init( &m_sem, 0, 0 ) != 0 ) { throw std::exception(); } } ~sem() { sem_destroy( &m_sem ); } bool wait() { return sem_wait( &m_sem ) == 0; } bool post() { return sem_post( &m_sem ) == 0; } private: sem_t m_sem; }; class locker { public: locker() { if( pthread_mutex_init( &m_mutex, NULL ) != 0 ) { throw std::exception(); } } ~locker() { pthread_mutex_destroy( &m_mutex ); } bool lock() { return pthread_mutex_lock( &m_mutex ) == 0; } bool unlock() { return pthread_mutex_unlock( &m_mutex ) == 0; } private: pthread_mutex_t m_mutex; }; class cond { public: cond() { if( pthread_mutex_init( &m_mutex, NULL ) != 0 ) { throw std::exception(); } if ( pthread_cond_init( &m_cond, NULL ) != 0 ) { pthread_mutex_destroy( &m_mutex ); throw std::exception(); } } ~cond() { pthread_mutex_destroy( &m_mutex ); pthread_cond_destroy( &m_cond ); } bool wait() { int ret = 0; pthread_mutex_lock( &m_mutex ); ret = pthread_cond_wait( &m_cond, &m_mutex ); pthread_mutex_unlock( &m_mutex ); return ret == 0; } bool signal() { return pthread_cond_signal( &m_cond ) == 0; } private: pthread_mutex_t m_mutex; pthread_cond_t m_cond; }; #endif
线程和进程:
思考这样一个问题:如果一个多线程程序的某个线程调用了fork函数,那么新创建的子进程是否将自动创建和父进程相同的数量的线程呢?
答案是:“否”。子进程只拥有一个执行线程,它是调用fork的那个线程的完整复制。并且子进程将自动继承父进程中互斥锁(条件变量之类) 的状态。也就是说,父进程中已经被加锁的互斥锁在子进程中也是被锁住的。这就引起了一个问题:子进程可能不清楚从父进程继承而来的互斥锁的具体状态(是加锁状态还是解锁状态)。这个互斥锁可能被加锁了,但并不是由调用fork函数的那个线程锁住的,而是由其他线程锁住的。如果是这种情况,则子进程若再次对该互斥锁执行加锁操作就会导致死锁。
#include <pthread.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <wait.h> pthread_mutex_t mutex; /*子线程运行的函数。它首先获得互斥锁mutex,然后暂停5s ,再释放该互斥锁*/ void* another( void* arg ) { printf( "in child thread, lock the mutex " ); pthread_mutex_lock( &mutex ); sleep( 5 ); pthread_mutex_unlock( &mutex ); } void prepare() { pthread_mutex_lock( &mutex ); } void infork() { pthread_mutex_unlock( &mutex ); } int main() { pthread_mutex_init( &mutex, NULL ); pthread_t id; pthread_create( &id, NULL, another, NULL ); //pthread_atfork( prepare, infork, infork ); /*父进程中的主线程暂停1s,以确保在执行fork操作之前,子线程已经开始运行并获得了互斥变量mutex*/ sleep( 1 ); int pid = fork(); if( pid < 0 ) { pthread_join( id, NULL ); pthread_mutex_destroy( &mutex ); return 1; } else if( pid == 0 ) { printf( "I anm in the child, want to get the lock " ); /*子进程从父进程继承了互斥锁mutex的状态,该互斥锁处于锁住的状态,这是由父进程中的子线程执行pthread_mutex_lock引起的,因此 ,下面这句加锁操作会一直阻塞,尽管从逻辑上来说它是不应该阻塞的*/ pthread_mutex_lock( &mutex ); printf( "I can not run to here, oop... " ); pthread_mutex_unlock( &mutex ); exit( 0 ); } else { pthread_mutex_unlock( &mutex ); wait( NULL ); } pthread_join( id, NULL ); pthread_mutex_destroy( &mutex ); return 0; }
不过,pthread提供了一个专门的函数pthread_atfork,以确保fork调用后父进程和子进程都拥有一个清楚的锁状态。该函数的定义如下:
#include <pthread.h> int pthread_atfork( void (*prepare)(void), void (*parent)(void),void (*child)(void) );该函数将建立3个fork句柄来帮助我们清理互斥锁的状态。该函数成功时返回0,失败则返回错误码。
- prepare 句柄将在fork调用创建出子进程之前被执行。它可以用来锁住父进程中的互斥锁。
- parent 句柄则是fork调用创建出子进程之后,而fork返回之前,在父进程中被执行。它的作用是释放所有在prepare 句柄中被锁住的互斥锁。
- child 句柄是在fork 返回之前,在子进程中被执行。和parent句柄一样,child 句柄也是用于释放所有在prepare 句柄中被锁住的互斥锁。
使用pthread_atfork函数:
void prepare() { pthread_mutex_lock( &mutex ); } void infork() { pthread_mutex_unlock( &mutex ); } pthread_atfork( prepare, infork, infork );
线程和信号:
每个线程都可以独立的设置信号掩码。进程信号掩码的函数sigpromask,但在多线程环境下我们应该使用如下所示的pthread 版本的sigpromask函数来设置线程信号掩码:
#include <pthread.h> include <signal.h> int pthread_sigmask( int how, const sigset_t* newmask, sigset_t* oldmask );由于进程中的所有线程共享该进程的信号,所以线程库将根据线程掩码决定把信号发送给哪个具体的线程。因此,如果我们在每个子线程中都单独设置信号掩码,就很容易导致逻辑错误。此外,所有线程共享信号处理函数。也就是说,当我们在一个线程中设置了某个信号的信号处理函数后,它将覆盖其他线程为同一个信号设置的信号处理函数。这两点都说明,我们应该定义一个专门的线程来处理所有的信号。这可以通过如下两个步骤来实现:
- 在主线程创建出其他子线程之前就调用pthread_sigmask来设置好信号掩码,所有新创建的子线程都将自动继承这个信号掩码。这样做之后,实际上所有线程都不会响应被屏蔽的信号了。
- 在某个线程中调用如下函数来等待信号并处理之:
#include <signal.h> int sigwait( const sigset_t* set, int* sig );
set 参数指定需要等待的信号的集合。我们可以简单的将其指定为在第一步中创建的信号掩码,表示在该线程中等待所有被屏蔽的信号。参数sig 指向的整数用于存储该函数返回的信号值。sigwait成功时返回0,失败则返回错误码。一旦sigwait正确返回,我们就可以对接收到的信号做处理了。很显然,如果我们使用了sigwait,就不应该再为信号设置信号处理函数了。这是因为当程序接收到了信号时,二者中只能有一个起作用。
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <errno.h> /* Simple error handling functions */ #define handle_error_en(en, msg) do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0) static void *sig_thread(void *arg) { printf( "yyyyy, thread id is: %ld ", pthread_self() ); sigset_t *set = (sigset_t *) arg; int s, sig; for (;;) { /*第二步骤,调用sigwait等待信号*/ s = sigwait(set, &sig); if (s != 0) handle_error_en(s, "sigwait"); printf("Signal handling thread got signal %d ", sig); } } int main(int argc, char *argv[]) { pthread_t thread; sigset_t set; int s; /*第一步骤,在主线程中设置信号掩码*/ sigemptyset(&set); sigaddset(&set, SIGQUIT); sigaddset(&set, SIGUSR1); s = pthread_sigmask(SIG_BLOCK, &set, NULL); if (s != 0) handle_error_en(s, "pthread_sigmask"); s = pthread_create(&thread, NULL, &sig_thread, (void *) &set); if (s != 0) handle_error_en(s, "pthread_create"); printf( "sub thread with id: %ld ", thread ); pause(); /* Dummy pause so we can test program */ }
pthread 还提供了下面的方法,使得我们可以明确的将一个信号发送给指定的线程:
#include <signal.h> int pthread_kill( pthread_t thread, int sig );thread参数指定目标线程。
sig参数指定待发送的信号。如果sig为0,则pthread_kill 不发送信号,但它仍然会执行错误检查。我们可以利用这种方式来检测目标线程是否存在。
pthread_kill 成功返回0,失败则返回错误码。
小结:
这篇看似内容很多,其实大都是走马观花,了解一下整个框架和常用的API,起到一个入门和索引的作用,真正用到的时候,知道从哪里入手。
这一本书的大概框架就被我们浏览完了(排除了我不太感兴趣的几个章节,如果需要的话,我们会回头再来研究的。)
关于服务器编程,目前来说只能算是兴趣了解一下,并没有太深入,这段时间事情比较多,打算看点别的方面的资料,后面的进度上可能会慢一点了。