《Linux – Linux高级编程 – 第二部分 进程与线程》第2章 线程(一)

1 线程基础

1.1线程概述

线程( thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期, solaris是这方面的佼佼者。 传统的Unix也支持线程的概念, 但是在一个进程(process)中只允许有一个线程, 这样多线程就意味着多进程。 现在, 多线程技术已经被许多操作系统所支持,包括Windows/NT,当然,也包括Linux。

传统多任务操作系统中一个可以独立调度的任务(或称之为顺序执行流)是一个进程。每个程序加载到内存后只可以唯一地对应创建一个顺序执行流,即传统意义的进程。每个进程的全部系统资源是私有的,如虚拟地址空间,文件描述符和信号处理等等。使用多进程实现多任务应用时存在如下问题:

1)任务切换,即进程间上下文切换,系统开销比较大。(虚拟地址空间以及task_struct 都需要切换)

2)多任务之间的协作比较麻烦,涉及进程间通讯。(因为不同的进程工作在不同的地址空间)
所以,为了提高系统的性能,许多操作系统规范里引入了轻量级进程的概念,也被称为线程。既然多进程有一些缺陷,那么有什么来解决呢?那就是多线程。接下来就谈谈多线程和多进程的比较。

使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。 我们知道, 在Linux系统下,启动一个新的进程必须分配给它独立的地址空间, 建立众多的数据表来维护它的代码段、 堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。

使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题, 有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作( timeconsuming)置于一个新的线程,可以避免这种尴尬的情况。

2) 使多CPU系统更加有效。操作系统会保证当线程数不大于 CPU数目时,不同的线程运行于不同的CPU上。

3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

通常线程指的是共享相同地址空间的多个任务。线程最大的特点就是在同一个进程中创建的线程共享该进程的地址空间;但一个线程仍用task_struct 来描述,线程和进程都参与统一的调度。所以,多线程的好处便体现出来:

1)大大提高了任务切换的效率;因为各线程共享进程的地址空间,任务切换时只要切换task_struct 即可;

2)线程间通信比较方便;因为在同一块地址空间,数据共享;
当然,共享地址空间也会成为线程的缺点,因为共享地址空间,如果其中一个线程出现错误(比如段错误),整个线程组都会崩掉!

Linux之所以称呼其线程为LWP( Light Weight Process ),因为从内核实现的角度来说,它并没有为线程单独创建一个结构,而是继承了很多进程的设计:

1)继承了进程的结构体定义task_struct ;
2)没有专门定义线程ID,复用了PID;
3)更没有为线程定义特别的调度算法,而是沿用了原来对task_struct 的调度算法。

在最新的Linux内核里线程已经替代原来的进程称为调度的实际最小单位。原来的进程概念可以看成是多个线程的容器,称之为线程组;即一个进程就是所有相关的线程构成的一个线程组。传统的进程等价于单线程进程。

每个线程组都有自己的标识符 tgid (数据类型为 pid_t ),其值等于该进程(线程组)中的第一个线程(group_leader)的PID。

1.2线程编程入门

Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。顺便说一下, Linux下pthread的实现是通过系统调用clone( )来实现的。clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone( )的详细情况,有兴趣的读者可以去查看有关文档说明。

1.2.1创建线程

pthread_create()函数描述如下:

所需头文件 #include
函数原型 int pthread_create(pthread_t thread, const pthread_attr_t attr,void (start_routine) (void ), void arg);
函数参数 thread:创建的线程
attr:指定线程的属性,NULL表示使用缺省属性
routine:线程执行的函数
arg:传递给线程执行的函数的参数
函数返回值 成功:0
失败:-1

1)这里start_routine 是回调函数(callback),其函数类型由内核来决定,这里我们将其地址传给内核;这个函数并不是线程创建了就会执行,而是只有当其被调度到cpu上时才会被执行。

2)arg 是线程执行函数的参数,这里我们将其地址穿进去,使用时需要先进行类型转换,才能使用;如果参数不止一个,我们可以将其放入到结构体中。

【注意】
1.在编译时注意加上-lpthread参数,以调用静态链接库。因为pthread并非Linux系统的默认库。
2.Linux中错误编号在/usr/include/asm-generic/errno.h文件中
$cat /usr/include/asm-generic/errno.h

cnQxPO.png

下面通过一个实例来深入了解一个线程是如何工作的。

/**Includes*********************************************************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h>  

/**【全局变量声明】*************************************************************/

/**【函数声明】*    ************************************************************/
void printids(const char *str);
void *thread_function(void *arg);  

int main(int argc, char *argv[])
{     
    pthread_t thread_id;  
    int err;
    err = pthread_create(&thread_id,NULL,thread_function,NULL); 

    if(err != 0)  
    {  
        perror("fail to pthread_create");   
    }  
    printids("main thread: ");
    sleep(1);
    exit(0);

    return 0;  
} 

void printids(const char *str)
{
    pid_t pid;
    pthread_t  tid;

    pid = getpid();
    tid = pthread_self();

    printf("%s pid %u tid %u (0x%x)\n",str,(unsigned int)pid,(unsigned int)tid,(unsigned int)tid);

}

void *thread_function(void *arg)  
{  
    printids("new thread: "); 

    return ((void *)0); 
}

【注】线程通过第三方的线程库来实现,所以这里要 -lpthread ,-l 是链接一个库,这个库是pthread;

编译运行:

cnQzGD.png

从结果可以看出,两个线程拥有相同的进程ID,但是线程ID却不同。

【小贴士】
pthread_self()函数

头文件 #include
函数声明 pthread_t pthread_self(void);
参数 None
返回值 返回线程ID

【注】进程和线程的区别

线程 进程
标识符类型 pthread_t pid_t
获取ID函数 pthread_self() getpid()
创建函数 pthread_creat() fork()

1.2.2线程的连接(合并)

pthread_join ()函数其函数描述如下:

所需头文件 #include
函数原型 int pthread_join(pthread_t thread, void **retval);
函数参数 thread:要等待的线程
retval:指向线程返回的函数
函数返回值 成功:0
失败:-1

这里,我们可以看到 value_ptr 是个二级指针,其是出参,存放的是线程返回参数的地址。

我们知道pthread_create()接口负责创建了一个线程。那么线程也属于系统的资源,这跟内存没什么两样,而且线程本身也要占据一定的内存空间。众所周知的一个问题就是C或C++编程中如果要通过malloc()或new分配了一块内存,就必须使用free()或delete来回收这块内存,否则就会产生著名的内存泄漏问题。既然线程和内存没什么两样,那么有创建就必须得有回收,否则就会产生另外一个著名的资源泄漏问题,这同样也是一个严重的问题。那么线程的合并就是回收线程资源了。

线程的合并是一种主动回收线程资源的方案。当一个进程或线程调用了针对其它线程的pthread_join()接口,就是线程合并了。这个接口会阻塞调用进程或线程,直到被合并的线程结束为止。当被合并线程结束,pthread_join()接口就会回收这个线程的资源,并将这个线程的返回值返回给合并者。

下面看一个实例:

/**Includes*********************************************************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h>  

/**【全局变量声明】*************************************************************/
char message[32] = "Hello World!";  

/**【函数声明】*    ************************************************************/
void *thread_function(void *arg);  

int main(int argc, char *argv[])
{  
    pthread_t a_thread;  
    void *thread_result;  

    if(pthread_create(&a_thread,NULL,thread_function,(void *)message) < 0)  
    {  
        perror("fail to pthread_create");  
        exit(-1);  
    }  

    printf("waiting for thread to finish\n");  
    if(pthread_join(a_thread,&thread_result) < 0)  
    {  
        perror("fail to pthread_join");  
        exit(-1);  
    }  
    printf("Message is now %s\n",message);  
    printf("thread_result is %s\n",(char *)thread_result);  

    return 0;  
}

void *thread_function(void *arg)  
{  
    printf("thread_function is running,argument is %s\n",(char *)arg);  
    strcpy(message,"marked by thread");  
    pthread_exit("Thank you for the cpu time");  
}   

编译运行:

cn17H1.png

从这个程序,我们可以看到线程之间是如何通信的,线程之间通过二级指针来传送参数的地址(这是进程所不具备的,因为他们的地址空间独立),但两个线程之间的通信,传递的数据的生命周期必须是静态的。可以使全局变量、static修饰的数据、堆里面的数据;这个程序中的message就是一个全局变量。其中一个线程可以修改它,另一个线程得到它修改过后的message。

可以看出,当一个线程通过调用pthread_exit退出或者简单地从启动例程中返回时,进程中的其他线程可以通过调用pthread_join函数获得该线程的退出状态。pthread_create和pthread_exit函数的无类型指针可以传递的值不止一个,这个指针可以传递包含复杂信息的结构的地址,但注意,这个结构所使用的内存在调用者完全调用以后必须是有效的。

1.2.3线程的退出

如果进程中的任意一个线程调用了exit、_Exit或者_exit,那么整个进程就会终止。如果默认的动作是终止进程,那么,发送到线程的信号就会终止整个进程。

单个线程可以通过3中方式退出,因此可以在不终止整个进程的情况下,停止它的控制流。
(1) 线程可以简单地从启动例程中返回,返回值是线程的退出码。
(2) 线程可以被同一进程中的其他线程取消。
(3) 线程调用pthread_exit()。

pthread_exit 函数其函数描述如下:

所需头文件 #include
函数原型 int pthread_exit(void *value_ptr );
函数参数 value_ptr:线程退出时返回的值
函数返回值 成功:0
失败:-1

调用线程一直阻塞,直到指定的线程调用pthread_exit函数、从启动例程中返回或者取消。如果线程简单地从它的启动例程返回,retval 就包含返回码。如果线程被取消,由retval 指定的内存单元就设置为PTHREAD_CANCELED。pthread_exit函数的返回指针retval可以通过调用pthread_join () 函数得到。

【注】和进程中的wait()、exit()一样,这里pthread_join 与 pthread_exit 是工作在两个线程之中。
可以通过调用pthread_join自动把线程设置为分离转态,这样资源就可以恢复。但是已经处于分离状态的线程,pthread_join调用就会失败,返回EINVAL,调用pthread_detach可以使得线程处于分离状态。

如果对线程的返回值不感兴趣,那么可以把retval设置为NULL,在这种情况下,pthread_join函数可以等待指定的线程终止,但并不获取线程的终止状态。

下面我们看看return、exit和pthread_exit的区别。

/**Includes*********************************************************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h>  

/**【全局变量声明】*************************************************************/

/**【函数声明】*****************************************************************/
void *thread_function(void *arg);  

int main(int argc, char *argv[])
{  
    pthread_t thread;   

    if(pthread_create(&thread,NULL,thread_function,(void *)argv[1]) < 0)  
    {  
        perror("fail to pthread_create.\n");  
        exit(-1);  
    }  

    printf("waiting for thread to finish.\n");
    sleep(2);
    printf("main thread.\n");
    pthread_exit(NULL); 
}  

void *thread_function(void *arg)  
{  
    if(strcmp("1",(char *)arg) == 0 )
    {
        printf("thread_function is return ,argument is %s.\n",(char *)arg);  
        return (void*)1;
    }
    else if(strcmp("2",(char *)arg) == 0)
    {
        printf("thread_function is pthread_exit ,argument is %s.\n",(char *)arg);
        pthread_exit(NULL);
    }
    else if(strcmp("3",(char *)arg) == 0)
    {
        printf("thread_function is exit ,argument is %s.\n",(char *)arg); 
        exit(3);        
    }

}  

编译运行结果如下:

cnGBcD.png

为了进一步理解pthread_exit函数,我们看看如下例子:

/**Includes*********************************************************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h>  

/**【全局变量声明】*************************************************************/

/**【函数声明】*****************************************************************/
void *thread_function1(void *arg);  
void *thread_function2(void *arg); 

int main(int argc,char *argv[])  
{  
    pthread_t thread1;   
    pthread_t thread2;   

    if(pthread_create(&thread1,NULL,thread_function1,NULL) < 0)  
    {  
        perror("fail to pthread_create.\n");  
        exit(-1);  
    }  
    if(pthread_create(&thread2,NULL,thread_function2,NULL) < 0)  
    {  
        perror("fail to pthread_create.\n");  
        exit(-1);  
    }  
    printf("waiting for thread to finish.\n");

    pthread_join(thread1,NULL);

    printf("main thread.\n");

    pthread_exit(NULL); 
}  

void *thread_function1(void *arg)  
{  
    printf("thread_function1.\n");

    sleep(2);
    printf("thread_function1 exit\n"); 
    pthread_exit(NULL); 
}  

void *thread_function2(void *arg)  
{  
    printf("thread_function2.\n");

    sleep(4);
    printf("thread_function2 exit\n");  
    pthread_exit(NULL);
}  

编译运行结果如下:

cnGyBd.png

可以看出,主线程先于线程2执行完成,但是并没有立即退出,而是等待所有线程执行完成才退出。我们可以去掉主函数的pthread_exit(NULL);语句。结果如下所示:

cnGgAI.png

可以看到主线程执行完成后,整个程序就退出了,并没有等线程2退出,因此pthread_exit函数的主要作用是阻塞线程,回收线程资源。

1.2.4线程的分离

与线程合并相对应的另外一种线程资源回收机制是线程分离,调用接口是pthread_detach()。线程分离是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。因为线程分离是启动系统的自动回收机制,那么程序也就无法获得被分离线程的返回值,这就使得pthread_detach()接口只要拥有一个参数就行了,那就是被分离线程句柄。默认情况下线程是非分离的。线程可以分离自己。

【注1】线程合并和线程分离都是用于回收线程资源的,可以根据不同的业务场景酌情使用。不管有什么理由,你都必须选择其中一种,否则就会引发资源泄漏的问题,这个问题与内存泄漏同样可怕。
【注2】在主线程最后调用pthread_exit,也就是当主线程运行到最后,然而子线程没有结束,主线程会阻塞,直到所有子线程结束;而调用pthread_join函数就相当于阻塞该线程直到子线程结束。

pthread_detach()函数是让线程由系统回收,这样可以把不在使用的线程资源再次合理利用。

/**Includes*********************************************************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h>  

/**【全局变量声明】*************************************************************/

/**【函数声明】*****************************************************************/
void *thread_function1(void *arg);  
void *thread_function2(void *arg); 

int main(int argc,char *argv[])  
{  
    pthread_t thread1;   
    pthread_t thread2;   

    if(pthread_create(&thread1,NULL,thread_function1,NULL) < 0)  
    {  
        perror("fail to pthread_create.\n");  
        exit(-1);  
    }  
    if(pthread_create(&thread2,NULL,thread_function2,NULL) < 0)  
    {  
        perror("fail to pthread_create.\n");  
        exit(-1);  
    }  
    printf("waiting for thread to finish.\n");

    pthread_join(thread1,NULL);

    printf("main thread.\n");

    pthread_exit(NULL); 
}  

void *thread_function1(void *arg)  
{  
    printf("thread_function1.\n");

    sleep(2);
    printf("thread_function1 exit\n"); 
    pthread_exit(NULL); 
}  

void *thread_function2(void *arg)  
{  
    pthread_detach(pthread_self());
    printf("thread_function2.\n");

    sleep(4);
    printf("thread_function2 exit\n");  
    pthread_exit(NULL);
}  

编译运行结果如下所示:

cnG5jg.png

1.2.5线程的取消

线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。一般情况下,线程在其主体函数退出的时候会自动终止,但同时也可以因为接收到另一个线程发来的终止(取消)请求而强制终止。在默认情况下,pthread_cancel函数会使得由标识的线程行为表现为调用了参数为PTHREAD_CANCELED的pthread_exit函数,但是,线程可以选择忽略取消或者控制如何被取消。注意pthread_cancel不是等待线程终止,它仅仅是提出请求。

pthread_cancel函数其函数描述如下:

所需头文件 #include
函数原型 int pthread_cancel(pthread_t thread);
函数参数 thread:线程ID
函数返回值 成功:0
失败:非0数,即错误编码。

 线程取消的理解

  1. 线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略(当禁止取消时)、或者立即终止(当在取消点或异步模式下)、或者继续运行至Cancelation-point(取消点,下面将描述),总之由不同的Cancelation状态决定。
  2. 线程接收到CANCEL信号的缺省处理(即pthread_create()创建线程的缺省状态)是继续运行至取消点再处理(退出),或在异步方式下直接退出。一个线程处理cancel请求的退出操作相当于pthread_exit(PTHREAD_CANCELED)。当然线程可以通过设置为PTHREAD_CANCEL_DISABLE来拒绝处理cancel请求,稍后会提及。

 取消状态
pthread_setcancelstate()函数取消线程状态,就是线程对取消信号的处理方式,忽略或者响应。线程创建时默认响应取消信号。

设置本线程对Cancel信号的反应,state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE,

分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为NULL则存入原来的Cancel状态以便恢复。

 取消类型
pthread_setcanceltype()函数是线程对取消信号的响应方式,立即取消或者延时取消。线程创建默认延时取消。

设置本线程取消动作的执行时机,type由两种取值:PTHREAD_CANCEL_DEFERRED和PTHREAD_CANCEL_ASYNCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出);oldtype如果不为NULL则存入运来的取消动作类型值。

 取消点
pthread_testcancel()是说pthread_testcancel在不包含取消点,但是又需要取消点的地方创建一个取消点,以便在一个没有包含取消点的执行代码线程中响应取消请求。

线程取消功能处于启用状态且取消状态设置为延迟状态时,pthread_testcancel()函数有效。

如果在取消功能处处于禁用状态下调用pthread_testcancel(),则该函数不起作用。

请务必仅在线程取消线程操作安全的序列中插入pthread_testcancel()。除通过pthread_testcancel()调用以编程方式建立的取消点意外,pthread标准还指定了几个取消点。测试退出点,就是测试cancel信号。

根据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、 pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系统调用都是cancelation-point,而其他pthread函数都不会引起cancelation动作。但是pthread_cancel的手册页声称,由于LinuxThread库与C库结合得不好,因而目前C库函数都不是cancelation-point;但CANCEL信号会使线程从阻塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为cancelation-point的系统调用前后调用pthread_testcancel(),从而达到POSIX标准所要求的目标,即如下代码段:

pthread_testcancel();   
retcode = read(fd, buffer,length);   
pthread_testcancel();

使用前须判断线程ID的有效性!即判断并保证:thrd != 0 否则有可能会出现“段错误”的异常!
接下来看个线程的取消实例。

/**Includes*********************************************************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h>  

/**【全局变量声明】*************************************************************/

/**【函数声明】*****************************************************************/
void *thread_function(void *arg);  

int main(int argc,char *argv[])  
{  
    pthread_t thread; 
    void *rval;
    if(pthread_create(&thread,NULL,thread_function,NULL) < 0)  
    {  
        perror("fail to pthread_create.\n");   
    }  

    printf("waiting for thread to finish.\n");

    sleep(2);
    //取消信号
    if(pthread_cancel(thread) !=0 )
    {
        perror("fail to pthread_cancel.\n"); 
    }

    if(pthread_join(thread,&rval) != 0)
    {
        perror("fail to pthread_join.\n"); 
    }

    printf("main thread exit,code is %ld.\n",(long )rval);

    pthread_exit(NULL); 
}  

void *thread_function(void *arg)  
{  
    int state;
    printf("thread_function.\n");
    //忽略取消信号
    state = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);
    if(state !=0 )
    {
        perror("fail to pthread_setcancelstate.\n");  
    }
    printf("I am new thread_function.\n");

    sleep(4);

    printf("enable cancel.\n");
    //取消信号
    state = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);
    if(state !=0 )
    {
        perror("fail to pthread_setcancelstate.\n");  
    }
    /*
    //取消信号类型,默认延时取消
    if(pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL) != 0)
    {
        perror("fail to pthread_setcanceltype.\n");  
    }
    */
    printf("first cancel point.\n");
    printf("second cancel point.\n");

    printf("thread_function exit.\n"); 

    pthread_exit((void *)20);
}   

编译运行结果如下所示:

cnGXCV.png

1.2.6线程的信号

int pthread_kill(pthread_t thread, int sig) 

向指定ID的线程发送sig信号,如果线程的代码内不做任何信号处理,则会按照信号默认的行为影响整个进程。也就是说,如果你给一个线程发送了SIGQUIT,但线程却没有实现signal处理函数,则整个进程退出。

pthread_kill(threadid, SIGKILL)也一样,他会杀死整个进程。

如果要获得正确的行为,就需要在线程内实现signal(SIGKILL,sig_handler)。

所以,如果int sig的参数不是0,那一定要清楚到底要干什么,而且一定要实现线程的信号处理函数,否则,就会影响整个进程。

那么,如果int sig的参数是0呢,这是一个保留信号,一个作用就是用来判断线程是不是还活着。
我们来看一下pthread_kill的返回值:

线程仍然活着:0;线程已不存在:ESRCH;信号不合法:EINVAL。

/**Includes*********************************************************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h> 
#include <signal.h> 
#include <sys/types.h>
#include <errno.h>

/**【全局变量声明】*************************************************************/

/**【函数声明】*    ************************************************************/
void *thread_function(void *arg);  

int main(int argc, char *argv[])
{ 
    pthread_t thread;  

    if(pthread_create(&thread,NULL,thread_function,NULL) < 0)  
    {  
        perror("fail to pthread_create");    
    }  

    printf("waiting for thread to finish\n");  

    sleep(1);

    int s = pthread_kill(thread,0);
    if(s == ESRCH)
    {
        printf("thread is not fonud.\n");
    }
    pthread_exit(NULL);
} 

void *thread_function(void *arg)  
{  
    printf("thread_function is running.\n");  

    printf("pthread is exit.\n");
}  

编译运行结果如下所示:

cnGj3T.png

再看看下面的例子。

/**Includes*********************************************************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h> 
#include <signal.h> 
#include <sys/types.h>
#include <errno.h>

/**【全局变量声明】*************************************************************/

/**【函数声明】*    ************************************************************/
void *thread_function(void *arg);  

int main(int argc, char *argv[])
{  
    pthread_t thread;  

    if(pthread_create(&thread,NULL,thread_function,NULL) < 0)  
    {  
        perror("fail to pthread_create");    
    }  

    printf("waiting for thread to finish\n");  

    int s = pthread_kill(thread,SIGQUIT);
    if(s == ESRCH)
    {
        printf("thread is not fonud.\n");
    }

    printf("main pthread is exit.\n");

    return 0;
} 

void *thread_function(void *arg)  
{  
    sleep(1);
    printf("thread_function is running.\n");  

    printf("pthread is exit.\n");
}  

编译运行结果如下所示:

cnJSu4.png

 信号集操作函数
sigaction函数用来查询和设置信号处理方式,它是用来替换早期的signal函数。sigaction函数原型及说明如下:

函数原型:

int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact)

函数说明:sigaction()会依参数signum指定的信号编号来设置该信号的处理函数

函数参数:
signum是指定信号的编号,除SIGKILL和SIGSTOP信号以外
act参数如下:
参数结构sigaction定义如下

void (*sa_handler) (int);
void  (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer) (void);
}

①sa_handler:此参数和signal()的参数handler相同,此参数主要用来对信号旧的安装函数signal()处理形式的支持
②sa_sigaction:新的信号安装机制,处理函数被调用的时候,不但可以得到信号编号,而且可以获悉被调用的原因以及产生问题的上下文的相关信息。
③sa_mask:用来设置在处理该信号时暂时将sa_mask指定的信号搁置
④sa_restorer: 此参数没有使用
⑤sa_flags:用来设置信号处理的其他相关操作,下列的数值可用,可用OR 运算(|)组合:
A_NOCLDSTOP:如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父进程
SA_ONESHOT/SA_RESETHAND:当调用新的信号处理函数前,将此信号处理方式改为系统预设的方式
SA_RESTART:被信号中断的系统调用会自行重启
SA_NOMASK/SA_NODEFER:在处理此信号未结束前不理会此信号的再次到来
SA_SIGINFO:信号处理函数是带有三个参数的sa_sigaction
oldact参数:如果参数oldact不是NULL指针,则原来的信号处理方式会由此结构sigaction返回
函数返回值:成功返回0,出错则返回-1,错误原因存于error中

int sigemptyset(sigset_t *set);//初始化set所指向的信号集,使其中所有信号对应的比特位清零,表示该信号集不包含任何信号
int sigfillset(sigset_t *set);//初始化set所指向的信号集,将其中所有信号对应的比特位置1,表示该信号集的有效信号包括系统支持的所有信号
int sigaddset(sigset_t *set, int signo);//表示将set所指向的信号集中的signo信号置1
int sigdelset(sigset_t *set, int signo);//表示将set所指向的信号集中的signo信号清零
int sigismember(const sigset_t *set, int signo);//用来判断set所指向的信号集的有效信号中是否包含signo信号,包含返回1,不包含返回0,出错返回-1

注意:在使用sigset_t类型的变量前,一定要调用sigemptyset或sigfillset进行初始化,使信号集处于某种确定的状态,初始化之后就可以调用sigaddset或sigdelset在信号集中添加或删除某种有效信号。

 多线程信号处理
pthread_sigmask函数:
how:
SIG_BLOCK: 结果集是当前集合参数集的并集(把参数set中的信号添加到信号屏蔽字中)
SIG_UNBLOCK: 结果集是当前集合参数集的差集(把信号屏蔽字设置为参数set中的信号)
SIG_SETMASK: 结果集是由参数集指向的集(从信号屏蔽字中删除参数set中的信号)
每个线程均有自己的信号屏蔽集(信号掩码),可以使用pthread_sigmask函数来屏蔽某个线程对某些信号的。

响应处理,仅留下需要处理该信号的线程来处理指定的信号。实现方式是:利用线程信号屏蔽集的继承关系(在主进程中对sigmask进行设置后,主进程创建出来的线程将继承主进程的掩码).

/**Includes*********************************************************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h>  
#include <signal.h>

/**【全局变量声明】*************************************************************/

/**【函数声明】*****************************************************************/
void sig_hander1(int arg);
void sig_hander2(int arg); 
void *thread_function1(void *arg); 
void *thread_function2(void *arg); 

int main(int argc,char *argv[])  
{  
    pthread_t thread1; 
    pthread_t thread2;

    if(pthread_create(&thread1,NULL,thread_function1,NULL) < 0)  
    {  
        perror("fail to pthread_create.\n");   
    }  
    if(pthread_create(&thread2,NULL,thread_function2,NULL) < 0)  
    {  
        perror("fail to pthread_create.\n");   
    } 

    sleep(1);

    if(pthread_kill(thread1,SIGQUIT) != 0)
    {
        perror("thread1 fail to pthread_kill.\n");  
    }
    if(pthread_kill(thread2,SIGQUIT) != 0)
    {
        perror("thread2 fail to pthread_kill.\n");  
    }

    pthread_join(thread1,NULL);
    pthread_join(thread2,NULL);
    printf("main thread exit.\n");

}  

void sig_hander1(int arg)
{
    printf("thread1 get singal.\n");

    return ;
}

void sig_hander2(int arg)
{
    printf("thread2 get singal.\n");

    return ;
}

void *thread_function1(void *arg)  
{
    printf("thread_function1.\n");
    struct sigaction act;
    memset(&act,0,sizeof(act));

    sigaddset(&act.sa_mask,SIGQUIT);
    act.sa_handler = sig_hander1;
    sigaction(SIGQUIT,&act,NULL);
    pthread_sigmask(SIG_BLOCK,&act.sa_mask,NULL);
    sleep(2);
    printf("thread_function1 exit.\n"); 
}  

void *thread_function2(void *arg)  
{  
    printf("thread_function2.\n");
    struct sigaction act;
    memset(&act,0,sizeof(act));

    sigaddset(&act.sa_mask,SIGQUIT);
    act.sa_handler = sig_hander2;
    sigaction(SIGQUIT,&act,NULL);
    //pthread_sigmask(SIG_BLOCK,&act.sa_mask,NULL);

    sleep(2);

    printf("thread_function2 exit.\n"); 
}  

编译运行结果如下所示:

cnJiU1.png

为何两次执行的结果不一样呢?是因为在多线程中调用sigaction函数时,在多个线程操作中是最后执行sigaction函数的线程处理信号。

1.2.7线程的清除

当一段代码被取消时需要恢复一些状态,必须使用清除处理器,当线程在等待一个条件变量时被取消,他将被唤醒,并保持互斥量的加锁状态,在线程终止前,通常需要恢复不变量,总是需要释放互斥来量。

可以把每一个线程考虑为有一个活动的清除处理函数的栈,用pthread_cleanup_push将清除处理函数加入到栈,pthread_cleanup_pop删除最近添加的处理函数,当线程被取消或调用pthread_exit时,pthread从最近增加清除处理函数开始,依次调用各个活动的清除处理函数。

清除处理函数也可以设计为线程被取消时,也能经常使用清除处理函数,不论取消是否正常完成,当pthread_cleanup_pop以非零值调用时就算线程没被取消,清除处理函数也要被执行

void pthread_cleanup_push(void(*routine)(void *), void *args);//注销处理程序

如果弹出函数中的参数为非零值,则会从栈中删除该处理程序并执行该处理程序。如果该参数为零,则会弹出该处理程序,而不执行它。

void pthread_cleanup_pop(int execute);//清除处理函数

线程可以安排它退出时需要调用的函数,这与进程退出时可以调用atexit函数安排退出时类似的。这样的函数称为线程清理处理程序。一个线程可以建立多个清理处理程序(thread cleanup handler)。处理程序记录在栈中,也就是说,他们的执行顺序与注册顺序相反。

pthread_cleanup_push () 函数描述如下:

头文件 #include
函数声明 void pthread_cleanup_push(void (routine)(void ),void *arg);
参数 routine:清理线程函数。
arg:清理函数的参数。
返回值 None

pthread_cleanup_pop () 函数描述如下:

头文件 #include
函数声明 void pthread_cleanup_pop(int execute);
参数 execute
返回值 None

当线程执行以下动作时,清理函数是由pthread_cleanup_push函数调度的,函数只有一个参数arg:
 调用pthread_exit时;
 响应取消请求时;
 用非零execute参数调用pthread_cleanup_pop时。

如果execute参数设置为0,清理函数将不被调用。不管发生上述那种情况,pthread_clean_up都将删除上次pthread_cleanup_push调用建立的清理函数程序。

这些函数都有一个限制没有与他们可以实现为宏,所以必须与线程相同的作用于匹配的形式使用。pthread_cleanup_push的宏定义可以包含字符{,这种情况下,在pthread_cleanup_pop的定义中要有对应的匹配字符}。

下面通过一个例子来说明pthread_cleanup_push和pthread_cleanup_pop的使用。

/**Includes*********************************************************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h>  
#include <signal.h>

/**【全局变量声明】*************************************************************/

/**【函数声明】*    ************************************************************/
void *first_clean(void *arg);
void *second_clean(void *arg);
void *thread_function1(void *arg); 
void *thread_function2(void *arg); 

int main(int argc,char *argv[])  
{  
    pthread_t thread1; 
    pthread_t thread2;

    if(pthread_create(&thread1,NULL,thread_function1,NULL) < 0)  
    {  
        perror("fail to pthread_create.\n");   
    }  
    if(pthread_create(&thread2,NULL,thread_function2,NULL) < 0)  
    {  
        perror("fail to pthread_create.\n");   
    } 

    sleep(2);

    printf("main thread exit.\n");

    pthread_exit(NULL);
}  

void *first_clean(void *arg)
{
    printf("%s first clean\n",(char *)arg);

    return (void *)0;
}

void *second_clean(void *arg)
{
    printf("%s second clean\n",(char *)arg);

    return (void *)0;
}

void *thread_function1(void *arg)  
{
    printf("thread_function1.\n");
    pthread_cleanup_push(first_clean, "thread1");//注销处理程序
    pthread_cleanup_push(second_clean, "thread1");//注销处理程序

    pthread_cleanup_pop(1);//清除处理函数
    pthread_cleanup_pop(0);//清除处理函数

    printf("thread_function1 exit.\n"); 

    return (void *)1;
}  

void *thread_function2(void *arg)  
{  
    printf("thread_function2.\n");
    pthread_cleanup_push(first_clean, "thread2");//注销处理程序
    pthread_cleanup_push(second_clean, "thread2");//注销处理程序

    pthread_cleanup_pop(0);//清除处理函数
    pthread_cleanup_pop(0);//清除处理函数

    printf("thread_function2 exit.\n"); 
}  

编译运行:

cnJAC6.png

再来看个例子。

/**Includes*********************************************************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <pthread.h>  

/**【函数声明】*    ************************************************************/
void cleanup(void *arg);
void *thr_fn1(void *arg);
void *thr_fn2(void *arg);

int main(int argc, char *argv[])
{
    int err;
    pthread_t tid1,tid2;
    void *tret;
    err = pthread_create(&tid1,NULL,thr_fn1,(void *)1);
    if(err != 0)
    {
        fprintf(stderr,"thread create 1 is error\n");
        return -1;
    }
    err = pthread_create(&tid2,NULL,thr_fn2,(void *)1);
    if(err != 0)
    {
        fprintf(stderr,"thread create 2 is error\n");
        return -2;
    }
    err = pthread_join(tid1,&tret);
    if(err != 0)
    {
        fprintf(stderr,"can't join with thread 1\n");
        return -2;
    }

    printf("thread 1 exit code %ld\n",(long)tret);
    err = pthread_join(tid2,&tret);
    if(err != 0)
    {
        fprintf(stderr,"can't join with thread 2\n");
        return -2;
    }
    printf("thread 2 exit code %ld\n",(long)tret);
    return 0;
}

void cleanup(void *arg)
{
    printf("cleanup:%s\n",(char*)arg);
}

void *thr_fn1(void *arg)
{
    printf("thread 1 start\n");
    pthread_cleanup_push(cleanup,"thread 1 first handler");
    pthread_cleanup_push(cleanup,"thread 1 second handler");
    printf("thread 1 push complete\n");
    if(arg)
    {
        return ((void *)1);
    }
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    return ((void *)1);
}

void *thr_fn2(void *arg)
{   
    printf("thread 2 start\n");
    pthread_cleanup_push(cleanup,"thread 2 first handler");
    pthread_cleanup_push(cleanup,"thread 2 second handler");
    printf("thread 2 push complete\n");
    if(arg)
    {
        pthread_exit((void *)2);
    }
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    pthread_exit((void *)2);
}

编译运行:

cnJQUI.png

两个线程都调用了,但是却只调用了第二个线程的清理处理程序,所以如果线程是通过从它的启动历程中返回而终止的话,那么它的清理处理程序就不会被调用,还要注意清理程序是按照与它们安装时相反的顺序被调用的。从代码输出也可以看到先执行的thread 2 second handler后执行的thread 2 first handler。

从以上内容可以看出,线程和进程有些相似之处,见下表所示:

cnJs2T.png

在默认的情况下,线程的终止状态会保存直到对该线程调用pthread_join。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,如果线程已经分离,线程的底层存储资源可以在线程终止时立即被收回。在线程别分离后,我们不能用pthread_join函数等待它的终止状态,因为对分离状态的线程调用pthread_join会产生未定义行为。可以调用pthread_detach分离线程。

Related posts

Leave a Comment