在这个浮躁的时代
只有自律的人才能脱颖而出
-- 《觉醒年代》
从零开始认识多线程 --- 线程控制
1 知识回顾
上一篇文章中,我们通过对地址空间的再次学习来认识了线程:
- 物理空间不是连续的,是4kb的内存块(页框)组成的。
- 页表映射是通过虚拟地址来索引物理地址: - 虚拟地址共32位:前10位用来索引页目录中的元素(页表),中间10位用来索引页表中的对应的元素(页框),后12位用来索引页框中的每一个字节
- 虚拟地址本质是一种资源,可以进行分配!对一个进程的数据进行分配执行,就是多线程的本质!
- Linux中的线程是通过进程模拟的(并没有单独设计出一个单独的线程模块)
- 进程中可以有多个进程(之前学习的是进程的特殊情况),他们共用一个地址空间。进程从内核来看,是承担分配系统资源的基本实体!
- Linux中的执行流是线程 ,CPU看到的执行流 <= 进程
进程与线程需要注意:
- 线程的调度成本比进程低很多,是由于硬件原因:CPU中存在一个cache会储存热点数据(进程相关数据) ,要访问数据时,会先在cache中寻找,如果命中直接访问,反之进行置换。切换进程需要更换热点数据,切换线程不需要切换。
- 线程的健壮性很差!一个线程出错会导致整个线程退出,而不同进程是独立的互不影响!进程和线程各有特长!
- 线程的本质是代码块!只使用函数的对应代码,即拿页表的一部分来执行!!!
- 线程的使用场景多为计算密集型和IO密集型,可以充分使用CPU的并行能力!
同一个进程中的线程虽然共享一个地址空间,但是还是有独属于自己的一些东西:
- 一组寄存器:在硬件中储存上下文数据,保证线程可以动态并行运行!
- 栈空间:线程中可以处理自己的临时变量,临时变量储存在自己独立的栈区,可以独立完成任务。
- 线程ID
- errno信号屏蔽字
- 调度优先级
复习的差不多了,我们了解了线程的基本概念,接下来就要开始学习如何管理线程 — 线程控制。根据我们之前学习的进程控制,大概可以估计一下线程控制的基本接口:线程创建 , 线程等待 , 线程退出…
2 线程控制
2.1 线程创建
万事开头难,我们先来看线程怎么创建:
PTHREAD_CREATE(3) Linux Programmer's Manual PTHREAD_CREATE(3)
NAME
pthread_create - create a new thread
SYNOPSIS
#include<pthread.h>intpthread_create(pthread_t *thread,const pthread_attr_t *attr,void*(*start_routine)(void*),void*arg);
Compile and link with -pthread.
pthread_create
是创建线程的接口,里面有4个参数:
- pthread_t *thread :输出型参数,线程ID。
- const pthread_attr_t *attr :线程属性(优先级,上下文…),默认传入nullptr
- void *(*start_routine) (void *) : 函数指针,线程需要执行的函数地址。
- void arg:想要传入到线程的信息,可以传入int,string地址或者传入一个类对象的地址。
再来看返回值:
RETURN VALUE
On success,pthread_create() returns 0; on error, it returns an error number,and the contents of *thread are undefined.
pthread
系列的函数的返回值是都是一样的:成功返回0,反之返回错误码!
2.2 线程等待
学习进程的时候,如果进程创建出来了,但是不进行等待,就拿不到退出信息,还会造成僵尸进程,进而造成内存泄漏。同样线程也需要进行等待。由主线程来等待新线程
PTHREAD_JOIN(3) Linux Programmer's Manual PTHREAD_JOIN(3)
NAME
pthread_join - join with a terminated thread
SYNOPSIS
#include<pthread.h>intpthread_join(pthread_t thread,void**retval);
Compile and link with -pthread.
这个函数里面有2个参数:
- pthread_t thread:需要进行等待的线程ID
- void **retval: 获取的返回信息
2.3 线程终止
牢记:main线程结束那么进程结束,所以一定要保证main线程最后退出。
- 最简单的线程终止是线程函数返回
return
! - 切记不要使用
exit()
,我们在进程控制中学习过exit()
可以退出进程,但是要注意线程是在一个进程中讨论的,新线程如果使用了exit()
那整个进程就退出了!exit()
不可以用来终止线程 - 操作系统也给我们提供了线程终止的接口:
PTHREAD_CANCEL(3) Linux Programmer's Manual PTHREAD_CANCEL(3)
NAME
pthread_cancel - send a cancellation request to a thread
SYNOPSIS
#include<pthread.h>intpthread_cancel(pthread_t thread);
Compile and link with -pthread.
通过这个参数,可以看出来这是个很简单的接口,终止对应tid的线程。只要线程存在,并且知道tid , 就可以终止线程(可以自己终止自己)。线程终止的返回值是一个整数!
3 测试运行
3.1 小试牛刀 — 创建线程
我们进行一个简单的测试,来使用这两个接口:
注意,使用线程库的接口需要动态链接
g++ -o $@ $^ -std=c++11 -lpthread
#include<iostream>#include<pthread.h>#include<unistd.h>#include<ctime>#include<string>// 测试 1void*ThreadRun(void*args){
std::cout <<"name: "<<*(std::string*)args <<" is running"<< std::endl;sleep(1);
std::string* ret =new std::string(*(std::string*)args +"finish...");return(void*)ret;}intmain(){// 创建一个新线程// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
pthread_t tid;
std::string name ="thread - 1";pthread_create(&tid,nullptr, ThreadRun,&name);//进程等待//int pthread_join(pthread_t thread, void **retval);
std::string *ret =nullptr;pthread_join(tid,(void**)&ret);
std::cout <<*(std::string*)ret << std::endl;return0;}
编译运行一下,我们可以看到:
新线程完成了任务!
问题 1 : main线程和new线程谁先运行? 不确定,和进程的调度方式一致,由具体情况来定。
问题 2 : 我们期望谁先退出?肯定是main线程,所以就有
join
来进行等待,阻塞等待线程退出。如果不进行
join
,就会造成类似僵尸进程的情况(内存泄漏)!
问题 3 :tid是什么样子的,我们可不可以看一看?当然可以:
std::string ToHex(int x){char buffer[128];snprintf(buffer,sizeof(buffer),"0x%x", x);return buffer;}
这样就可以打印出来tid的十六进制:
这数字好像和
lwp
不一致啊
为什么tid这么大?其实
tid
是一个虚拟地址!!!
3.2 探幽析微 — 理解线程参数
问题 4 : 全面看待线程函数传参。上面我们的程序传入了
name
变量的地址,让线程获取了对应的名字。如果想要传入多个变量或方法,可以传入类对象的地址:
classThreadData{public:
std::string name;int num;};
vvoid *ThreadRun(void*args){
ThreadData* td =static_cast<ThreadData*>(args);
std::cout <<"name: "<< td->name <<" is running"<< std::endl;
std::cout <<"num: "<< td->num << std::endl;sleep(1);
std::string *ret =new std::string(*(std::string *)args +"finish...");return(void*)ret;}
std::string ToHex(int x){char buffer[128];snprintf(buffer,sizeof(buffer),"0x%x", x);return buffer;}intmain(){// 创建一个新线程// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
pthread_t tid;// std::string name = "thread - 1";
ThreadData td;
td.name ="thread - 1";
td.num =100;pthread_create(&tid,nullptr, ThreadRun,&td);// 查看tidsleep(1);
std::cout <<"tid: "<<ToHex(tid)<< std::endl;// 进程等待// int pthread_join(pthread_t thread, void **retval);
std::string *ret =nullptr;pthread_join(tid,(void**)&ret);
std::cout <<*(std::string *)ret << std::endl;return0;}
这样就可以传入多个变量:
所以这个
void*
的变量是可以传入任何地址的,一定要想到可以传入类对象。但是刚写的有些问题,我们上面的写法是在主线程的栈区创建变量,让新线程读取主线程的栈,不太合适(破坏了一定独立性)!如果多个变量都传入了这个变量,那么修改一个就会造成所以的线程中的数据都发生改变!!!这可不行!推荐写:
ThreadData* td =newThreadData();
td->name ="thread - 1";
td->num ="100";pthread_create(&tid,nullptr, ThreadRun, td);
这是在堆区进行开辟空间,然后将该空间交给新线程来管理!就不会出现这样的问题了!以后我们都使用这种方式来传递参数!!!
3.3 小有心得 — 探索线程返回
问题 5 :线程的返回值输出型参数
void** retval
,他需要我们传递一个
void*
变量,然后返回值就交给了
void*
变量!这个过程就是对一个指针进行改变其指向的内容的操作。
下面是一个让新线程进行加法工作的程序
void*ThreadRun(void*args){
ThreadData* td =static_cast<ThreadData*>(args);
std::cout <<"name: "<< td->name <<" is running"<< std::endl;
std::cout <<"num: "<< td->num << std::endl;sleep(1);delete td;//返回值
std::string *ret =new std::string(*(std::string *)args +"finish...");return(void*)ret;}
这就将
void*
变量返回给
&(void* ret)
变量,让ret指向对应的堆区。这就类似
int a
放入
int *
中就可以改变a的值
问题 5 :如何全面的看待线程的返回。我们知道如果一个线程出现问题,整个进程就会退出。所以线程的返回只有正常的返回,没有异常的返回,出现异常整个进程会直接退出,根本没有返回错误信息的机会!和传入参数音参数一样,我们也可以返回一个类对象来传递多个变量。
#include<iostream>#include<pthread.h>#include<unistd.h>#include<ctime>#include<string>// 测试 1classThreadData{public:
std::string name;int num1;int num2;};classThreadResult{public:
std::string name;int num1;int num2;int ans;};void*ThreadRun(void*args){
ThreadData *td =static_cast<ThreadData *>(args);
std::cout <<"name: "<< td->name <<" is running"<< std::endl;
std::cout <<"num1: "<< td->num1 <<" num2: "<< td->num2 << std::endl;sleep(1);
ThreadResult *ret =newThreadResult();
ret->name = td->name;
ret->num1 = td->num1;
ret->num2 = td->num2;
ret->ans = td->num2 + td->num1;delete td;return(void*)ret;}
std::string ToHex(int x){char buffer[128];snprintf(buffer,sizeof(buffer),"0x%x", x);return buffer;}intmain(){// 创建一个新线程// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
pthread_t tid;// std::string name = "thread - 1";
ThreadData *td =newThreadData();
td->name ="thread - 1";
td->num1 =100;
td->num2 =88;pthread_create(&tid,nullptr, ThreadRun, td);// 查看tidsleep(1);
std::cout <<"tid: "<<ToHex(tid)<< std::endl;// 进程等待// int pthread_join(pthread_t thread, void **retval);
ThreadResult *ret =nullptr;pthread_join(tid,(void**)&ret);
std::cout << ret->num1 <<" + "<< ret->num2 <<" = "<< ret->ans << std::endl;return0;}
来看返回值:
我们成功获取了新线程中设置的返回值!非常nice!
3.4 求索无厌 — 实现多线程
问题 6 :上面只是创建了单独的一个线程,那如何创建多线程呢?
可以通过维护一个vector数组来对tid进行统一管理
void*ThreadRun(void*args){
std::string name =static_cast<constchar*>(args);while(true){
std::cout << name <<"is running ..."<< std::endl;sleep(1);}return(void*)0;}
std::string ToHex(int x){char buffer[128];snprintf(buffer,sizeof(buffer),"0x%x", x);return buffer;}constint num =10;intmain(){
std::vector<pthread_t> tids;for(int i =0; i < num; i++){// 1. 线程ID
pthread_t tid;// 2. 线程名字char* name =newchar[128];snprintf(name,128,"thread - %d", i +1);pthread_create(&tid,nullptr, ThreadRun, name);//保存所有线程的ID
tids.push_back(tid);}//joinsleep(100);return0;}
这样就创建出了10个新线程,但是我们看这些新线程的的名字好像不太对:
怎么不是1 - 10???完全是乱的!因为线程谁先被调度运行不确定!而我们传入的名字是在主线程的栈区域,可能在新线程还没有调度,name就已经在主线程中被覆盖了!解决办法很简单,我们创建在堆区就可以了
for(int i =0; i < num; i++){// 1. 线程ID
pthread_t tid;// 2. 线程名字//在堆区进行创建。防止被重写覆盖char* name =newchar[128];snprintf(name,128,"thread - %d", i +1);pthread_create(&tid,nullptr, ThreadRun, name);
pids.push_back(tid);}
这样就整齐多了!
接下来就要进行等待:
我们已经通过vector容器来维护了创建所有线程的tid,所以只需要对所有的tid进行join就好了!
void*ThreadRun(void*args){
std::string name =static_cast<constchar*>(args);while(true){
std::cout << name <<"is running ..."<< std::endl;sleep(3);break;}returnnullptr;}
std::string ToHex(int x){char buffer[128];snprintf(buffer,sizeof(buffer),"0x%x", x);return buffer;}constint num =10;intmain(){
std::vector<pthread_t> tids;for(int i =0; i < num; i++){// 1. 线程ID
pthread_t tid;// 2. 线程名字char* name =newchar[128];snprintf(name,128,"thread - %d", i +1);pthread_create(&tid,nullptr, ThreadRun, name);//保存所有线程的ID
tids.push_back(tid);}//joinfor(auto tid : tids){pthread_join(tid ,nullptr);
std::cout <<ToHex(tid)<<" quit..."<< std::endl;}}
来看运行效果:
非常好!!!
我们也可以通过返回值来获取线程的名字:
for(auto tid : tids){void* name =nullptr;pthread_join(tid ,&name);
std::cout <<(constchar*)name<<" quit..."<< std::endl;delete(constchar*)name;}
非常优雅!
3.5 返璞归真 — 线程终止与线程分离
问题 7 :线程终止的返回值
我们来看看通过线程终止接口终止的线程返回值是什么样的:
void*ThreadRun(void*args){
std::string name =static_cast<constchar*>(args);while(true){
std::cout << name <<"is running ..."<< std::endl;sleep(3);//break;}return args;}
std::string ToHex(int x){char buffer[128];snprintf(buffer,sizeof(buffer),"0x%x", x);return buffer;}constint num =10;intmain(){
std::vector<pthread_t> tids;for(int i =0; i < num; i++){// 1. 线程ID
pthread_t tid;// 2. 线程名字char* name =newchar[128];snprintf(name,128,"thread - %d", i +1);pthread_create(&tid,nullptr, ThreadRun, name);//保存所有线程的ID
tids.push_back(tid);}//joinsleep(3);for(auto tid : tids){pthread_cancel(tid);
std::cout <<" cancel: "<<ToHex(tid)<< std::endl;void* ret=nullptr;pthread_join(tid ,&ret);
std::cout <<(longlongint)ret <<" quit..."<< std::endl;}return0;}
可以看的,被
phread_cancel()
终止的线程的返回值是 -1!这个
-1
其实是宏定义
#define PTHREAD_CANCELED ((void *) -1)
。线程终止的方式有三种:
- 线程函数 return
- pthread_cancel 新线程退出结果为-1
- pthread_exit
问题 8 :可不可以不通过join线程,让他执行完就退出呢,当然可以!
这里需要线程分离接口:
PTHREAD_DETACH(3) Linux Programmer's Manual PTHREAD_DETACH(3)
NAME
pthread_detach - detach a thread
SYNOPSIS
#include<pthread.h>intpthread_detach(pthread_t thread);
Compile and link with -pthread.
通过这个接口,分离出去的线程依然属于进程内部,但不需要被等待了。举个例子,之前再讲线程与进程的关系时,我们把不同的线程比作家庭成员,做好自己分内的事情,既可以让家庭幸福,即进程成功运行。而进程分离就好比你长大了,自己搬出去住,不受父母管了,但是依旧属于这个家庭。这种状态就是线程分离。
当然,如果想要将自己分离出去,就要知道自己的tid,这里需要接口:
PTHREAD_SELF(3) Linux Programmer's Manual PTHREAD_SELF(3)
NAME
pthread_self - obtain ID of the calling thread
SYNOPSIS
#include<pthread.h>
pthread_t pthread_self(void);
Compile and link with -pthread.
这个接口会返回调用它的线程的ID。如同getpid()
void*ThreadRun(void*args){// 线程分离pthread_detach(pthread_self());
std::string name =static_cast<constchar*>(args);while(true){
std::cout << name <<"is running ..."<< std::endl;sleep(3);break;}return args;}
std::string ToHex(int x){char buffer[128];snprintf(buffer,sizeof(buffer),"0x%x", x);return buffer;}constint num =10;intmain(){
std::vector<pthread_t> tids;for(int i =0; i < num; i++){// 1. 线程ID
pthread_t tid;// 2. 线程名字char*name =newchar[128];snprintf(name,128,"thread - %d", i +1);pthread_create(&tid,nullptr, ThreadRun, name);// 保存所有线程的ID
tids.push_back(tid);}sleep(3);for(auto tid : tids){pthread_cancel(tid);
std::cout <<" cancel: "<<ToHex(tid)<< std::endl;void*ret =nullptr;int n =pthread_join(tid,&ret);
std::cout <<(longlongint)ret <<" quit... , n: "<< n << std::endl;}return0;}
可以看到,如果我们等待一个已经分离出去的线程,会得到
22
号错误信息!所以不能 join 一个分离的线程!
所以主线程就可以不管新线程,可以继续做自己的事情,不用阻塞在join!
但是注意:线程分离了,依然是同一个进程!一个线程出异常,会导致整个进程退出!
上面是自己分离自己。也可以通过主线程分离新进程:
for(auto tid : tids){pthread_detach(tid);//主线程分离新线程}
4 语言层的线程封装
上面讲的是Linux系统提供给我们的系统调用,帮助我们可以进行线程控制,也叫做原生线程库。我们熟悉了底层的原生线程库,就会方便很多。
我们来看C++11中的线程
#include<iostream>#include<pthread.h>#include<unistd.h>#include<ctime>#include<vector>#include<string>#include<thread>voidthreadrun(int num){while(num){
std::cout <<" num: "<< num << std::endl;}}// C++中线程库intmain(){
std::thread mythread(threadrun,10);while(true){
std::cout <<"main thread..."<< std::endl;sleep(1);}
mythread.join();return0;}
注意,虽然是使用的语言层的线程库,但是依旧要连接
thread
动态库,因为语言层线程库的本质是对原生线程库接口的封装!!!无论是
java
还是
python
都要与原生线程库产生联系!
Thanks♪(・ω・)ノ谢谢阅读!!!
下一篇文章见!!!
版权归原作者 叫我龙翔 所有, 如有侵权,请联系我们删除。