文章目录
本课题环境:ubuntu16、glibc 2.23
一、线程
概念:线程是操作系统能够调度和执行的基本单位,在Linux中也被称之为轻量级进程
线程操作的基本函数如下:
POSIX函数 | 作用 |
---|---|
pthread_create | 创建一个POSIX线程 |
pthread_self | 获取该线程ID |
pthread_equal | 判断线程ID相等 |
pthread_detach | 设置线程为分离状态、即使它自动释放 |
pthread_join | 等待线程完成 |
pthread_cancel | 取消线程 |
pthread_exit | 结束线程 |
pthread_kill | 向线程发送信号 |
线程定义在pthread.h文件中,那么要创建线程就要进行引用预编译,线程创建函数定义如下:
#include <pthread.h>
/*
参数一:线程标识指针
参数二:线程属性(一般可使用默认值NULL)
参数三:线程体的开始地址(线程要执行的代码段)、一经创建即执行
参数四:线程体的函数参数(可使用默认NULL)
返回值:创建成功为0,反之不为0
*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
与之对应的终止线程函数如下:
#include <pthread.h>
void pthread_exit(void *retval);
函数在正常情况下是用于显式的退出线程,在main()结束前调用pthread_exit()那么子线程就不会受到main函数结束的影响,反之,在main函数结束时子线程将会被自动终止。实例如下:
#include<iostream>
#include<unistd.h>
#include<pthread.h> //必须要的头文件
using namespace std;
#define NUM_THREADS 7 //定义子线程个数
void * info(void * arg){ //线程体
sleep(2);
cout <<"info ->" << arg <<"\n";
}
int main(){
pthread_t tids[NUM_THREADS]; //定义线程id空间
int index[NUM_THREADS];
for (int i =0 ; i< NUM_THREADS ; ++i){
cout<<"main()\n";
// 线程id、线程参数、线程体开始地址、传入的函数参数
index[i] = i;
int ret = pthread_create(&tids[i],NULL,info,(void*)&index[i]);
cout << hex <<"tids[i] -> 0x" << tids[i] << "\n";
if(ret!=0) {
cout<<"thread return -> "<< ret <<"idx ->"<< i <<"\n"; //返回0则表示创建线程
成功
break;
}
}
pthread_exit(NULL);
}
显示调用pthread_exit后的运行结果:
注释pthread_exit后的运行结果:
向子线程传递多个参数,则可通过结构体访问地址实现
#include<iostream>
#include<unistd.h>
#include<pthread.h> //必须要的头文件
using namespace std;
#define NUM_THREADS 7 //定义子线程个数
struct Dog {
int id;
char* data;
}; //用于传递信息的结构
void * info(void * arg){ //线程体
struct Dog * dog;
dog = (struct Dog*) arg;
cout <<"info -> " << dog->id <<"\n";
cout <<"info -> " << dog->data <<"\n";
pthread_exit(NULL);
}
int main(){
pthread_t tids[NUM_THREADS]; //定义线程id空间
int index[NUM_THREADS];
struct Dog dog[NUM_THREADS];
for (int i =0 ; i< NUM_THREADS ; ++i){
cout<<"main()\n";
// 线程id、线程参数、线程体开始地址、传入的函数参数
//index[i] = i;
//int ret = pthread_create(&tids[i],NULL,info,(void*)index[i]);
dog[i].id = i;
dog[i].data = "this Message";
int ret = pthread_create(&tids[i],NULL,info,(void*)&dog[i]); //传递结构体地址
cout << hex <<"tids[i] -> 0x" << tids[i] << "\n";
if(ret!=0) {
cout<<"thread return -> "<< ret <<"idx ->"<< i <<"\n"; //返回0则表示创建线程成功
break;
}
}
pthread_exit(NULL);
}
运行结果如图:
二、线程分离回收
线程状态可分为joinable、unjoinable状态,默认状态情况下是非分离状态。
子线程创建时会从父线程中copy栈内存充当子线程使用资源。当main线程终止时,它的子线程也会终止(上面的例子有阐述),当线程终止后子线程所占用的内存资源都没有被释放,就会导致资源浪费问题,那么就需要对线程进行回收资源。
回收资源:
具体使用通过 【设置分离属性 】和 【 pthread_join()】 两种方法来处理:
默认情况下线程属性是非分析状态,那么就可以使用pthread_join()函数等待线程显示的结束线程,然后分离后的线程再退出时系统会处理回收资源不需要人为回收资源,函数定义如下:
1. 使用pthread_join()函数进行回收
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
/*
前提:使用pthread_join()的线程必须是非分离的,
作用:在函数所指定的线程中等待线程终止
参数一:线程分配ID
参数二:可为NULL,不为NULL时可用于保存线程返回值
返回值:成功->0 失败->errorId
*/
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdlib>
using namespace std;
void * info (void *arg){
sleep(1);
pthread_exit(NULL); //手动退出线程 、设置返回状态
}
int main(){
pthread_attr_t attr;
pthread_t thread_id;
pthread_create(&thread_id,NULL,info,NULL);
cout<<"status -> " << status << "\n";
pthread_join(thread_id,NULL); //mian线程暂停等待子线程完成
cout<<"status -> " << status << "\n";
cout<<"main() ---------------------------------end "<< "\n";
pthread_exit(NULL); //不影响子线程
}
此程序就是使用pthread_join()函数对子线程进行等待运行结果:
注释pthread_join()函数后运行结果:
从上面运行结果可知,如果子线程在做一个网络请求时,那么就会影响到主线程的任务执行,从而导致整个程序卡顿。
2. 使用线程分离属性使系统自动回收:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdlib>
#include <errno.h>
using namespace std;
void * info (void *arg){
sleep(1);
int data = 10;
pthread_exit((void * )data); //手动退出线程 、设置返回状态
}
int main(){
pthread_attr_t attr;
pthread_attr_init(&attr); //初始化属性
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); //设置属性为可分离
int status;
pthread_t thread_id;
pthread_create(&thread_id,&attr,info,NULL); //设置线程属性
cout<< "join id -> "<< pthread_join(thread_id,NULL) << "\n"; //这里看下pthread_join()的返回值
pthread_attr_destroy(&attr); //销毁属性
cout<<"main() ---------------------------------end "<< "\n";
pthread_exit(NULL); //不影响子线程
}
运行效果:
此时具有可分离属性的线程使用pthread_join()函数并没有执行成功返回了错误代码22而22在errno.h
文件中表示:EINVAL线程不是可连接的线程,从侧面验证了这个线程成功的被设置为了可分离线程,那么它则会被系统自动回收资源。上述代码是在线程创建时对其进行设值,那么同样也可以在线程创建之后进行设置为分离状态使用如下方法:
#include <pthread.h>
int pthread_detach(pthread_t thread);//传入线程ID
获取线程返回码
当一个子线程在完成相对应的任务时,比如发送一个网络请求,当请求成功时想要通过返回对应的状态码告诉用户请求得到成功回应然后再进行相对应的处理,那么就可以通过pthread_join()函数的参数二进行一个线程标识传递,当等待子线程完成时收集子线程的返回码,调用实例:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdlib>
using namespace std;
#define NUM_THREADS 5
void * info (void *arg){
sleep(1);
int data = 10;
pthread_exit((void * )data); //手动退出线程 、设置返回状态
}
int main(){
pthread_attr_t attr;
int status;
pthread_t thread_id;
pthread_create(&thread_id,NULL,info,NULL);
cout<<"status -> " << status << "\n";
pthread_join(thread_id,(void ** )&status); //mian线程暂停等待子线程完成 ,传递状态码
cout<<"status -> " << status << "\n";
cout<<"main() ---------------------------------end "<< "\n";
pthread_exit(NULL); //不影响子线程
}
线程取消
线程的属性可分为可取消属性、不可取消属性。而取消模式又分为立即取消模式、延时取消模式。函数定义如下:
#include <pthread.h>
/*
参数一:状态|类型
参数二:用于保存旧状态的指针
*/
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
在实际应用中子线程难免会对动态内存进行操作,那么如果这时我们手动的调用取消线程后,那么此时线程结束后堆空间的内存就会一直存在会导致数据丢失、内存泄漏、的问题,或者在有锁而没有释放导致死锁的问题。那么应对这种情况线程的cancel type
来了它分为PTHREAD_CANCEL_DEFERRED、PTHREAD_CANCEL_ASYNCHRONOUS
两种状态。前者作为线程的缺省状态表示延时取消,当子线程被取消时线程体遇到取消点才会真正的取消线程执行,那么可以通过linux中的man pthreads
命令(查看库函数)查看定义的取消点函数(函数过多就不展示了、一般的IO操作函数都是取消点)
实例:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdlib>
using namespace std;
void * info (void *arg){
//pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL); //设置该线程不可取消
//pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL); //默认状态为可取消
//pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL); //取消模式为立即取消
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL); //遇到取消点则取消(默认)
int all = 0;
for(int i =0 ;i < 1000000000 ; i++ ){
all++;
}
cout << " all -> " << all << "\n";
pthread_exit(NULL); //手动退出线程 、设置返回状态
}
int main(){
int status;
pthread_t thread_id;
pthread_create(&thread_id,NULL,info,NULL); //创建即启动线程
usleep(100); //子线程运行n毫秒后取消
pthread_cancel(thread_id); //取消线程
int result = pthread_join(thread_id,(void ** )&status);
cout << "thread status -> " << status << "\n"; //获取线程返回状态码
if(status == -1){cout << "thread is cancel status ! \n"; }
else {cout << "thread is normal status ! \n";}
cout<<"main() ---------------------------------end "<< "\n";
pthread_exit(NULL); //不影响子线程
}
在上述代码中,通过获取线程返回状态码查看是否线程被手动取消、当子线程属性被设置为线程不可取消状态时,那么父线程中调用取消则会失效;在缺省值下子线程是可取消状态。
因为cout操作符是一种IO操作所以可以作为一个取消点,所以能响应cancel。上述程序运行图:
我们在开启线程后的10毫秒处就调用的取消线程函数,但是实际还是等到for循环累加完成遇到cout才真正结束线程执行,那么换成立即取消类型再看看效果:
//pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL); //设置该线程不可取消
//pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL); //默认状态为可取消
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL); //取消模式为立即取消
//pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL); //遇到取消点则取消(默认)
运行截图:
返回码返回-1则表示线程被取消了,且程序并没有执行到子线程的cout语句块。
线程终止释放、清理
上述说了线程的结束和取消,那么在这之后还需要对线程的资源进行释放、事物回滚才算是线程任务结束了。
在POSIX线程API中提供了一个pthread_cleanup_push()/pthread_cleanup_pop()
函数对用于自动释放资源函数函数操作调用线程的线程取消清理处理程序堆栈。清理处理器是一个在线程被取消时自动执行的函数(或在下面描述的各种其他情况下);例如,它可以解锁一个互斥锁,以便该进程中的其他线程可以使用该互斥锁。
定义如下:
#include <pthread.h>
/*
参数:将用于释放资源的清理函数地址
*/
void pthread_cleanup_push(void (*routine)(void *),void * arg);//将清理函数推送到清理处理程序堆栈的顶部
void pthread_cleanup_pop(int execute);//pthread cleanup pop()函数会移除堆栈顶部的例程,如果execute为非零值,可以选择执行它。
函数实现如下:
#define pthread_cleanup_push(routine,arg)
{ struct _pthread_cleanup_buffer _buffer;
_pthread_cleanup_push (&_buffer, (routine), (arg));
#define pthread_cleanup_pop(execute)
_pthread_cleanup_pop (&_buffer, (execute)); }
改对函数采用单链表模式(先进后出),函数以宏的方式实现所以必需要成双成对的出现在函数体中且存在于同一级代码段
实例:
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h> //必须要的头文件
using namespace std;
int flag;
void clearfunc(void* arg){
cout<<"clearfunc start..\n";
flag = 0;
}
void clearfunc2(void* arg){
cout<<"clearfunc2 start..\n";
}
void clearfunc3(void* arg){
cout<<"clearfunc3 start..\n";
}
void * info(void * arg){
cout<<"开始获取网络数据...\n";
pthread_cleanup_push(clearfunc,NULL);
pthread_cleanup_push(clearfunc2,NULL);
pthread_cleanup_push(clearfunc3,NULL);
flag = 1;
sleep(2); //假设子线程要执行2秒的延时操作
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
pthread_exit(NULL);
}
int main(){
pthread_t tid;
pthread_create(&tid,NULL,info,NULL);
int status;
sleep(1); //1秒后取消子线程
pthread_cancel(tid);
int ret = pthread_join(tid,(void**)&status);
cout << "thread status -> "<<status << "\n";
cout<<"flag -> "<<flag <<"\n";
cout<<"main---------------end\n";
pthread_exit(NULL);
}
程序运行结果:
这就说明了即使程序被手动取消还是执行我们的释放资源函数,达到了子线程真正的结束。
三、信号处理
将一个程序作为服务启动时./demo &
加上&使它处于后台运行而不占用shell、同时可以在程序中使用fork()
函数此时父进程退出后,fork出来的子线程将子后台运行,且由系统托管。函数定义:
#include <unistd.h>
pid_t fork(void);
/*
作用:Fork()通过复制调用进程来创建一个新进程。新进程称为子进程。调用进程称为父进程
返回值:如果成功,子进程的PID将在父进程中返回,子进程中返回0。失败时,在父进程中返回-1,不创建子进程,并适当地设置errno
*/
使用fork()
函数后台运行如下:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
int main(){
int pid = fork();
if(pid>0){pthread_exit(0);} //fork成功后父进程退出
cout<<"fork pid -> " << pid << "\n";
}
程序运行后的进程截图:
信号发送
经过上述进程情况说明后,在前台运行的程序可以通过Ctrl + c
终止程序运行,那么在后台运行的程序就需要用信号去终止它,在上面的程序中我们的父进程已经退出,只留下一个fork出来的子进程那么此时就可以通过kill 命令(缺省信号值为SIGTERM程序结束信号)发送信号使后台进程终止
关于kill
命令函数定义如下:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
/*
参数一:进程ID号
参数二:信号类型(默认值为SIGTERM程序结束信号)
作用: 向进程发送一个信号
*/
信号接收
从上面可知kill()函数可以作为信号发送,而进程作为信号接收体,那么就有对应的响应方式,我们通过signal()函数去响应信号,函数定义如下:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
/*
参数一:信号类型
参数二:用于处理该信号类型的函数地址
*/
实例:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<signal.h>
using namespace std;
void info (int sig){
cout<< "info()\n" ;
}
int main(){
signal(SIGINT,info); //改变默认的中断处理行为(Ctrl + c) 使它执行我们的函数
//signal(SIGINT,SIG_IGN); //直接忽略该类型信号
//signal(SIGTERM,info); //改变kill的缺省值的行为
while(true){
sleep(1);
cout<<"work ....\n";
}
}
程序运行结果:
此时使用kill -9 pid 即可强制终止进程,向程序发出的是SIGKILL信号,编号是9,此信号不能被忽略,也无法捕获,程序将突然死亡。可以看到这时使用了一个负数那么参数pid有以下几种情况:
1)pid>0 将信号传给进程号为pid 的进程。
2)pid=0 将信号传给和目前进程相同进程组的所有进程,常用于父进程给子进程发送信号,同时发送信号者进程也会收到自己发出的信号。
3)pid=-1 将信号广播传送给系统内所有的进程,例如系统关机时,会向所有的登录窗口广播关机信息。
4)pid<-1 将送往以-pid为组标识的进程
可靠信号&&不可靠信号
可靠信号信号值范围:0-32、不可靠信号信号值范围:34-64
所谓不可靠信号是指当这个信号类型在短时间内发送过多信号导致内核并不能全部缓存下来,从而导致信号丢失。我的linux内核可靠信号的缓存最大值是7640如图所示:
实例:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<signal.h>
using namespace std;
void info (int sig){
sleep(1); //该函数将执行至少1秒
cout<< "info() recv -> " << sig << "\n" ;
}
int main(){
signal(2,info); //接收到信息值为2的信号则调用info函数(不可靠信号)
signal(34,info); //接收到信息值为34的信号则调用info函数(可靠信号)
int i =0;
while(true){
i++;
cout<<"work "<< i <<"%....\n";
usleep(100000);
}
}
从上图运行结果可知短时间内发送了多个不可靠信息就导致了后面的信息丢失了,然后再看看可靠信息是否有这种情况:
发送信号:
接收信号:
当遇到不同信息接收时会中断当前信号处理函数,先处理新的信号再回头处理旧的信号实例如下(采用上面的源程序):
解决方式:使用sigaction
函数定义如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
/*
参数一:接收的信号值
参数二:信号响应的处理方式
参数三:用于保存线先前的响应处理方式可为NULL
*/
struct sigaction {
void (*sa_handler)(int);//响应函数地址
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; //将要搁置的信号
int sa_flags; //信号处理的相关操作
void (*sa_restorer)(void);}
//sa_falgs的值
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
将sigaction替换掉原来的处理信号方式后:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<signal.h>
#include<string.h>
using namespace std;
void info (int sig){
for (int i =0 ; i< 3; i++){
sleep(1);
cout<< "info() recv -> " << sig << " idx ->" << i << "\n" ;
}
}
int main(){
//signal(2,info); //接收到信息值为22的信号则调用info函数
//signal(30,info);
struct sigaction sig;
memset(&sig,NULL,sizeof(sig)); //初始化结构体
sig.sa_handler = info; //执行信号响应函数
sigaddset(&sig.sa_mask,30); //将信号30置为等待状态
sigaction(2,&sig,NULL);
sigaction(30,&sig,NULL);
int i =0;
while(true){
i++;
cout<<"work "<< i <<"%....\n";
usleep(100000);
}
}
信号发送操作:
信号接收操作:
四、多线程信号处理
在posix程序开启了多个子线程后,该程序是以整个进程作为信号接收体,而主线程就是进程号,那么信号就会由main线程进行响应处理,当main线程屏蔽了该类型的信号则会转移到可响应的子线程中。
当信号被main线程接收并响应后并不会中断子线程的运行,实例:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<signal.h>
using namespace std;
void info(int sig){
cout<< "info()----------------start... sig -> "<< sig << "\n";
for(int i=0 ;i < 3; i++){
usleep(1000000);
cout<<"info()->%" << i+1 << "\n";
}
cout<<"info()-------------end!" << "\n";
}
void * threadfunc(void *arg){
cout<< "threadfunc()------------start..." << "\n";
for(int i=0;i<10;i++){ //模拟子线程进行一个下载任务
usleep(1000000);
cout<<"download->%" << i+1 << "0\n";
}
pthread_exit(0); //结束该线程
}
int main(){
signal(15,info); //处理信号
pthread_t tid;
pthread_create(&tid,NULL,threadfunc,NULL);
pthread_join(tid,NULL);
cout<<"threadfunc()------------end!" << "\n";
int num =0;
while(1){ //模拟主线程做的事情
num++;
cout<< "sleep..." << num << "\n";
sleep(1);
}
cout<< "main() ------------end!";
}
程序运行如图:
发送信号:
接收信号:
主线程向子线程发送信号,可以使用pthread_kill()函数:
#include <signal.h>
int pthread_kill(pthread_t thread, int sig);
/*
参数一:线程ID
参数二:信号值
返回值:信号得到响应(0)、线程不存在(ESRCH)、信号不合法(EINVAL)
作用:用于给线程发送信号
*/
向指定的线程发送sig信号,如果线程代码内不做处理,则按照信号默认的行为影响整个进程,也就是说,如果你给一个线程发送了SIGQUIT,但线程却没有实现signal处理函数,则整个进程退出。
使用实例:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<signal.h>
using namespace std;
void info(int sig){
cout<< "info()----------------start... sig -> "<< sig << "\n";
for(int i=0 ;i < 3; i++){
usleep(1000000);
cout<<"info()->%" << i+1 << "\n";
}
cout<<"info()-------------end!" << "\n";
}
void threadfunc_signal(int sig){ //线程信号处理函数
cout<<"threadfunc_signal() 处理信号值->"<< sig <<" 此线程不想强制退出~~ \n";
}
void * threadfunc(void *arg){
cout<< "threadfunc()------------start..." << "\n";
signal(2,threadfunc_signal);
for(int i=0;i<10;i++){ //模拟子线程进行一个下载任务
usleep(1000000);
cout<<"download->%" << i+1 << "0\n";
}
pthread_exit(0); //结束该线程
}
int main(){
signal(15,info); //处理信号
pthread_t tid;
//创建线程
pthread_create(&tid,NULL,threadfunc,NULL);
cout<<"将在2秒后发送信号给子线程...\n";
sleep(2);
//int result = pthread_kill(tid,0); //使用信号值0 当作测试信号判断线程是否存活,返回0则表示该线程存活
int result = pthread_kill(tid,2); //使用信号值2缺省值下会终止整个进程
cout<<"子线程信号result -> " <<result <<"\n";
pthread_join(tid,NULL);
cout<<"threadfunc()------------end!" << "\n";
int num =0;
while(1){ //模拟主线程做的事情
num++;
cout<< "sleep..." << num << "\n";
sleep(1);
}
cout<< "main() ------------end!";
}
运行效果图: