喜欢的点赞,收藏,关注一下把!
进程概念上篇文章已经讲完了,下面我们就来说说线程控制。
我们使用的接口是pthread线程库,也叫做原生线程库给我们提供的,这个库遵守POSIX标准的,跟我们System V是相对应的一种标准。
POSIX线程库
pthread_create 创建一个新的线程
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
这个接口线程概念哪里我们已经用过。创建了一个线程。
今天我们想创建多个线程,并给每个线程都写上编号。
#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
#include<cstdio>
using namespace std;
void* start_rountine(void* args)
{
//安全的进行强制类型转化
string name=static_cast<const char*>(args);
while(true)
{
cout<<"new thread create success, name: "<<name<<endl;
sleep(1);
}
}
int main()
{
#define NUM 10
for(int i=0;i<NUM;++i)
{
pthread_t id;
//pthread_create(&id,nullptr,start_rountine,(void*)"thread new");
char namebuffer[64];
snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i);
pthread_create(&id,nullptr,start_rountine,namebuffer);
}
while(true)
{
cout<<"new thread create success, name: main thread"<<endl;
sleep(1);
}
return 0;
}
for(int i=0;i<NUM;++i)
{
pthread_t id;
char namebuffer[64];
snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i);
pthread_create(&id,nullptr,start_rountine,namebuffer);
//这里sleep一秒,再看一下运行结果
sleep(1);
}
为什么不加sleep,线程编号都变成9了呢?
因为无法保证谁先运行,所以在运行时可能出现一些奇怪的情况。
就如上面我们创建线程时,有可能这个线程创建出来了,但是还没来得及执行后序代码,而优先跑的主线程,主线程上来之后直接对缓冲区写入,而上一个线程只是把缓冲区地址给过去了,但缓冲区本身被下一次格式化显示所覆盖了。所以最终到底线程编号都是9。
所以如果想创建一批线程这种写法不太对的。
正确写法如下:
#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
#include<cstdio>
#include<vector>
using namespace std;
struct ThreadDate
{
pthread_t tid;
char namebuffer[64];
};
void* start_rountine(void* args)//这个是传值传参,形参是实参的拷贝
{
//安全的进行强制类型转化
ThreadDate* td=static_cast<ThreadDate*>(args);
int cnt=10;
while(cnt)
{
//该函数拿到之后,解引用就可以访问到了
cout<<"new thread create success, name: "<<td->namebuffer<<" cnt: "<<cnt-- <<endl;
sleep(1);
}
return nullptr;
}
int main()
{
#define NUM 10
for(int i=0;i<NUM;++i)
{
//每一次都会新创建一个ThreadDate对象
ThreadDate* td=new ThreadDate();
snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
//每个线程都调用start_rouytine,都会把每次新创建对象的地址传给该函数
pthread_create(&td->tid,nullptr,start_rountine,td);
//new出现新对象每个都是独立的,不会互相影响的
}
while(true)
{
cout<<"new thread create success, name: main thread"<<endl;
sleep(1);
}
return 0;
}
为了后序方便处理,因此还可以添加一个vector对象
int main()
{
vector<ThreadDate*> threads;
#define NUM 10
for(int i=0;i<NUM;++i)
{
ThreadDate* td=new ThreadDate();
snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
pthread_create(&td->tid,nullptr,start_rountine,td);
//这样不仅每个线程数据除了自己拿到了,主线程也全部拿到了
threads.push_back(td);
}
for(auto& iter:threads)
{
cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" success"<<endl;
}
while(true)
{
cout<<"new thread create success, name: main thread"<<endl;
sleep(1);
}
return 0;
}
那我现在就有几个问题了。
1.start_routine,现在是被几个线程执行的呢?
回答:10个
那这个函数现在是什么状态?
回答:重入,信号时谈到一个函数被多个执行流访问,这个函数就是可重入状态。
2.该函数是可重入函数吗?
回答:是的
3.start_routine内定义的变量,多个执行流进来,会不会互相影响?
回答:不会,
如何证明?
void* start_rountine(void* args)
{
//安全的进行强制类型转化
ThreadDate* td=static_cast<ThreadDate*>(args);
int cnt=10;
while(cnt)
{
cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;
cnt--;
sleep(1);
}
return nullptr;
}
由运行结果说明。
在函数内定义的变量,都叫做局部变量,具有临时性,这是语言上的概念今天依旧适用。在多线程情况下,也没有问题。通过这个概念我们也证明了一件事情:其实每一个线程都有自己独立的栈结构。
在哪呢?下面说。
进程终止有三种方式,我们先说两种,然后把线程等待说一说,下面再说第三种。
return
线程函数结束,return的时候,线程就算终止了,或者在任意位置return。
关于返回值void*,下个话题再说。
以前我们也用exit终止过进程,这里可以用来终止线程吗?
void* start_rountine(void* args)
{
//安全的进行强制类型转化
ThreadDate* td=static_cast<ThreadDate*>(args);
int cnt=10;
while(cnt)
{
cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;
cnt--;
sleep(1);
exit(1);
}
return nullptr;
}
不能,因为exit是终止进程的!任何一个执行流调用exit都会让整个进程退出,
pthread_exit
那一个线程调用这个函数就终止哪一个线程。
参数先不管,等会说。
void* start_rountine(void* args)
{
//安全的进行强制类型转化
ThreadDate* td=static_cast<ThreadDate*>(args);
int cnt=10;
while(cnt)
{
cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;
cnt--;
sleep(1);
pthread_exit(nullptr);
}
return nullptr;
}
由此说明每一个线程调用pthread_exit都把自己终止了,并不影响主线程
这个调用也可以放在return的位置,都是一样的。
void* start_rountine(void* args)
{
//安全的进行强制类型转化
ThreadDate* td=static_cast<ThreadDate*>(args);
int cnt=10;
while(cnt)
{
cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;
cnt--;
sleep(1);
}
pthread_exit(nullptr);
}
注意到不管是return,还是pthread_exit的参数,返回类型都是void*,有返回值,如果我想拿到这个返回值,该怎么办呢?
这里我们不得不先谈线程等待的问题
线程也是要被等待的。
如果不等待会有什么问题呢?
以前说过僵尸进程,子进程退出了,你不wait等待,子进程是僵尸状态,从而造成内存泄漏。那么在多线程这里,每一个线程在退出时,它的PCB并没有被释放,如果不等待,会造成类型僵尸进程的问题-----内存泄漏
所以线程也必须要被等待,目的有两个
1.获取新线程的退出信息(可以不关心,但还是要等)
2.回收新线程对应的PCB等内核资源,防止内存泄漏(暂时无法查看)
pthread_join
等待线程结束
thread:线程ID(你等哪一个线程)
retval:void**?一个void*就很难理解了。这是什么意思呢?
返回值:成功返回0;失败返回错误码
我们先使用这个函数进行等待再说。
int main()
{
vector<ThreadDate*> threads;
#define NUM 10
for(int i=0;i<NUM;++i)
{
ThreadDate* td=new ThreadDate();
snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
pthread_create(&td->tid,nullptr,start_rountine,td);
//这样不仅每个线程数据除了自己拿到了,主线程也全部拿到了
threads.push_back(td);
}
for(auto& iter:threads)
{
cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" success"<<endl;
}
for(auto& iter:threads)
{
//阻塞式的等待每一个线程
int n=pthread_join(iter->tid,nullptr);
assert(n == 0);
(void)n;
cout<<"join : "<<iter->namebuffer<<" success"<<endl;
delete iter;
}
//这里就可以看出主线程式阻塞式的等待,全部等待成功,才打印这句话
cout<<"main thread quit!!"<<endl;
return 0;
}
要等待的时候设置一下join,操作系统就会自动帮我收回曾经为线程所创建的轻量级进程相关的资源。
现在谈一谈线程退出返回值问题。
我们发现线程退出,用return返回时它的类型是void*,用pthread_exit需要我们传入一个线程的参数类型也是void*,而我们想获得线程执行完的结果,对我们来说也可以通过join获得它,当然也可以回收它。
我们看到join的第二个参数,void** 也和void有关,难道都是巧合吗?
我们主线程join新线程,第二个参数是void** ,到底是什么意思呢?
你想作为输出型参数把结果拿回去,而返回值可是void*,你想把结果带出去,就必须是void**
下面我们演示一下。
struct ThreadDate
{
int number;
pthread_t tid;
char namebuffer[64];
};
void* start_rountine(void* args)
{
//安全的进行强制类型转化
ThreadDate* td=static_cast<ThreadDate*>(args);
int cnt=5;
while(cnt)
{
cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;
cnt--;
sleep(1);
}
return (void*)td->number;//传值返回
}
这里返回可能会有warning,因为整数4个字节,而linux默认release编译,指针大小8字节,但并不影响。
那么请问,这个整数返回的时候,如果返回到void*里面,请问这个整数或者说这个变量在哪里呢?
其实上面的返回就相当于把这个数字写到指针变量里。4字节写到8字节里
void* ret=(void*)td->number;
for(auto& iter:threads)
{
void* ret=nullptr;//注意是void*
//阻塞式的等待每一个线程
int n=pthread_join(iter->tid,&ret);//&地址是void**
assert(n == 0);
(void)n;
cout<<"join : "<<iter->namebuffer<<" success"<<endl;
delete iter;
}
这个join函数内部就做了这样一件事情
void** retp=&ret;
*retp=return (void*)td->number //*retp解引用就是ret
这样就相对于把返回值写到ret变量里。
然后再打印出来。
for(auto& iter:threads)
{
void* ret=nullptr;//注意是void*
//阻塞式的等待每一个线程
int n=pthread_join(iter->tid,&ret);//&地址是void**
assert(n == 0);
(void)n;
//注意这里如果用int打印,会有精度损失,8->4
//所以用long long
//cout<<"join : "<<iter->namebuffer<<" success , number: "<<(int)ret<<endl;
cout<<"join : "<<iter->namebuffer<<" success , number: "<<(long long)ret<<endl;
delete iter;
}
确实拿到了返回结果。
这里想告诉大家的时,未来线程的返回值可以通过join第二个参数拿到。
如果这里还是不懂话,我们换个值。
我想让每个线程都能返回这个值。
这里可以理解成pthread库里面为了保存当前线程的返回值定义了一个变量类型是void*,return 这里可以认为把111强转成void*,我们把它当成指针来看,return就是把111写到void变量当中
接下来就是,我在自己的用户空间里定义了一个指针变量void ret
而pthread_jion本质:是从库中获取执行线程的退出结果!
&ret,就是指向这块空间
然后join内部就相当于做
(&ret)就是我们的定义的ret,然后把111拷贝到ret里
因此,return就相当于把这个数字返回到库中了,而库为当前线程维护一个小的变量,它保存的是该线程退出时void变量,直接把这个数字写入到这个变量中,而我们在用户空间定义一个void*ret变量,如果不用pthrad_join函数怎么接收这个返回值呢?假设返回值变量名称是下,是不是就可以直接ret=x了啊。但是我们又不能直接读取库里的内容,必须通过函数来调用,所以只能&ret,然后就如void** retp=&ret;retp=x;而retp就是ret,这样而能把x赋给ret了。
还有一种角度就是,形参是是实参的临时拷贝,如果就传ret,形参的改变并不会影响实参,这个值你就拿不回来。
同样pthread_exit也可以传返回值
void* start_rountine(void* args)
{
//安全的进行强制类型转化
ThreadDate* td=static_cast<ThreadDate*>(args);
int cnt=5;
while(cnt)
{
cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;
cnt--;
sleep(1);
}
pthread_exit((void*)111);
}
既然假的地址,整数都能被外部拿到,那么如果返回的是堆空间的地址呢?对象的地址呢?
struct ThreadReturn
{
int exit_code;
int exit_result;
};
void* start_rountine(void* args)
{
//安全的进行强制类型转化
ThreadDate* td=static_cast<ThreadDate*>(args);
int cnt=5;
while(cnt)
{
cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;
cnt--;
sleep(1);
}
//注意千万不能直接申请一个对象
//ThreadReturn tr; //这是在栈上开辟空间,出了栈就销毁了
//tr.exit_code
//tr.exit_result
//return (void*)&tr
ThreadReturn *tr=new ThreadReturn();//堆开辟的空间
tr->exit_code=1;
tr->exit_result=111;
return (void*)tr;
}
int main()
{
vector<ThreadDate*> threads;
#define NUM 10
for(int i=0;i<NUM;++i)
{
ThreadDate* td=new ThreadDate();
td->number=i+1;
snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
pthread_create(&td->tid,nullptr,start_rountine,td);
//这样不仅每个线程数据除了自己拿到了,主线程也全部拿到了
threads.push_back(td);
}
for(auto& iter:threads)
{
cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" success"<<endl;
}
for(auto& iter:threads)
{
ThreadReturn* tr=nullptr;
int n=pthread_join(iter->tid,(void**)&tr);//&地址是void**
assert(n == 0);
(void)n;
cout<<"join : "<<iter->namebuffer<<" success , exit_code: "<<tr->exit_code<<" exit_result: "<<tr->exit_result<<endl;
delete iter;
}
//这里就可以看出主线程式阻塞式的等待,全部等待成功,才打印这句话
cout<<"main thread quit!!"<<endl;
return 0;
}
还有一个小问题,以前进程退出的时候,我们除了可以拿到退出码,还有退出信号
pthread_join为什么没有对应的参数可以接收线程异常的信号呢
为什么没有见到,线程退出的时候,对应的退出信号呢? 线程异常了怎么呢?
线程出异常,收到信号,整个进程都会退出!pthread_join连返回的机会都没有,pthread_join:默认就会认为函数会调用成功!不考虑异常问题,异常问题是你进程该考虑的问题。
上面说了线程终止有三种方式,接下来就说这第三种方式。
线程是可以被cancel取消的!
注意:线程要被取消,前提是这个线程已经跑起来了。
pthread_cancel
取消一个执行中的线程
thread:线程ID
void* start_rountine(void* args)
{
//安全的进行强制类型转化
ThreadDate* td=static_cast<ThreadDate*>(args);
int cnt=5;
while(cnt)
{
cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;
cnt--;
sleep(1);
}
//正常跑完返回的100,那被取消的线程返回的是什么呢?
return (void*)100;
}
int main()
{
vector<ThreadDate*> threads;
#define NUM 10
for(int i=0;i<NUM;++i)
{
ThreadDate* td=new ThreadDate();
td->number=i+1;
snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
pthread_create(&td->tid,nullptr,start_rountine,td);
//这样不仅每个线程数据除了自己拿到了,主线程也全部拿到了
threads.push_back(td);
}
for(auto& iter:threads)
{
cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" success"<<endl;
}
//线程取消
sleep(5);//先让线程跑起来
for(int i=0;i<threads.size()/2;++i)
{
pthread_cancel(threads[i]->tid);
cout<<"pthread_cancel: "<<threads[i]->namebuffer<<" success"<<endl;
}
for(auto& iter:threads)
{
void* ret=nullptr;//注意是void*
int n=pthread_join(iter->tid,&ret);//&地址是void**
assert(n == 0);
(void)n;
cout<<"join : "<<iter->namebuffer<<" success , number: "<<(long long)ret<<endl;
delete iter;
}
//这里就可以看出主线程式阻塞式的等待,全部等待成功,才打印这句话
cout<<"main thread quit!!"<<endl;
return 0;
}
一个线程如果是被取消的,退出码是-1。
一般都是主线程取消新线程的。
接下来重新认识我们的线程库(语言版)
C++也有多线程,使用要包含一个thread库文件
mythread:mythread.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f mythread
#include<iostream>
#include<thread>
#include<unistd.h>
using namespace std;
void* thread_run(void* args)
{
while(true)
{
cout<<"我是新线程..."<<endl;
sleep(1);
}
}
int main()
{
//创建,并执行对应的方法
thread t1(thread_run);
while(true)
{
cout<<"我是主线程..."<<endl;
sleep(1);
}
//回收
t1.join();
return 0;
}
我故意把makefile中pthread弄掉了,编译不能通过了。
我们明明没有用过pthread_create怎么会有这个呢?我用的可是C++的多线程,从来没有用过pthread库啊。
那把它加上去看看,就没有报错了。我用的是C++的多线程,但是我照样看的是轻量级进程。
任何语言,在Linux中如果要实现多线程,必定要用pthread库。
如何看到C++11中多线程库呢?
C++11的多线程库,在Linux环境中,本质是对pthread库的封装!
也就是说线程是可以等待的,等待的时候,join的阻塞式的等待,如果我们不想等待呢?
pthread_detach
线程分离,线程结束后,自动释放线程资源。
thread:线程ID
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离自己。
如果是自己分离自己该如何得知自己的线程ID呢?
pthread_self
谁调用这个函数,就获得线程ID
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
string changeId(const pthread_t &thread_id)
{
char tid[128];
snprintf(tid, sizeof(tid), "0x%x", thread_id);
return tid;
}
void* start_routine(void* args)
{
string threadname=static_cast<const char*>(args);
while(true)
{
cout<<threadname<<" running ... : "<<changeId(pthread_self())<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
cout<<"main thread running ... new thread id:"<<changeId(tid)<<endl;
pthread_join(tid,nullptr);
return 0;
}
由此证明pthread_self可以获取线程ID的
下面使用pthread_detach分离线程
void* start_routine(void* args)
{
string threadname=static_cast<const char*>(args);
//pthread_detach(pthread_self());//设置自己为分离状态
while(true)
{
cout<<threadname<<" running ... : "<<changeId(pthread_self())<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
string main_id=changeId(pthread_self());
cout<<"main thread running ... new thread id:"<<changeId(tid)<<"main thread id: "<<main_id<<endl;
//一个线程默认是joinable的,如果设置了分离状态,不能够进行等待了
int n=pthread_join(tid,nullptr);
cout<<"result: "<<strerror(n)<<endl;
return 0;
}
先不分离看看结果
void* start_routine(void* args)
{
string threadname=static_cast<const char*>(args);
pthread_detach(pthread_self());//设置自己为分离状态
int cnt=5;
while(cnt--)
{
cout<<threadname<<" running ... : "<<changeId(pthread_self())<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
string main_id=changeId(pthread_self());
cout<<"main thread running ... new thread id:"<<changeId(tid)<<" main thread id: "<<main_id<<endl;
//一个线程默认是joinable的,如果设置了分离状态,不能够进行等待了
int n=pthread_join(tid,nullptr);
cout<<"result: "<<n<<" : "<<strerror(n)<<endl;
return 0;
}
现在分离再看运行结果
为什么分离和不分离的运行结果都是一样的呢?
之前说过,主线程和新线程谁先跑是不确定的。
可能先跑的主线程,然后就先等待了,还是阻塞式等待,而新线程还没有执行到pthread_detach,等到新线程分离了,但是主线程不知道,所以等新线程退出了还是回收你。
接下来我们先让新进程分离,然后主进程在等待看一看运行结果。
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
string main_id=changeId(pthread_self());
cout<<"main thread running ... new thread id:"<<changeId(tid)<<" main thread id: "<<main_id<<endl;
//先让新进程跑上2秒
sleep(2);
//一个线程默认是joinable的,如果设置了分离状态,不能够进行等待了
int n=pthread_join(tid,nullptr);
cout<<"result: "<<n<<" : "<<strerror(n)<<endl;
return 0;
}
所以推荐分离应该在线程创建成功后,由主线程进行分离新线程
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
pthread_detach(tid);//主线程对新线程进行分离
string main_id=changeId(pthread_self());
cout<<"main thread running ... new thread id:"<<changeId(tid)<<" main thread id: "<<main_id<<endl;
//sleep(2);
//一个线程默认是joinable的,如果设置了分离状态,不能够进行等待了
int n=pthread_join(tid,nullptr);
cout<<"result: "<<n<<" : "<<strerror(n)<<endl;
return 0;
}
这样结果就更明显了。
因为新线程一跑起来就分离了,主线程在等待就报错了
如果主线程对新线程分离了,就不用再等待了,主线程做自己的事情就好了。
void* start_routine(void* args)
{
string threadname=static_cast<const char*>(args);
int cnt=5;
while(cnt--)
{
cout<<threadname<<" running ... : "<<changeId(pthread_self())<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
pthread_detach(tid);//主线程对新线程进行分离
string main_id=changeId(pthread_self());
while(true)
{
cout<<"main thread running ... new thread id:"<<changeId(tid)<<" main thread id: "<<main_id<<endl;
sleep(1);
}
return 0;
}
其实线程控制到这里就结束了。
但是我们一直还有一些问题没有解决,
线程ID是什么?
每个线程都有独立的栈结构,可是虚拟地址空间的栈只有一个,那这个栈在哪?
下面我们就说一说。
Linux无法直接提供创建系统调用接口!而只能给我们提供创建轻量级进程的接口!
但是我们的程序员用户可不管这些轻量级进程,我只要线程。
没有办法,所以在操作系统和应用程序员之间设计了一个库,这就是我们一直在用的pthread库,我们称之为原生线程库。
当一个程序员想用线程的时候,是不是得有线程的ID,线程的状态,优先级等,独立栈这些属性。
但还要做一个事情,我们程序员要线程时,并不是直接向操作系统要的,而是从库中要。所以原生线程库,可能要存在多个线程。
也就是说你用库中的接口创建了线程,别人可以同时在用吗?当然是可以的。你在开发时用。别人在运行时用。
所以当前原生线程库要不要对线程进行管理呢?
当然要的!不然你怎么知道你的线程ID是多少,你怎么知道你当前线程的栈在哪里,栈大小,你怎么你的线程其他属性有哪些。
下面这就是我们线程的属性,它是联合体。
将来线程属性字段就会按照特定的要求填充到这个数组里。每一个字段都有自己的含义。
虽然要对线程做管理,但是不像进程那么多。
如何管理?
先描述,在组织!
只不过这个描述(线程的属性),比较少!
可以理解成每次创建线程,都需要在库中先创建一个结构体,这里每一个结构体都会对应一个轻量级进程,
Linux方案:用户级线程,用户关心的线程属性在库中,内核提供线程执行流的调度
Linux 用户级线程:内核级轻量级进程 = 1:1
库不关心线程如何调度,只关心线程ID,栈在哪里,栈的大小等属性这是由库维护的,相当于库也帮我们创建一个数据结构来描述这个线程。
下面在看一张图,两张图结合一起能让我们对这块知识有更清楚的认识。
虚拟地址空间有一块区域,叫做共享区,这个我们在动静态库的时候说过。
说到底pthread库是一个磁盘文件只不过是库文件罢了 。进程用了这个库,是一定要加载到内存中,然后映射到进程地址空间的共享区里面。
每一个线程当被创建时,除了要在内核中创建对应的轻量级线程,还要库中创建对应的描述线程属性的相关结构体
这里只是给出了重要的三条属性,第一个是线程ID,第二个线程的局部存储,第三个线程栈结构。
这就是库中描述的对象,那怎么组织的呢?
有一个线程,就创建一个线程的结构体,我们可以称之为TCB,这个线程控制块是放在一起的,你可以想象成看出是一个数组,有了数组之后,为了更好的找到某一个线程,我们只需要找到数组的起始地址,而这个数组的起始位置并没有采用下标,动态库加载到内存之后不是有起始地址吗,所以对应的每个TCB都有自己的起始地址。
所以实际上线程ID就是在共享区线程库中对应线程TCB的地址。
根据这个地址就可以找到对应线程的相关属性了。
所以当一个线程结束时,库会把该进程的返回值填到该线程ID所对应的TCB中,主线程在进程join的时候,根据该线程的ID去对应TCB中拿返回值。
现在也可以回答之前的问题了,地址空间里只有一个栈,可是说每个线程都有自己独立的栈结构,那么这个栈在那呢?
这个栈在库中的对应TCB中,每个栈都有自己的私有栈。
换句话说,未来创建多个线程,主线程的栈在虚拟地址空间中,用的时主线程栈,而其他线程你想用对应的栈,这个栈在共享区的线程库中的对应的TCB结构体中,库帮我们维护好的。
库帮我们创建轻量级进程,调用的接口是clone,第一个参数指明回调函数,第二个指向线程的独立栈,当TCB创建好后,把栈空间的起始地址传递给child_stack,而未来每个新线程都用的是自己独立栈。不和主线程用同一个栈。
总结一下:
当你创建线程时,对应的线程实际上,它帮我们在线程库中创建对应的线程控制块(可以认为时TCB),线程控制块对应的地址就是线程控制块起始地址,这个TCB里面包含线程的ID,线程私有栈结构,线程的局部存储,创建好之后底层库继续帮我们调用clone,在底层创建轻量级进程,然后把我们创建好的TCB对应的回调方法,私有栈等参数传给clone,所以底层的线程一旦创建好了,它就时依赖我们的原生线程库,进而新线程在使用的临时变量就是压到了自己的child_statck所指的栈中,也就是在我们的库中直接保存了。
所以线程的ID就是我们传说中在共享区的线程库中对应线程TCB的地址。
这种解决方案的线程,我们称之为用户级线程!
线程的局部存储我们说一下。
我们知道线程大部分资源都是共享的,关于全局变量我们也演示过了。
int g_val=0;
string changeId(const pthread_t &thread_id)
{
char tid[128];
snprintf(tid, sizeof(tid), "0x%x", thread_id);
return tid;
}
void* start_routine(void* args)
{
string threadname=static_cast<const char*>(args);
//pthread_detach(pthread_self());//设置自己为分离状态
//int cnt=5;
while(true)
{
cout<<threadname<<" running ... : "<<changeId(pthread_self())<<\
" g_val: "<<g_val<<" &g_val: "<<&g_val<<endl;
++g_val;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
pthread_detach(tid);//主线程对新线程进行分离
string main_id=changeId(pthread_self());
while(true)
{
cout<<"main thread running ... new thread id:"<<changeId(tid)<<" main thread id: "<<main_id<<\
" g_val: "<<g_val<<" &g_val: "<<&g_val<<endl;
sleep(1);
}
return 0;
}
上面运行结果符合预期
下面我们改一下代码,在运行一下看结果
__thread int g_val=0;
发现新线程值一直在++,而且地址也变得很大,而主线程值没有变,但是地址也变得很大,并且两个地址都不一样。这是为什么呢?
添加__thread,可以将一个内存类型设置为线程局部存储
这个变量依旧是全局变量,只不过在编译得时候,给每一个线程都来一份,每个线程都有一份,那么它们在访问这个变量时就互不影响了;
那为什么地址会有那么大变化呢?
原因在于刚开始定义得全局变量在已初始化数据段,而你一旦把它设置了局部存储,那么这个变量就被映射到了对应得线程TCB中得线程局部存储,而线程局部存储可是在共享区中,共享区可是接近栈了,比已初始化数据段可要高,而虚地址空间从低地址到高地址的。
如果未来想给线程定义一些私有属性,不想放在栈上,或者new或者malloc你就可以定义成这样,这种局部存储是介于全局变量和局部变量之间的线程特有的存储方案。
以上就是线程控制的全部东西!
既然学完了,那现在就用一下,我们尝试把线程接口进行封装,就像C++那样调用我们的线程!
#pragma once
#include<iostream>
#include<pthread.h>
#include<string>
#include<functional>
using namespace std;
class Thread
{
public:
//这里也可以按照C的方法typedef void*(*func_t)(void*)
typedef function<void*(void*)> func_t;
private:
string _name;
func_t _func;//回调函数
void* _args;//回调函数参数
pthread_t _tid;//线程ID
};
以后我们写代码难免会遇见C和C++混编的情况,可能会觉得C++提供了很多方便的接口给我们用,但是不好意思,有些C++提供的接口,底层不认识。底层可能用纯C写的。
class Thread
{
public:
typedef function<void*(void*)> func_t;
const int num=1024;
public:
Thread(func_t func,void* args=nullptr,int number=0)
:_func(func)
,_args(args)
{
char namebuffer[num];
snprintf(namebuffer,sizeof(namebuffer),"thread->%d",number);
_name=namebuffer;
//调用回调函数
//报错
int n=pthread_create(&_tid,nullptr,_func,_args);
assert(n == 0);
(void)n;
}
private:
string _name;
func_t _func;//回调函数
void* _args;//回调函数参数
pthread_t _tid;//线程ID
};
pthread_create接口不认识C++提供的这个东西。
解决方法有很多,换成C的,或者别的方法,下面提供一种方法
我写一个能pthread_create库能认识的回调函数方法。
void* start_routine(void* args)
{
return _func(_args);
}
Thread(func_t func,void* args=nullptr,int number=0)
:_func(func)
,_args(args)
{
char namebuffer[num];
snprintf(namebuffer,sizeof(namebuffer),"thread->%d",number);
_name=namebuffer;
//调用回调函数
int n=pthread_create(&_tid,nullptr,start_routine,_args);
assert(n == 0);
(void)n;
}
但这里还有错。考虑一下为什么。
原因在于
void* start_routine(void* args)//类内函数,有缺省参数(this指针)
{
return _func(_args);
}
那我static一下,就变成静态函数了,没有this指针了
static void* start_routine(void* args)//类内函数,有缺省参数(this指针)
{
return _func(_args);
}
但问题又出来了,这又是什么问题呢?
static void* start_routine(void* args)//类内函数,有缺省参数(this指针)
{
//静态函数只能调用静态成员和静态函数
return _func(_args);
}
那把这两个成员变量都变成静态成员变量可不可以?
不可以的,把这个两个成员变成静态的了,那这个两个变量就是所有线程共享的了。
这里我们再写一个类,把this指针保存起来,然后再去调用,顺便把剩下成员函数谢谢,简单的线程封装就写好了
#pragma once
#include<iostream>
#include<pthread.h>
#include<string>
#include<functional>
#include<cstdio>
#include<cassert>
using namespace std;
//声明
class Thread;
class Context
{
public:
Thread* _this;
void* args_;
Context()
:_this(nullptr)
,args_(nullptr)
{}
};
class Thread
{
public:
typedef function<void*(void*)> func_t;
const int num=1024;
public:
static void* start_routine(void* args)//类内函数,有缺省参数(this指针)
{
Context* tr=static_cast<Context*>(args);
tr->_this->_func(tr->args_);
//静态函数只能调用静态成员和静态函数
//return _func(_args);
}
Thread(func_t func,void* args=nullptr,int number=0)
:_func(func)
,_args(args)
{
char namebuffer[num];
snprintf(namebuffer,sizeof(namebuffer),"thread->%d",number);
_name=namebuffer;
Context* tr=new Context();
tr->_this=this;
tr->args_=_args;
//调用回调函数
int n=pthread_create(&_tid,nullptr,start_routine,tr);
// int n=pthread_create(&_tid,nullptr,start_routine,_args);
assert(n == 0);
(void)n;
}
void join()
{
int n=pthread_join(_tid,nullptr);
assert(n==0);
(void)n;
}
~Thread()
{}
private:
string _name;
func_t _func;//回调函数
void* _args;//回调函数参数
pthread_t _tid;//线程ID
};