【图书推荐】《Linux C与C++一线开发实践(第2版)》_linux c与c++一线开发实践pdf-CSDN博客
《Linux C与C++一线开发实践(第2版)(Linux技术丛书)》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)
线程安全退出是编写多线程程序时一个重要的事项。在Linux下,线程的结束通常由以下原因所致:
(1)在线程函数中调用pthread_exit函数。
(2)线程所属的进程结束了,比如进程调用了exit。
(3)线程函数执行结束后返回(return)了。
(4)线程被同一进程中的其他线程通知结束或取消。
第1种方式,与Windows下的线程退出函数ExitThread不同,pthread_exit不会导致C++对象被析构,所以可以放心使用。第2种方式最好不用,因为线程函数如果有C++对象,则C++对象不会被销毁。第3种方式推荐使用,线程函数执行到return后结束是最安全的方式,尽量将线程设计成这样的形式。第4种方式通常用于其他线程要求目标线程结束运行的情况,比如目标线程中执行一个耗时的复杂科学计算,但用户等不及想中途停止它,此时就可以向目标线程发送取消信号。其实,(1)和(3)属于线程自己主动终止,(2)和(4)属于被动终止,就是自己并不想终止,但外部线程希望自己终止。
一般情况下,进程中各个线程的运行是相互独立的,线程的终止并不会相互通知,也不会影响其他的线程。对于可连接线程,它终止后,所占用的资源并不会随着线程的终止而归还系统,而是仍为线程所在的进程持有,可以调用pthread_join函数来同步并释放资源(这一点前面已经讲过了,这里又讲一遍,希望读者能记住)。
1. 线程主动终止
线程主动终止一般是指线程函数中使用了return语句或调用了pthread_exit函数。函数pthread_exit声明如下:
void pthread_exit(void *retval);
其中,参数retval就是线程退出的时候返回给主线程的值。注意,线程函数的返回类型是void*。另外,在main线程中调用“pthread_exit(NULL);”的时候,将结束main线程,但进程并不立即退出。
下面来看一个线程主动终止的例子。
【例8.12】线程终止并得到线程的退出码
(1)打开Visual Studio Code,新建一个test.cpp文件,在test.cpp中输入代码:
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define PTHREAD_NUM 2
void *thrfunc1(void *arg) // 第一个线程函数
{
static int count = 1; // 这里需要的是静态变量
pthread_exit((void*)(&count)); // 通过pthread_exit结束线程
}
void *thrfunc2(void *arg)
{
static int count = 2;
return (void *)(&count); // 线程函数返回
}
int main(int argc, char *argv[])
{
pthread_t pid[PTHREAD_NUM]; // 定义两个线程id
int retPid;
int *pRet1; // 注意这里是指针
int * pRet2;
if ((retPid = pthread_create(&pid[0], NULL, thrfunc1, NULL)) != 0)
// 创建第1个线程
{
perror("create pid first failed");
return -1;
}
if ((retPid = pthread_create(&pid[1], NULL, thrfunc2, NULL)) != 0)
// 创建第2个线程
{
perror("create pid second failed");
return -1;
}
if (pid[0] != 0)
{
pthread_join(pid[0], (void**)& pRet1); // 注意pthread_join的第2个参数的用法
printf("get thread 0 exitcode: %d\n", * pRet1); // 打印线程返回值
}
if (pid[1] != 0)
{
pthread_join(pid[1], (void**)& pRet2);
printf("get thread 1 exitcode: %d\n", * pRet2); // 打印线程返回值
}
return 0;
}
(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的第2个参数类型是void**二级指针,那么我们就把整型指针pRet1的地址(int**类型)赋给它,再显式地转换为void**即可。
要注意一点,返回整数数值的时候使用了static关键字,这是因为必须确定返回值的地址是不变的。如果不用static,则对于count变量而言,以内存上来讲,属于在栈区开辟的变量,那么在调用结束的时候,必然是释放内存空间的,相对而言,这时候就没办法找到count所代表内容的地址空间。这就是为什么很多人在看到swap交换函数的时候,写成swap(int,int)是没有办法进行交换的。因此,如果我们需要修改传过来的参数,就必须使用这个参数的地址,或者一个变量本身是不变的内存地址空间,这样才可以进行修改,否则修改失败或者返回值是随机值。而把返回值定义成静态变量,这样线程结束时,其存储单元依然存在,这样做main线程中可以通过指针引用到它的值,并打印出来。读者可以试试不用静态变量,结果必将不同。还可以试着返回一个字符串,这样比返回一个整数更加简单明了。
2. 线程被动终止
一个线程可能在执行一项耗时的计算任务,用户可能没耐心等待,希望结束该线程。此时线程就要被动终止了。如何被动终止呢?一种方法是在同进程的另一个线程中通过函数pthread_kill发送信号给要终止的线程,目标线程收到信号后再退出。另一种方法是在同进程的其他线程中通过函数pthread_cancel来取消目标线程的执行。我们先来看看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 signal的参数不是0,那么一定要清楚到底要干什么,而且一定要实现线程的信号处理函数,否则就会影响整个进程。
【例8.13】向线程发送请求结束信号
(1)打开Visual Studio Code,新建一个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--; // 每过1秒,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(一个保留信号),然后判断其返回值,根据返回值就可以知道目标线程是否还存活着。请看下例。
【例8.14】判断线程是否已经结束
(1)打开Visual Studio Code,新建一个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,取消成功。取消成功的意思是不但取消请求发送成功,而且目标线程停止运行了。
【例8.15】取消线程失败
(1)打开Visual Studio Code,新建一个test.cpp文件,在test.cpp中输入代码:
#include<stdio.h>
#include<stdlib.h>
#include <pthread.h>
#include <unistd.h> // sleep
void *thfunc(void *arg)
{
int i = 1;
printf("thread start-------- \n");
while (1) // 死循环
i++;
return (void *)0;
}
int main()
{
void *ret = NULL;
int iret = 0;
pthread_t tid;
pthread_create(&tid, NULL, thfunc, NULL); // 创建线程
sleep(1);
pthread_cancel(tid); // 发送取消线程的请求
pthread_join(tid, &ret); // 等待线程结束
if (ret == PTHREAD_CANCELED) // 判断是否成功取消线程
printf("thread has stopped,and exit code: %d\n", ret);
// 打印返回值,应该是-1
else
printf("some error occured");
return 0;
}
(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。
【例8.16】取消线程成功
(1)打开Visual Studio Code,新建一个test.cpp文件,在test.cpp中输入代码:
#include<stdio.h>
#include<stdlib.h>
#include <pthread.h>
#include <unistd.h> // sleep
void *thfunc(void *arg)
{
int i = 1;
printf("thread start-------- \n");
while (1)
{
i++;
pthread_testcancel(); // 让系统测试取消请求
}
return (void *)0;
}
int main()
{
void *ret = NULL;
int iret = 0;
pthread_t tid;
pthread_create(&tid, NULL, thfunc, NULL); // 创建线程
sleep(1);
pthread_cancel(tid); // 发送取消线程的请求
pthread_join(tid, &ret); // 等待线程结束
if (ret == PTHREAD_CANCELED) // 判断是否成功取消线程
printf("thread has stopped,and exit code: %d\n", ret);
// 打印返回值,应该是-1
else
printf("some error occured");
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
thread start--------
thread has stopped,and exit code: -1
可以看到,这个例子取消线程成功了,目标线程停止运行,返回pthread_join,并且得到的线程返回值正是PTHREAD_CANCELED。原因就在于我们在while死循环中添加了函数pthread_testcancel,让系统每次循环都去检查一下有没有取消请求。不使用pthread_testcancel也行,可以在while循环中用sleep函数来代替,但这样会影响while的速度。在实际开发中可以根据具体项目具体分析。