购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.3 利用POSIX多线程API函数进行多线程开发

在用POSIX多线程API函数进行开发之前,我们首先要熟悉这些API函数。常见的与线程有关的基本API函数见表3-1。

表3-1 常见的与线程有关的基本API函数

使用这些API函数,需要包含头文件pthread.h,并且在编译的时候需要加上库pthread,表示包含多线程库文件。

3.3.1 线程的创建

POSIX API中,创建线程的函数是pthread_create,该函数声明如下:

     int pthread_create(pthread_t *pid, const pthread_attr_t *attr,void
*(*start_routine)(void *),void *arg);

其中参数pid是一个指针,指向创建成功后的线程的ID;pthread_t其实就是unsigned long int;attr是指向线程属性结构pthread_attr_t的指针,如果为NULL则使用默认属性;start_routine指向线程函数的地址,线程函数就是线程创建后要执行的函数;arg指向传给线程函数的参数,如果成功,函数返回0 。

CreateThread创建完子线程后,主线程会继续执行CreateThread后面的代码,这就可能会出现创建的子线程还没执行完主线程就结束了的情况,比如控制台程序,主线程结束就意味着进程结束了。在这种情况下,我们就需要让主线程等待,待子线程全部运行结束后再继续执行主线程。还有一种情况,主线程为了统计各个子线程的工作结果而需要等待子线程结束完毕后再继续执行,此时主线程就要等待了。POSIX提供了函数pthread_join来等待子线程结束,即子线程的线程函数执行完毕后,pthread_join才返回,因此pthread_join是个阻塞函数。函数pthread_join会让主线程挂起(即休眠,让出CPU),直到子线程都退出,同时pthread_join能让子线程所占资源得到释放。子线程退出后,主线程会接收到系统的信号,从休眠中恢复。函数pthread_join声明如下:

     int pthread_join(pthread_t pid, void **value_ptr);

其中参数pid是所等待线程的ID;value_ptr通常可设为NULL,如果不为NULL,则pthread_join复制一份线程退出值到一个内存区域,并让*value_ptr指向该内存区域,因此pthread_join还有一个重要功能就是能获得子线程的返回值(这一点后面会看到)。如果函数成功,返回0,否则返回错误码。

接下来介绍几个简单的例子,创建线程。

例3.1 】 创建一个简单的线程,不传参数。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

     #include <pthread.h>
     #include <stdio.h>
     #include <unistd.h> //sleep
    
     void *thfunc(void *arg) //线程函数
     {
         printf("in thfunc\n");
       return (void *)0;
     }
     int main(int argc, char *argv [])
      {
         pthread_t tidp;
         int ret;
    
         ret = pthread_create(&tidp, NULL, thfunc, NULL); //创建线程
         if (ret)
         {
             printf("pthread_create failed:%d\n", ret);
             return -1;
         }
    
         sleep(1); //main线程挂起1秒钟 ,为了让子线程有机会执行
         printf("in main:thread is created\n");
    
         return 0;
     }

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost test]# g++ -o test test.cpp -lpthread
     [root@localhost test]# ./test
     in thfunc
     in main:thread is created
     [root@localhost test]#

在这个例子中,首先创建一个线程,线程函数在打印一行字符串后结束,而主线程在创建子线程后,会等待一秒钟,避免因为主线程的过早结束而导致进程结束。如果没有等待函数sleep,则可能子线程的线程函数还没来得及执行,主线程就结束了,这样导致子线程的线程都没有机会执行,因为主线程已经结束,整个应用程序已经退出了。

例3.2 】 创建一个线程,并传入整型参数。

(1)打开SI,新建一个test.cpp文件,在test.cpp中输入代码如下:

     #include <pthread.h>
     #include <stdio.h>
    
     void *thfunc(void *arg)
     {
         int *pn = (int*)(arg);                                   //获取参数的地址
         int n = *pn;
    
         printf("in thfunc:n=%d\n", n);
         return (void *)0;
     }
     int main(int argc, char *argv [])
     {
         pthread_t tidp;
         int ret, n=110;
    
         ret = pthread_create(&tidp, NULL, thfunc, &n); //创建线程并传递n的地址
         if (ret)
         {
             printf("pthread_create failed:%d\n", ret);
             return -1;
         }
    
         pthread_join(tidp,NULL);                                 //等待子线程结束
         printf("in main:thread is created\n");
    
         return 0;
     }

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost test]# g++ -o test test.cpp -lpthread
     [root@localhost test]# ./test
     in thfunc:n=110
     in main:thread is created
     [root@localhost test]#

这个例子和例3.1有两点不同,一是创建线程的时候,把一个整型变量的地址作为参数传给线程函数;二是等待子线程结束没有用sleep函数,而是用pthread_join。sleep只是等待一个固定的时间,有可能在这个固定的时间内,子线程早已经结束,或者子线程运行的时间大于这个固定时间,因此用它来等待子线程结束并不精确;而用函数pthread_join则会一直等到子线程结束后才会执行该函数后面的代码,我们可以看到它的第一个参数是子线程的ID。

例3.3 】 创建一个线程,并传递字符串作为参数。

(1)打开SI,新建一个test.cpp文件,在test.cpp中输入代码如下:

     #include <pthread.h>
     #include <stdio.h>
    
     void *thfunc(void *arg)
     {
        char *str;
        str = (char *)arg;                             //得到传进来的字符串
        printf("in thfunc:str=%s\n", str);             //打印字符串
        return (void *)0;
     }
     int main(int argc, char *argv [])
     {
        pthread_t tidp;
        int ret;
        const char *str = "hello world";
    
        ret = pthread_create(&tidp, NULL, thfunc, (void *)str);//创建线程并传递str
        if (ret)
        {
            printf("pthread_create failed:%d\n", ret);
            return -1;
        }
        pthread_join(tidp, NULL);                                         //等待子线程结束
        printf("in main:thread is created\n");
    
        return 0;
     }

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost test]# g++ -o test test.cpp -lpthread
     [root@localhost test]# ./test
     in thfunc:n=110,str=hello world
     in main:thread is created
     [root@localhost test]#

例3.4 】 创建一个线程,并传递结构体作为参数。

(1)打开SI,新建一个test.cpp文件,在test.cpp中输入代码如下:

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

      -bash-4.2# g++ -o test test.cpp -lpthread
     -bash-4.2# ./test
     in thfunc:n=110,str=hello world
     in main:thread is created
     -bash-4.2#

例3.5 】 创建一个线程,共享进程数据。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

     #include <pthread.h>
     #include <stdio.h>
    
     int gn = 10; //定义一个全局变量,将会在主线程和子线程中用到
     void *thfunc(void *arg)
     {
        gn++;    //递增1
        printf("in thfunc:gn=%d,\n", gn);  //打印全局变量gn值
        return (void *)0;
     }
    
     int main(int argc, char *argv [])
     {
         pthread_t tidp;
         int ret;
    
         ret = pthread_create(&tidp, NULL, thfunc, NULL);
         if (ret)
         {
             printf("pthread_create failed:%d\n", ret);
             return -1;
         }
         pthread_join(tidp, NULL);                  //等待子线程结束
         gn++;                                      //子线程结束后,gn再递增1
         printf("in main:gn=%d\n", gn);             //再次打印全局变量gn值
    
         return 0;
     }

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     -bash-4.2# g++ -o test test.cpp -lpthread
     -bash-4.2# ./test
     in thfunc:gn=11,
     in main:gn=12
     -bash-4.2#

从此例中可以看到,全局变量gn首先在子线程中递增1,等子线程结束后,再在主线程中递增1。两个线程都对同一个全局变量进行了访问。

3.3.2 线程的属性

POSIX标准规定线程具有多个属性。线程的主要属性包括:分离状态(Detached State)、调度策略和参数(Scheduling Policy and Parameters)、作用域(Scope)、栈尺寸(Stack Size)、栈地址(Stack Address)、优先级(Priority)等。Linux为线程属性定义一个联合体pthread_attr_t,注意是联合体而不是结构体,定义的地方在/usr/include/bits/ pthreadtypes.h中,定义如下:

     union pthread_attr_t
     {
       char __size[__SIZEOF_PTHREAD_ATTR_T];
       long int __align;
     };

从这个定义中可以看出,属性值都是存放在数组__size中的,不方便存取。但Linux中有一组专门用于存取属性值的函数。如果要获取线程的属性,首先要用函数pthread_getattr_np来获取属性结构体值,再用相应的函数来获得某个属性具体值。函数pthread_getattr_np声明如下:

     int pthread_getattr_np(pthread_t thread, pthread_attr_t *attr);

其中参数thread是线程ID,attr返回线程属性结构体的内容。如果函数成功,返回0,否则返回错误码。注意,使用该函数需要在pthread.h前定义宏_GNU_SOURCE,代码如下:

     #define _GNU_SOURCE   /* See feature_test_macros(7) */
     #include <pthread.h>

并且,当函数pthread_getattr_np获得的属性结构体变量不再需要的时候,应该用函数pthread_attr_destroy进行销毁。

我们前面用pthread_create创建线程时,属性结构体指针参数用了NULL,此时创建的线程具有默认属性,即为非分离、大小为1MB的堆栈,与父进程具有同样级别的优先级。如果要创建非默认属性的线程,可以在创建线程之前用函数pthread_attr_init来初始化一个线程属性结构体,再调用相应API函数来设置相应的属性,接着把属性结构体的指针作为参数传入pthread_create。函数pthread_attr_init声明如下:

     int pthread_attr_init(pthread_attr_t *attr);

其中参数attr为指向线程属性结构体的指针。如果函数成功,返回0,否则返回一个错误码。

需要注意的是,使用pthread_attr_init初始化线程属性,使用完(即传入pthread_create)后需要使用pthread_attr_destroy进行销毁,从而释放相关资源。函数pthread_attr_destroy声明如下:

     int pthread_attr_destroy(pthread_attr_t *attr);

其中参数attr为指向线程属性结构体的指针,如果函数成功,返回0,否则返回一个错误码。

除了创建时指定属性外,我们也可以通过一些API函数来改变已经创建了线程的默认属性。通过函数pthread_getattr_np可以获取线程的属性,该函数可以获取某个正在运行的线程的属性,函数声明如下:

     int pthread_getattr_np(pthread_t thread, pthread_attr_t *attr);

其中参数thread用于获取属性的线程ID,attr用于返回得到的属性。如果函数成功,返回0,否则返回错误码。

下面我们通过例子来演示该函数的使用方法。

1.分离状态

分离状态是线程的一个很重要的属性。POSIX线程的分离状态决定一个线程以什么样的方式终止。要注意和前面线程状态的区别,前面所说的线程的状态是不同操作系统上的线程都有的状态(它是线程当前活动状态的说明),而这里所说的分离状态是POSIX标准下的属性所特有的,它用于表明该线程以何种方式终止。默认的分离状态是可连接,即创建线程时如果使用默认属性,则分离状态属性就是可连接,因此,默认属性下创建的线程是可连接线程。

POSIX下的线程要么是分离状态的,要么是非分离状态的(也称可连接的,joinable)。前者用宏PTHREAD_CREATE_DETACHED表示,后者用宏PTHREAD_CREATE_JOINABLEB表示。默认情况下创建的线程是可连接的,一个可连接的线程可以被其他线程收回资源和取消,并且它不会主动释放资源(比如栈空间),必须等待其他线程来回收其资源,因此我们要在主线程使用函数pthread_join,该函数是个阻塞函数,当它返回时,所等待的线程的资源也就被释放了。再次强调,如果是可连接线程,当线程函数自己返回结束时或调用pthread_exit结束时都不会释放线程所占用的堆栈和线程描述符(总计8KB多),必须调用pthread_join且返回后,这些资源才会被释放。这对于父进程长时间运行的线程来说,其结果会是灾难性的。因为父进程不退出并且没有调用pthread_join,则这些可连接线程的资源就一直不会释放,相当于变成僵尸线程了,僵尸线程越来越多,以后再想创建新线程将变得没有资源可用。如果不用pthread_join,即使父进程先于可连接子线程退出,也不会泄露资源。如果父进程先于子线程退出,那么它将被init进程所收养,这个时候init进程就是它的父进程,它将调用wait系列函数为其回收资源,因此不会泄露资源。总之,一个可连接的线程所占用的内存仅当有线程对其执行pthread_join后才会释放,因此为了避免内存泄漏,可连接的线程在终止时,要么已被设为DETACHED(可分离),要么使用pthread_join来回收资源。另外,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程将得到错误代码ESRCH。

了解了可连接线程,我们来看可分离的线程,这种线程运行结束时,其资源将立刻被系统回收。可以这样理解,这种线程能独立(分离)出去,可以自生自灭,父线程不用管它了。将一个线程设置为可分离状态有两种方式,一种是调用函数pthread_detach,它可以将线程转换为可分离线程;另一种是在创建线程时就将它设置为可分离状态,基本过程是首先初始化一个线程属性的结构体变量(通过函数pthread_attr_init),然后将其设置为可分离状态(通过函数pthread_attr_setdetachstate),最后将该结构体变量的地址作为参数传入线程创建函数pthread_create,这样所创建出来的线程就直接处于可分离状态。

函数pthread_attr_setdetachstate用来设置线程的分离状态属性,声明如下:

     int pthread_attr_setdetachstate(pthread_attr_t * attr, int detachstate);

其中参数attr是要设置的属性结构体;detachstate是要设置的分离状态值,可以取值PTHREAD_CREATE_DETACHED或PTHREAD_CREATE_JOINABLE。如果函数成功,返回0,否则返回非零错误码。

例3.6 】 创建一个可分离线程。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

     #include <iostream>
     #include <pthread.h>
    
     using namespace std;
    
     void *thfunc(void *arg)
     {
         cout<<("sub thread is running\n");
         return NULL;
     }
    
     int main(int argc, char *argv[])
     {
         pthread_t thread_id;
         pthread_attr_t thread_attr;
         struct sched_param thread_param;
         size_t stack_size;
         int res;
    
         res = pthread_attr_init(&thread_attr);
         if (res)
             cout<<"pthread_attr_init failed:"<<res<<endl;
    
         res = pthread_attr_setdetachstate( &thread_attr,PTHREAD_CREATE_DETACHED);
         if (res)
             cout<<"pthread_attr_setdetachstate failed:"<<res<<endl;
    
         res = pthread_create(   &thread_id,     &thread_attr, thfunc,
             NULL);
         if (res )
             cout<<"pthread_create failed:"<<res<<endl;
         cout<<"main thread will exit\n"<<endl;
    
         sleep(1);
         return 0;
     }

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost test]# g++ -o test test.cpp -lpthread
     [root@localhost test]# ./test
     main thread will exit
    
     sub thread is running
     [root@localhost test]#

在上面代码中,我们首先初始化了一个线程属性结构体,然后设置其分离状态为PTHREAD_CREATE_DETACHED,并用这个属性结构体作为参数传入线程创建函数中。这样创建出来的线程就是可分离线程。这意味着,该线程结束时,它所占用的任何资源都可以立刻被系统回收。程序的最后我们让主线程挂起1秒,让子线程有机会执行。因为如果主线程很早就退出,将会导致整个进程很早退出,子线程就没机会执行了。

如果子线程执行的时间长,则sleep的设置比较麻烦。有一种机制不用sleep函数即可让子线程完整执行。对于可连接线程,主线程可以用pthread_join函数等待子线程结束。而对于可分离线程,并没有这样的函数,但可以采用这样的方法:先让主线程退出而进程不退出,一直等到子线程退出了,进程才退出,即在主线程中调用函数pthread_exit,在主线程如果调用了pthread_exit,那么此时终止的只是主线程,而进程的资源会为由主线程创建的其他线程保持打开的状态,直到其他线程都终止。值得注意的是,如果在非主线程(即其他子线程)中调用pthread_exit则不会有这样的效果,只会退出当前子线程。下面不用sleep函数,重新改写例3.6。

例3.7 】 创建一个可分离线程,且主线程先退出。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost test]# g++ -o test test.cpp -lpthread
     [root@localhost test]# ./test
     main thread will exit
    
     sub thread is running
     [root@localhost test]#

正如我们预料的那样,主线程中调用了函数pthread_exit将退出主线程,但进程并不会在此刻退出,而是要等到子线程结束后才退出。因为是分离线程,它结束的时候,所占用的资源会立刻被系统回收。而如果是一个可连接线程,则必须在创建它的线程中调用pthread_join来等待可连接线程的结束并释放该线程所占的资源。因此上面代码中,如果我们创建的是可连接线程,则函数main中不能调用pthread_exit预先退出。

除了直接创建可分离线程外,还能把一个可连接线程转换为可分离线程。这有个好处,就是我们把线程的分离状态转为可分离后,它自己退出或调用pthread_exit后就可以由系统回收其资源。转换方法是调用函数pthread_detach,该函数可以把一个可连接线程转变为一个可分离的线程,声明如下:

     int pthread_detach(pthread_t thread);

其中参数thread是要设置为分离状态的线程的ID。如果函数成功,返回0,否则返回一个错误码,比如错误码EINVAL表示目标线程不是一个可连接的线程,ESRCH表示该ID的线程没有找到。要注意的是,如果一个线程已经被其他线程连接了,则pthread_detach不会产生作用,并且该线程继续处于可连接状态。同时,如果一个线程成功进行了pthread_detach后,则无法被连接。

下面我们来看一个例子,首先创建一个可连接线程,然后获取其分离状态,再把它转换为可分离线程来获取其分离状态属性。获取分离状态的函数是pthread_attr_getdetachstate,该函数声明如下:

     int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

其中参数attr为属性结构体指针,detachstate返回分离状态。如果函数成功,返回0,否则返回错误码。

例3.8 】 获取线程的分离状态属性。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost Debug]# ./test
     Thread's detachstate attributes:
     Detach state        = PTHREAD_CREATE_JOINABLE

从运行结果可见,默认创建的线程就是一个可连接线程,即其分离状态属性是可连接的。下面我们再看一个例子,把一个可连接线程转换成可分离线程,并查看其前后的分离状态属性。

例3.9 】 把可连接线程转为可分离线程。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost Debug]# ./test
     Detach state        = PTHREAD_CREATE_JOINABLE
     after pthread_detach,
     Detach state        = PTHREAD_CREATE_DETACHED
2.栈尺寸

栈尺寸是线程的一个重要属性。这对于在线程函数中开设栈上的内存空间非常重要。如局部变量、函数参数、返回地址等都存放在栈空间里,而动态分配的内存(比如用malloc)或全局变量等都属于堆空间。在线程函数中开设局部变量(尤其数组)要注意不要超过默认栈尺寸大小。获取线程栈尺寸属性的函数是pthread_attr_getstacksize,声明如下:

     int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

其中参数attr指向属性结构体,stacksize用于获得栈尺寸(单位是字节),它指向size_t类型的变量。如果函数成功,返回0,否则返回错误码。

例3.10 】 获得线程默认栈尺寸大小和最小尺寸。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost Debug]# ./test
     Default stack size is 8392704 byte; minimum is 16384 byte
3.调度策略

调度策略也是线程的一个重要属性。某个线程肯定有一种策略来调度它。进程中有了多个线程后,就要管理这些线程如何去占用CPU,这就是线程调度。线程调度通常由操作系统来安排,不同的操作系统其调度方法(或称调度策略)不同,比如有的操作系统采用轮询法来调度。在理解线程调度之前,先要了解实时与非实时。实时就是指操作系统对一些中断等的响应时效性非常高,非实时则正好相反。目前像VxWorks属于实时操作系统(Real-time Operating System,RTOS),而Windows和Linux则属于非实时操作系统,也叫分时操作系统(Time-sharing Operating System,TSOS)。响应实时的表现主要是抢占,抢占是通过优先级来控制的,优先级高的任务最先占用CPU。

Linux虽然是个非实时操作系统,但其线程也有实时和分时之分,具体的调度策略可以分为3种:SCHED_OTHER(分时调度策略)、SCHED_FIFO(先来先服务调度策略)、SCHED_RR(实时的分时调度策略)。我们创建线程的时候可以指定其调度策略。默认的调度策略是SCHED_OTHER。SCHED_FIFO和SCHED_RR只用于实时线程。

(1)SCHED_OTHER

SCHED_OTHER表示分时调度策略(也可称作轮转策略),是一种非实时调度策略,系统会为每个线程分配一段运行时间,称为时间片。该调度策略是不支持优先级的,如果我们去获取该调度策略下的最高和最低优先级,可以发现都是0。该调度策略有点像在售楼处选房,对每个选房人都预先给定相同的一段时间,前面的人在选房,他不出来,后一个人是不能进去选房的,而且不能强行赶他出来(即不支持优先级,没有VIP特权之说)。

(2)SCHED_FIFO

SCHED_FIFO表示先来先服务调度策略,是一种实时调度策略,支持优先级抢占(真实支持优先级,因此可以算一种实时调度策略)。在SCHED_FIFO策略下,CPU让一个先来的线程执行完再调度下一个线程,顺序就是按照创建线程的先后。线程一旦占用CPU则一直运行,直到有更高优先级任务到达或自己放弃CPU。如果有和正在运行的线程具有同样优先级的线程已经就绪,则必须等待正在运行的线程主动放弃后才可以运行这个就绪的线程。在SCHED_FIFO策略下,可设置的优先级的范围是1~99。

(3)SHCED_RR

SHCED_RR表示时间片轮转(轮询)调度策略,但支持优先级抢占,因此也是一种实时调度策略。SHCED_RR策略下,CPU会分配给每个线程一个特定的时间片,当线程的时间片用完,系统将重新分配时间片,并将线程置于实时线程就绪队列的尾部,这样保证了所有具有相同优先级的线程能够被公平地调度。

下面我们来看个例子,获取这3种调度策略下可设置的最低和最高优先级。主要使用的函数是sched_get_priority_min和sched_get_priority_max,这两个函数都在sched.h中声明,其声明如下:

     int sched_get_priority_min(int policy);
     int sched_get_priority_max(int policy);

该函数获取实时线程可设置的最低和最高优先级值。其中参数policy为调度策略,可以取值为SCHED_FIFO、SCHED_RR或SCHED_OTHER。函数返回可设置的最低和最高优先级。对于SCHED_OTHER,由于是分时策略,因此返回0;另外两个策略,返回最低优先级是1,最高优先级是99。

例3.11 】 获取线程3种调度策略下可设置的最低和最高优先级。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost Debug]# ./test
     Valid priority range for SCHED_OTHER: 0 - 0
     Valid priority range for SCHED_FIFO: 1 - 99
     Valid priority range for SCHED_RR: 1 - 99

对于SCHED_FIFO和SHCED_RR调度策略,由于支持优先级抢占,因此具有高优先级的可运行的(就绪状态下的)线程总是先运行。并且,一个正在运行的线程在未完成其时间片时,如果出现一个更高优先级的线程就绪,正在运行的这个线程就可能在未完成其时间片前被抢占,甚至一个线程会在未开始其时间片前就被抢占了,而要等待下一次被选择运行。当Linux系统进行切换线程的时候,将执行一个上下文转换的操作,即保存正在运行的线程的相关状态,装载另一个线程的状态,开始新线程的执行。

需要说明的是,虽然Linux支持实时调度策略(比如SCHED_FIFO和SCHED_RR),但它依旧属于非实时操作系统,这是因为实时操作系统对响应时间有着非常严格的要求,而Linux作为一个通用操作系统达不到这一要求(通用操作系统要求能支持一些较差的硬件,从硬件角度来看达不到实时要求),此外Linux的线程优先级是动态的,也就是说即使高优先级线程还没有完成,低优先级的线程还是会得到一定的时间片。USA的宇宙飞船常用的操作系统VxWorks就是一个RTOS(实时操作系统)。

3.3.3 线程的结束

线程安全退出是编写多线程程序时的一个重要部分。Linux下,线程的结束通常有以下几种方法:

(1)在线程函数中调用函数pthread_exit。

(2)线程所属的进程结束了,比如进程调用了exit。

(3)线程函数执行结束后(return)返回了。

(4)线程被同一进程中的其他线程通知结束或取消。

和Windows下的线程退出函数ExitThread不同,方法(1)中的pthread_exit不会导致C++对象被析构,所以可以放心使用;方法(2)最好不用,因为线程函数如果有C++对象,则C++对象不会被销毁;方法(3)推荐使用,线程函数执行到return后结束,是最安全的方式,应该尽量将线程设计成这样的形式,即想让线程终止运行时,它们就能够return(返回);方法(4)通常用于其他线程要求目标线程结束运行的情况,比如目标线程正执行一个耗时的复杂科学计算,但用户等不及了想中途停止它,此时就可以向目标线程发送取消信号。其实,方法(1)和(3)属于线程自己主动终止;方法(2)和(4)属于被动结束,就是自己并不想结束,但外部线程希望自己终止。

一般情况下,进程中各个线程的运行是相互独立的,线程的终止并不会相互通知,也不会影响其他的线程。对于可连接线程,它终止后所占用的资源并不会随着线程的终止而归还系统,而是仍为线程所在的进程持有,可以调用函数pthread_join来同步并释放资源。

1.线程主动结束

线程主动结束,一般就是在线程函数中使用return语句或调用函数pthread_exit。函数pthread_exit声明如下:

     void pthread_exit(void *retval);

其中参数retval就是线程退出的时候返回给主线程的值。注意线程函数的返回类型是void*;在主线程中调用pthread_exit(NULL);的时候,将结束主线程,但进程并不会立即退出。

下面来看个线程主动结束的例子。

例3.12 】 线程终止并得到线程的退出码。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost Debug]# ./test
     get thread 0 exitcode: 1
     get thread 1 exitcode: 2

从这个例子可以看到,线程返回值有两种方式,一种是调用函数pthread_exit,另一种是直接return。这个例子中,用了不少强制转换,首先看函数thrfunc1中的最后一句pthread_exit((void*)(&count));,我们知道函数pthread_exit的参数的类型为void *,因此只能通过指针的形式出去,故先把整型变量count转换为整型指针,即&count,那么&count为int*类型,这个时候再与void*匹配,需要进行强制转换,也就是代码中的(void*)(&count);。函数thrfunc2中的return这个关键字进行返回值的时候,同样也是需要进行强制类型的转换,线程函数的返回类型是void*,那么对于count这个整型变量来说,必须转换为void型的指针类型(即void*),因此有(void*)((int*)&count);。

对于接收返回值的函数pthread_join来说,有两个作用,其一就是等待线程结束,其二就是获取线程结束时的返回值。pthread_join的第二个参数类型是void**二级指针,那我们就把整型指针pRet1的地址,即int**类型赋给它,再显式地转为void**即可。

再要注意一点,返回整数数值的时候使用到了static这个关键字,这是因为必须确定返回值的地址是不变的。如果不用static,则对于count变量而言,在内存上来讲,属于在栈区开辟的变量,那么在调用结束的时候,必然是释放内存空间的,就没办法找到count所代表内容的地址空间。这就是为什么很多人在看到swap交换函数的时候,写成swap(int,int)是没有办法进行交换的,所以,如果我们需要修改传过来的参数的话,必须要使用这个参数的地址,或者是一个变量本身是不变的内存地址空间,才可以进行修改,否则,修改失败或者返回值是随机值。而把返回值定义成静态变量,这样线程结束,其存储单元依然存在,这样做在主线程中可以通过指针引用到它的值,并打印出来。若用静态变量,结果必将不同。读者可以试着返回一个字符串,这样就比返回一个整数更加简单明了。

2.线程被动结束

某个线程在执行一项耗时的计算任务时,用户可能没耐心等待,希望结束该线程。此时线程就要被动地结束了。一种方法是可以在同进程的另外一个线程中通过函数pthread_kill发送信号给要结束的线程,目标线程收到信号后再退出;另外一种方法是在同进程的其他线程中通过函数pthread_cancel来取消目标线程的执行。我们先来看看pthread_kill,向线程发送信号的函数是pthread_kill,注意它不是杀死(kill)线程,是向线程发信号,因此线程之间交流信息可以用这个函数,要注意的是接收信号的线程必须先用函数sigaction注册该信号的处理函数。函数pthread_kill声明如下:

     int pthread_kill(pthread_t threadId, int signal);

其中参数threadId是接收信号的线程ID;signal是信号,通常是一个大于0的值,如果等于0,则用来探测线程是否存在。如果函数成功,返回0,否则返回错误码,如ESRCH表示线程不存在,EINVAL表示信号不合法。

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

例3.13 】 向线程发送请求结束信号。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

     #include <iostream>
     #include <pthread.h>
     #include <signal.h>
     #include <unistd.h>                                 //sleep
     using namespace std;
    
     static void on_signal_term(int sig)                 //信号处理函数
     {
         cout << "sub thread will exit" << endl;
         pthread_exit(NULL);
     }
     void *thfunc(void *arg)
     {
          signal(SIGQUIT, on_signal_term);               //注册信号处理函数
    
         int tm = 50;
         while (true)                                    //死循环,模拟一个长时间计算任务
         {
             cout << "thrfunc--left:"<<tm<<" s--" <<endl;
             sleep(1);
             tm--;                                       //每过一秒,tm就减1
         }
    
         return (void *)0;
     }
    
     int main(int argc, char *argv[])
     {
         pthread_t     pid;
         int res;
    
         res = pthread_create(&pid, NULL, thfunc, NULL);  //创建子线程
         sleep(5);                                                 //让出CPU 5秒,让子线程执行
         pthread_kill(pid, SIGQUIT);//5秒结束后,开始向子线程发送SIGQUIT信号,通知其结束
         pthread_join(pid, NULL);                                  //等待子线程结束
          cout << "sub thread has completed,main thread will exit\n";
         return 0;
     }

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost cpp98]# ./test
     thrfunc--left:50 s--
     thrfunc--left:49 s--
     thrfunc--left:48 s--
     thrfunc--left:47 s--
     thrfunc--left:46 s--
     sub thread will exit
     sub thread has completed,main thread will exit

可以看到,子线程在执行的时候,主线程等了5秒后就开始向其发送信号SIGQUIT。在子线程中已经注册了SIGQUIT的处理函数on_signal_term。如果不注册信号SIGQUIT的处理函数,则将调用默认处理,即结束线程所属的进程。读者可以试试把signal(SIGQUIT, on_signal_term);注释掉,再运行一下可以发现子线程在运行5秒后,整个进程结束了。pthread_kill(pid, SIGQUIT);后面的语句不会再执行。

pthread_kill还有一种常见的应用,即判断线程是否还存活,方法是发送信号0,这是一个保留信号,然后判断其返回值,根据返回值就可以知道目标线程是否还存活着。

例3.14 】 判断线程是否已经结束。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

     #include <iostream>
     #include <pthread.h>
     #include <signal.h>
     #include <unistd.h>              //sleep
     #include "errno.h"               //for ESRCH
     using namespace std;
    
     void *thfunc(void *arg)          //线程函数
     {
         int tm = 50;
         while (1)                    //如果要线程停止,这里可以改为tm>48或其他
         {
             cout << "thrfunc--left:"<<tm<<" s--" <<endl;
             sleep(1);
             tm--;
         }
         return (void *)0;
     }
    
     int main(int argc, char *argv[])
     {
         pthread_t     pid;
         int res;
    
         res = pthread_create(&pid, NULL, thfunc, NULL); //创建线程
         sleep(5);
         int kill_rc = pthread_kill(pid, 0);                      //发送信号0,探测线程是否存活
     //打印探测结果
         if (kill_rc == ESRCH)
             cout<<"the specified thread did not exists or already quit\n";
         else if (kill_rc == EINVAL)
             cout<<"signal is invalid\n";
         else
             cout<<"the specified thread is alive\n";
    
        return 0;
     }

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost cpp98]# g++ -o test test.cpp -lpthread
     [root@localhost cpp98]# ./test
     thrfunc--left:50 s--
     thrfunc--left:49 s--
     thrfunc--left:48 s--
     thrfunc--left:47 s--
     thrfunc--left:46 s--
     the specified thread is alive

上面例子中主线程休眠5秒后,探测子线程是否存活,结果是活着,因为子线程一直在死循环。如果要让探测结果为子线程不存在了,可以把死循环改为一个可以跳出循环的条件,比如while(tm>48)。

除了通过函数pthread_kill发送信号来通知线程结束外,还可以通过函数pthread_cancel来取消某个线程的执行,所谓取消某个线程的执行,也是发送取消请求,请求其终止运行。要注意,就算发送成功也不一定意味着线程停止运行了。函数pthread_cancel声明如下:

     int pthread_cancel(pthread_t thread);

其中参数thread表示要被取消线程(目标线程)的线程ID。如果发送取消请求成功则函数返回0,否则返回错误码。发送取消请求成功并不意味着目标线程立即停止运行,即系统并不会马上关闭被取消的线程,只有在被取消的线程下次调用一些系统函数或C库函数(比如printf),或者调用函数pthread_testcancel(让内核去检测是否需要取消当前线程)时,才会真正结束线程。这种在线程执行过程中,检测是否有未响应取消信号的地方,叫取消点。常见的取消点在有printf、pthread_testcancel、read/write、sleep等函数调用的地方。如果被取消线程成功停止运行,将自动返回常数PTHREAD_CANCELED(这个值是-1),可以通过pthread_join获得这个退出值。

函数pthread_testcancel让内核去检测是否需要取消当前线程,声明如下:

     void pthread_testcancel(void);

pthread_testcancel函数可以在线程的死循环中让系统(内核)有机会去检查是否有取消请求过来,如果不调用pthread_testcancel,则函数pthread_cancel取消不了目标线程。我们可以来看下面两个例子,第一个例子不调用函数pthread_testcancel,则无法取消目标线程;第二个例子调用了函数pthread_testcancel,取消成功了,即取消请求不但发送成功了,而且目标线程停止运行了。

例3.15 】 取消线程失败。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost cpp98]# ./test
     thread start--------
     ^C
     [root@localhost cpp98]#

从运行结果可以看到,程序打印thread start--------后就没反应了,只能按快捷键Ctrl+C来停止进程,这说明在主线程中虽然发送取消请求了,但并没有让子线程停止运行,因为如果停止运行,pthread_join是会返回并打印其后面的语句的。下面我们来改进这个程序,在while循环中加一个函数pthread_testcancel。

例3.16 】 取消线程成功。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost cpp98]# g++ -o test test.cpp -lpthread
     [root@localhost cpp98]# ./test
     thread start--------
     thread has stopped,and exit code: -1

可以看到,这个例子取消线程成功,目标线程停止运行,pthread_join返回,并且得到的线程返回值正是PTHREAD_CANCELED。原因是在while死循环中添加了函数pthread_testcancel,让系统每次循环都去检查下有没有取消请求。如果不用pthread_testcancel,则可以在while循环中用sleep函数来代替,但这样会影响while的速度,在实际开发中,可以根据具体项目具体分析。

3.3.4 线程退出时的清理机会

主动结束可以认为是线程正常终止,这种方式是可预见的;被动结束是其他线程要求其结束,这种退出方式是不可预见的,是一种异常终止。不论是可预见的线程终止还是异常终止,都会存在资源释放的问题。在不考虑因运行出错而退出的前提下,如何保证线程终止时能顺利地释放掉自己所占用的资源,特别是锁资源,就是一个必须考虑的问题。最经常出现的情形是资源独占锁的使用:线程为了访问临界资源而为其加上锁,但在访问过程中被外界取消,如果取消成功了,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程,也就是需要一个在线程退出时执行清理的机会。关于锁后面会讲到,这里只需要知道谁上了锁,谁就要负责解锁,否则会引起程序死锁。我们来看一个场景。

比如线程1执行这样一段代码:

     void *thread1(void *arg)
     {
     pthread_mutex_lock(&mutex);  //上锁
     //调用某个阻塞函数,比如套接字的accept,该函数等待客户连接
     sock = accept(...);
     pthread_mutex_unlock(&mutex);
     }

在这个例子中,如果线程1执行accept时,线程会阻塞(也就是等在那里,有客户端连接的时候才返回,或者出现其他故障),在线程1等待时,线程2想关掉线程1,于是调用pthread_cancel或者类似函数,请求线程1立即退出。这时候线程1仍然在accept等待中,当它收到线程2的cancel信号后,就会从accept中退出,终止线程,但注意这个时候线程1还没有执行解锁函数pthread_mutex_unlock(&mutex);,即锁资源没有释放,造成其他线程的死锁问题,也就是其他在等待这个锁资源的线程将永远等不到了。所以必须在线程接收到cancel后用一种方法来保证异常退出(也就是线程没达到终点)时可以做清理工作(主要是解锁方面)。

POSIX线程库提供了函数pthread_cleanup_push和pthread_cleanup_pop,让线程退出时可以做一些清理工作。这两个函数采用先入后出的栈结构管理,前者会把一个函数压入清理函数栈,后者用来弹出栈顶的清理函数,并根据参数来决定是否执行清理函数。多次调用函数pthread_cleanup_push将把当前在栈顶的清理函数往下压,弹出清理函数时,在栈顶的清理函数先被弹出。综上所述,栈的特点是,先进后出。函数pthread_cleanup_push声明如下:

     void pthread_cleanup_push(void (*routine)(void *), void *arg);

其中参数routine是一个函数指针,arg是该函数的参数。由pthread_cleanup_push压栈的清理函数在下面三种情况下会执行:

(1)线程主动结束时,比如return或调用pthread_exit时。

(2)调用函数pthread_cleanup_pop,且其参数为非0时。

(3)线程被其他线程取消时,也就是有其他线程对该线程调用pthread_cancel函数。

函数pthread_cleanup_pop声明如下:

     void pthread_cleanup_pop(int execute);

其中参数execute用来决定在弹出栈顶清理函数的同时,是否执行清理函数,取0时表示不执行清理函数,非0时则执行清理函数。需要注意的是,函数pthread_cleanup_pop与pthread_cleanup_push必须成对出现在同一个函数中,否则就是语法错误。

了解了这两个函数,我们把上面可能会引起死锁的线程1的代码这样改写:

     void *thread1(void *arg)
     {
     pthread_cleanup_push(clean_func,...)   //压栈一个清理函数 clean_func
     pthread_mutex_lock(&mutex);                    //上锁
     //调用某个阻塞函数,比如套接字的accept,该函数等待客户连接
     sock = accept(...);
    
     pthread_mutex_unlock(&mutex);                  //解锁
     pthread_cleanup_pop(0);                        //弹出清理函数,但不执行,因为参数是0
     return NULL;
     }

在上面的代码中,如果accept被其他线程取消后线程退出,会自动调用函数clean_func,在这个函数中可以释放锁资源。如果accept没有被取消,那么线程继续执行,当执行到pthread_mutex_unlock(&mutex);时,表示线程正确地释放资源了,再执行到pthread_cleanup_pop(0);会把前面压栈的清理函数clean_func弹出栈,并且不会去执行它(因为参数是0)。现在的流程就安全了。

例3.17 】 线程主动结束时调用清理函数。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

     #include <stdio.h>
     #include <stdlib.h>
     #include <pthread.h>
     #include <string.h>                                    //strerror
    
     void mycleanfunc(void *arg)                            //清理函数
     {
        printf("mycleanfunc:%d\n", *((int *)arg)); //打印传进来的不同参数
     }
     void *thfrunc1(void *arg)
     {
        int m=1;
        printf("thfrunc1 comes \n");
        pthread_cleanup_push(mycleanfunc, &m);              //把清理函数压栈
        return (void *)0;                                   //退出线程
        pthread_cleanup_pop(0);    //把清理函数出栈,这句不会执行,但必须有,否则编译不过
     }
    
     void *thfrunc2(void *arg)
     {
        int m = 2;
        printf("thfrunc2 comes \n");
        pthread_cleanup_push(mycleanfunc, &m);              //把清理函数压栈
        pthread_exit(0);                                    //退出线程
        pthread_cleanup_pop(0); //把清理函数出栈,这句不会执行,但必须有,否则编译不过
     }
    
     int main(void)
     {
        pthread_t pid1,pid2;
        int res;
        res = pthread_create(&pid1, NULL, thfrunc1, NULL);  //创建线程1
        if (res)
        {
            printf("pthread_create failed: %d\n", strerror(res));
            exit(1);
        }
        pthread_join(pid1, NULL);                                     //等待线程1结束
    
        res = pthread_create(&pid2, NULL, thfrunc2, NULL);  //创建线程2
        if (res)
        {
            printf("pthread_create failed: %d\n", strerror(res));
            exit(1);
        }
        pthread_join(pid2, NULL);                                     //等待线程2结束
    
        printf("main over\n");
        return 0;
     }

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost cpp98]# g++ -o test test.cpp -lpthread
     [root@localhost cpp98]# ./test
     thfrunc1 comes
     mycleanfunc:1
     thfrunc2 comes
     mycleanfunc:2
     main over

从此例中可以看到,无论return或pthread_exit都会引起清理函数的执行。值得注意的是,pthread_cleanup_pop必须和pthread_cleanup_push成对出现在同一个函数中,否则编译不过,读者可以把pthread_cleanup_pop注释掉后再编译试试。这个例子是线程主动调用清理函数,下面我们再看由pthread_cleanup_pop执行清理函数的情况。

例3.18 】 pthread_cleanup_pop调用清理函数。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

     #include <stdio.h>
     #include <stdlib.h>
     #include <pthread.h>
     #include <string.h> //strerror
    
     void mycleanfunc(void *arg)                    //清理函数
     {
        printf("mycleanfunc:%d\n", *((int *)arg));
     }
     void *thfrunc1(void *arg)                               //线程函数
     {
         int m=1,n=2;
         printf("thfrunc1 comes \n");
         pthread_cleanup_push(mycleanfunc, &m);              //把清理函数压栈
         pthread_cleanup_push(mycleanfunc, &n);              //再压一个清理函数压栈
         pthread_cleanup_pop(1);                             //出栈清理函数,并执行
         pthread_exit(0);                                    //退出线程
         pthread_cleanup_pop(0);                             //不会执行,仅仅为了成对
     }
    
     int main(void)
     {
         pthread_t pid1 ;
         int res;
         res = pthread_create(&pid1, NULL, thfrunc1, NULL);                //创建线程
         if (res)
         {
             printf("pthread_create failed: %d\n", strerror(res));
             exit(1);
         }
         pthread_join(pid1, NULL);                                         //等待线程结束
    
         printf("main over\n");
         return 0;
     }

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost cpp98]# g++ -o test test.cpp -lpthread
     [root@localhost cpp98]# ./test
     thfrunc1 comes
     mycleanfunc:2
     mycleanfunc:1
     main over

从此例中可以看出,我们连续压了两次清理函数入栈,第一次压栈的清理函数就到栈底,第二次压栈的清理函数就到了栈顶,出栈的时候应该是第二次压栈的清理函数先执行,因此pthread_cleanup_pop(1);执行的是传n进去的清理函数,输出的整数值是2。pthread_exit退出线程时,引发执行的清理函数是传m进去的清理函数,输出的整数值是1。下面介绍最后一种情况,线程被取消时引发清理函数。

例3.19 】 取消线程时引发清理函数。

(1)打开UE,新建一个test.cpp文件,在test.cpp中输入代码如下:

(2)上传test.cpp到Linux,在终端下输入命令:g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

     [root@localhost cpp98]# g++ -o test test.cpp -lpthread
     [root@localhost cpp98]# ./test
     i=2
     i=3
     i=4
     ...
     i=24383
     i=24384
     i=24385
     i=24386
     i=24387
     i=24388
     i=24389i=24389
     mycleanfunc:24389
     thread has stopped,and exit code: -1

从这个例子可以看出,子线程在循环打印i的值,直到被取消。由于循环里有系统调用printf,因此取消成功时,将会执行清理函数,在清理函数中打印的i值,将是执行很多次i++后的i值,这是因为我们压栈清理函数的时候,传给清理函数的是i的地址,而执行清理函数的时候,i的值已经变了,因此打印的是最新的i值。 EtxiV1LQ2n6UcY0GZIaiSbbWRLxYGWRYHA3LHx4DPQiBhFIPhWE2YB2NVVlP/BU8

点击中间区域
呼出菜单
上一章
目录
下一章
×