目标:
线程的概念及基本操作
线程和线程安全,可重入。
线程和进程的区别。
本质:线程是进程的一个执行分支,是进程内部运行的一个执行流。
相对于原先的进程概念,进程切换的成本变小了
当我们今天创建“进程”,不独立创建地址空间,用户级页表,甚至不进行IO将程序的数据和代码加载到内存,只创建task_struct,然后让新的PCB和老的PCB指向相同的mm_struct,然后通过合理的资源分配,让每一个task_struct都能使用进程的一部分资源。
此时每个PCB被CPU调度的时候,执行的粒度,要比原始进程(单执行流的进程)更小一点。
ps:执行到代码时,可能会跳转到共享区后经过页表转换找到对应的库。
定义:
这种执行粒度更小的执行流叫线程。
CPU,进程地址空间如何看待进程:
从地址空间:的角度,其实是进程的资源窗口。
从CPU: 让不同的执行流执行不同的代码函数,切分代码区。让栈区给每个进程私有一份,CPU在调度的时候各个执行流执行的就是不同的代码。
如何重新理解进程:
站在操作系统的角度,进程是承担分配系统资源的基本单位
,在创建第二个轻量级进程/以及后面的实际上都是用的进程早已申请好的资源,让线程去找进程要资源。如同社会,学校,学生之间的关系,学校获取社会的部分资源,学生在学校获取部分资源,社会如同系统,学校如同进程,学生如同线程。
如何看待之前所学的进程:
之前所学的进程全都是单执行流的进程,进程是承担资源分配的实体,但都只有一个执行流,所以之前在单线程流的进程当中调度的基本单位说是进程也没问题。
Linux线程 vs 其他平台线程:
Linux(类Unix):站在CPU的角度,看待一个个线程和之前的进程并没有区别。只是可能在执行“进程流”的时候比以往的进程更加轻量化了。即同一进程当中的线程要进行上下文切换的时候,成本必然会比两个不相关进程要低,因为他们的地址空间和用户级页表和物理内存的数据都无需更改,进行调度另一个线程要保存的资源要更少。
所以,Linux下没有真正意义上面的线程概念,而是将进程的模拟,我们也称Linux下的进程为轻量级进程。
其他平台:例如windows,具有真正的线程概念,由于线程:进程一定是n:1,这么多的线程就需要管理,管理就需要描述+组织起来。要管理线程,线程描述:TCB,而系统就会同时存在PCB和TCB,两者内部还要产生关系,表示该线程所属哪个线程,对于CPU来说调度上也会比只有进程的系统难度大。由于OS既要进行线程和进程管理,他们都是执行流,会出现大量的重复属性
,这样的操作系统往往设计的比较复杂,效率上就比不过Linux了。
而Linux下只需要一套PCB,一套调度算法,一套机制就能将进程和线程都调度起来。
线程库应当有的字段:
//属性
//线程id
//优先级
//上下文
//地址空间
//记账数据
//.....
结论:进程是分配系统资源的基本实体,线程是OS调度的基本单位。但是由于Linux没有线程的概念,所以Linux是没有提供有关于线程的系统调用接口,但是有轻量级进程相关的系统调用接口,如vfork,我们下面讲的是pthread库,是原生线程库。
什么叫进程内部?
线程本质是在进程的地址空间内运行!
如何做到每个线程拥有进程的部分资源?
划分的时候将用户级页表即可,看到页表的一部分实际上就是看到资源的一部分。
多线程的优点:
举个例子
如同看视屏的时候,一个进程内的线程有的负责解码工作,有的负责下载,有的负责播放。
计算量特别大的程序,可以将计算分配到不同的线程当中执行。
总结:创建成本小,切换成本小,占用成本小。
多线程的缺点:
1.性能损失,所有的PCB都共享地址空间,理论上,每个线程都能够看到进程所有的资源。与此同时相当于资源不变,线程数多的时候,可能会导致CPU的消耗在线程切换,而不是真正的处理事务上。
2.健壮性降低,一个线程的奔溃会影响其他线程。
3.缺乏访问控制,进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
好处:线程间通信成本低,因为两个线程都是用的同一份进程地址空间。
缺点:一定存在大量的临界资源,访问临界资源时需要各种互斥和同步的机制来保证线程安全。
线程是否越多越好?
线程数合适最好,线程与线程进行切换有切换成本
,若存在大量线程,CPU耗费的资源可能大部分都在线程间切换
上面,通常情况下,一般多线程创建数和cpu数或者核数相等
即可。
如果是在IO密集型,可以多创建一些。
Linux不能直接在OS层面提供线程调用接口,线程相关的概念都是在原生线程库,在应用程完成现成的请求操作。
返回值为0则成功,不成功就是错误码。
第一个参数thread_t是本地的无符号整形的地址,第二个是线程的状态,第三个参数是线程执行的函数,第四个参数是执行函数所要的参数,若只有一个可以直接传,若有批量可以定义一个结构体进行传参。
ps -aL
查看轻量级线程,L表示light,轻量的意思。
测试pthread_create的使用
#include
#include
#include
#include
void* Thread1(void* arg)
{
int count = 5;
while (count--)
{
printf("%s\n", (char*)arg);
sleep(10);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, Thread1, (void*)"Thread1");
//主线程
while (true)
{
printf("I am main thread!\n");
sleep(10);
}
return 0;
}
[ljh@VM-0-11-centos 3.17]$ ps -aL | head -1 && ps -aL |grep test
LWP则是CPU进行调度的单位:
LWP是标识执行流是一个轻量级进程。并且可以看到主线程的pid与lwp是相同的
。并且可以观察到主执行流的PID和LWP的数值是相同的。
信号的发送单位是进程:
一旦信号发送给一个进程,进程当中的所有线程都会挂掉。如:线程是进程的一个执行分支,发生野指针,除0导致线程退出的同时,意味着进程触发了该错误。
线程ID+寄存器数据+栈
其中线程ID和寄存器数据能够体现线程是可以上下文切换的,而栈结构独有是保存线程的临时变量,这能保证线程是独立运行的。(通过全局指针变量指向线程的局部变量虽然也可以让其他线程访问到栈的数据)
文件描述表共享的理解
多进程下,不共享文件描述符表
,但是表的内容可以是一样的
(管道,两个进程指向的表不同,但是内容可以一样),多线程的文件描述符表是相同的。即父进程fork创建子进程,父子用的不是一张文件描述符表,但是文件描述符表的内容是相同的。
倘若在从线程函数当中return我们可以发现,主线程还在,其他线程退出。
倘若是exit,则全部线程都退出,则进程退出。
试想一下一个线程干完活后直接exit干掉整个进程,导致其他线程的工作无法执行,这会造成很严重的结果。
而在main函数中return相当于进程退出。
exit是终止进程的,绝对不能用来终止线程
pthread id可以用pthread_self()来获取,获取的不是LWP,而是用户层的一个指针。通过gettid可以获得线程id,但是要调用syscall接口。
在线程函数return,与pthread_exit,他们的返回对象都不能是局部对象,不然会线程函数结束局部对象销毁了。
返回需要是堆上数据:
一般来说,返回一个常数时,它可以拷贝似的被外部接受,而且我们通常让线程去做数据处理的时候可以把数据放在静态区,主线程做数据汇总,这样从线程返回值就可以返回一个描述线程返回结果的数字,但是如果是大块内存数据需要返回,就可以申请到堆上返回。
在线程函数pthread_exit:
功能:线程终止 原型 void pthread_exit(void *value_ptr); 参数 value_ptr:
value_ptr不要指向一个局部变量
。 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
pthread_cancel取消其他线程,最好用于主线程取消其他线程。
pthread_cancel,线程内部调用pthread_cancel或者其他线程调用,被杀掉的线程返回-1。
功能:取消一个执行中的线程 原型 int pthread_cancel(pthread_t thread); 参数 thread:线程ID 返回值:成功返回0;失败返回错误码
#include
#include
#include
#include
void* Thread1(void* arg)
{
//int count = 5;
while(true)
{
printf("ids[i]: %d\n",*(int*)arg);
sleep(1);
}
return nullptr;
}
int main()
{
#define NUM 2
pthread_t ids[NUM];
for(int i = 0; i < NUM;++i)
{
printf("thread ids is:%p\n",ids[i]);
pthread_create(ids+i,nullptr,Thread1, new int(i));
}
//停留5s后杀死从线程
sleep(5);
pthread_cancel(ids[0]);
sleep(5);
pthread_cancel(ids[1]);
while(1)
{
printf("main thread running!\n");
sleep(1);
}
return 0;
}
结果:正常运行,主线程杀掉其他两个死循环的任务。
从线程可以删掉主线程吗?
是可以做到的,不过会导致主进程状态变为僵尸进程
,此时无法被回收。站在OS,线程还活着,进程对他来说就没死,主进程虽然僵尸,但是两个线程还在跑,两个线程看到主线程并没有死,只是变成僵尸进程了。
所以我们不要这样做,这其实算一个bug。
杀掉主线程测试:
#include
#include
#include
#include
//记录主线程并给其他线程看到,所以不能定义在栈上,这里采用全局变量,在未初始化数据区当中。
pthread_t main_thread;
void* Thread1(void* arg)
{
int count = 5;
while(true)
{
if(count -- == 0)
{
//杀掉主线程流
pthread_cancel(main_thread);
}
printf("ids[i]: %d\n",*(int*)arg);
sleep(1);
}
return nullptr;
}
int main()
{
#define NUM 2
main_thread = pthread_self();
pthread_t ids[NUM];
for(int i = 0; i < NUM;++i)
{
printf("thread ids is:%p\n",ids[i]);
pthread_create(ids+i,nullptr,Thread1, new int(i));
}
//停留5s后杀死从线程
sleep(5);
pthread_cancel(ids[0]);
sleep(5);
pthread_cancel(ids[1]);
while(1)
{
printf("main thread running!\n");
sleep(1);
}
return 0;
}
在从线程的上下文,实际上也有可能出现僵尸状态。 线程终止后,一般也要等待,不然会出现类似僵尸进程的现象。且不容易观测。
代码:
#include
using namespace std;
#include
#include
void* ThreadRun(void* arg)
{
printf("child running!\n");
return nullptr;
}
int main()
{
pthread_t tid;
printf("pid is :%p\n",tid);
//不传参
pthread_create(&tid,nullptr,ThreadRun,nullptr);
printf("pid is :%p\n",tid);
while(1)
{
printf("father is running!\n");
sleep(1);
}
return 0;
}
结果:
nil是一种想把这个对象释放掉的一种状态。 后面pid变成一个比较大的值,实际上这个值是一个地址,指向的是一个共享内存的空间。
线程是不可以替换的,调用exec*系列函数会导致整个进程被替换。
#include
using namespace std;
#include
#include
#include
void PrintTable(sigset_t* table)
{
for(int i = 1; i<= 31;++i)
{
if(sigismember(table,i))
{
printf("1");
}
else{
printf("0");
}
}
printf("\n");
}
void handler(int signo)
{
int count = 5;
while(count--)
{
sleep(1);
printf("onther thread handlering!\n");
}
}
void* ThreadRun(void* args)
{
printf("other thread running \n");
execl("/bin/ls","-l",nullptr);
printf("other thread end\n");
}
#include
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRun,nullptr);
while(1)
{
sleep(1);
printf("main running!\n");
}
return 0;
}
结果:
进程替换后,所有线程的代码都被替换。
为什么要join等待从线程
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用未退出线程的地址空间。
join等待的默认方式
pthread_join只能阻塞式等待,若需要快速处理,可以用detach分离。
测试pthread_join之后是否能够将从线程的退出状态获取。
编码建议
由于从线程的退出状态也需要被主线程获取,或者说从线程不能在主线程退出后继续工作,因为主线程退出就会将所有的资源都进行释放,此时从线程即使分离(pthread_detach)于主线程也会受到影响,所以线程等待函数是以阻塞方式进行的。所以我们编码时要让主线程尽量最后一个退出。
注意:推荐线程退出使用return或者pthread_self(),不推荐使用pthread_cancel()进行退出,即使要用,也推荐在主线程退掉其他线程的时候使用,并且在从线程运行一段时间再使用!!
函数原型+参数介绍
功能:等待线程结束 原型 int pthread_join(pthread_t thread, void **value_ptr); 参数 thread:线程ID value_ptr:它指向一个指针,后者指向线程的返回值 返回值:成功返回0;失败返回错误码
第一个参数选择等待的线程ID(用户的),第二个参数可以获得线程退出的退出码,也可以自定了结构体获取更多的信息。
返回值:成功返回0,错误返回退出码。
线程运行函数的返回值实际上就是写道ptrhead_join的输出型和参数当中的,线程退出也是先将返回值写进PCB,再有主线程调用pthread_join去该PCB的对应字段获取内容。
理解pthread_join的第二个参数式void **,由于要拿到线程退出函数的结果,需要定义void*变量,传该变量的地址,才能从线程函数获得结果。
#include
using namespace std;
#include
#include
#include
void PrintTable(sigset_t* table)
{
for(int i = 1; i<= 31;++i)
{
if(sigismember(table,i))
{
printf("1");
}
else{
printf("0");
}
}
printf("\n");
}
void handler(int signo)
{
int count = 5;
while(count--)
{
sleep(1);
printf("onther thread handlering!\n");
}
}
void* ThreadRun(void* args)
{
signal(2,handler);
while(1)
{
sleep(1);
printf("other thread running!\n");
sigset_t pending;
sigpending(&pending);
PrintTable(&pending);
}
}
int main()
{
pthread_t tid ;
pthread_create(&tid,nullptr,ThreadRun,nullptr);
sigset_t block;
sigemptyset(&block);
sigaddset(&block,2);
sigprocmask(SIG_BLOCK,&block,nullptr);
while(1)
{
//打印pending表
sigset_t pending;
sigpending(&pending);
PrintTable(&pending);
sleep(1);
printf("main runing!\n");
}
return 0;
}
一段伪代码解释一下:
join如何拿到返回值
int pthread_join(pthread_t thread,void** retval)
{
//通过thread定位到要等待线程的数据
//该线程有一个字段保存void*返回值,函数运行的返回值
//假设线程保存退出结果的字段叫return_val
*retval = thread->return_val;
}
void main()
{
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRun,nullptr);
void* ret;
pthread_join(tid,&ret);
//ret 此时就是拿到了退出结果。
}
代码:
将返回值设置为结构体获取批量信息。
#include
using namespace std;
#include
#include
struct Status
{
Status(int _code,int _runtime)
:code(_code)
,runtime(_runtime)
{}
int code;
int runtime;
};
void* ThreadRun(void* args)
{
printf("other running and quit!\n");
//error 这里局部变量出了作用域就销毁了
//struct Status a(11,22);
struct Status* a = new Status(11,22);
return (void*)a;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRun,nullptr);
//sleep(2);//主线程停止1s,给从线程时间返回
void* res;
pthread_join(tid,&res);
//res为退出状态结构体
struct Status* s = (struct Status*)res;
printf("code:%d , runtime:%d\n",s->code,s->runtime);
return 0;
}
返回值的妙用:
测试:
当主线程调用pthread_cacel进行退出获取退出码的情况。
#include
using namespace std;
#include
#include
void* ThreadRun(void* arg)
{
printf("child running!\n");
return (void*)22;
}
int main()
{
pthread_t tid;
printf("pid is :%p\n",tid);
//不传参
pthread_create(&tid,nullptr,ThreadRun,nullptr);
pthread_cancel(tid);
printf("pid is :%p\n",tid);
printf("main thread get other thread message!\n");
void* rev;
pthread_join(tid,&rev);
printf("other thread quit code is:%d\n",(int)rev);
return 0;
}
结果:
获取的退出状态此时是-1而不是22,这说明退出时并不是正常退出。这种情况是因为主线程已经杀掉了从线程,而从线程还没开始调度,而引发的歧义。
测试2:
主线程创建之后,等待2s后再让进程退出
此时主线程可以获取从线程的退出结果。这种方式是比较符合语义的,因为创建线程一定是要让线程先跑,随后才终止线程。
测试:
在从线程函数内部用pthread_self()终止自己,子线程join观察退出状态。
void* ThreadRun(void* arg)
{
printf("child running!\n");
pthread_cancel(pthread_self());
return (void*)22;
}
int main()
{
pthread_t tid;
printf("pid is :%p\n",tid);
//不传参
pthread_create(&tid,nullptr,ThreadRun,nullptr);
//sleep(2);//主线程停止1s,给从线程时间返回
printf("pid is :%p\n",tid);
printf("main thread get other thread message!\n");
void* rev;
pthread_join(tid,&rev);
printf("other thread quit code is:%d\n",(int)rev);
return 0;
}
结果:
退出状态还是22,说明从线程执行的时候执行完pthread_cancel后还继续return了,说明pthread_cancel是一个延时性的函数。
测试:
从线程等待两秒后退出,观察结果
此时退出码是-1,这个退出码是一个宏,是一个((void*)-1)
小结论:
线程终止pthread_cancel其实真心不好用,一般用在主线程杀掉从线程。
注意:
1.线程当中发生异常等待就没有意义了,线程发生异常后会将进程杀死,所以线程当中只能出现两种状态:
2.线程不同的退出方式对线程等待的影响:
*return,pthread_exit(void p);**的作用相同。
pthread_cancel尽量不使用,使用要注意线程要先跑起来。
为什么要分离:
分离的本质,是让主线程不用再join新线程,让新线程退出的时候,自动回收资源。
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
如何分离比较好:
总结:在主线程pthread_detach会好一些,在线程内部pthread_detach可能main线程join的太早会出现一些问题。
例子:
线程的分离类似70年代的人分家,儿子跟老爸说出去单过,或者老爸跟儿子说搬出去过。
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
测试:
为了避免产生时序性问题,sleep让从线程先分离自己,此时主线程pthread_join的返回值不是0就是join失败。
分离之后不用join,这里只是给一个错误,主线程退出,从线程也会退出。
#include
using namespace std;
#include
#include
#include
void* ThreadRun(void* args)
{
pthread_detach(pthread_self());
while(1)
{
sleep(1);
printf("other thread running!\n");
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRun,nullptr);
sleep(1);//避免产生时序性
int ret = pthread_join(tid,nullptr);
cout << ret <<endl;//这里要看到失败的结果
return 0;
}
测试2:
主线程拥有相分离线程的id号,由主线程进行分离,可以不会像上面一样考虑会不会运行二义性。
#include
using namespace std;
#include
#include
#include
void* ThreadRun(void* args)
{
while(1)
{
sleep(1);
printf("other thread running!\n");
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRun,nullptr);
pthread_detach(tid);
int ret = pthread_join(tid,nullptr);
cout << ret <<endl;//这里要看到失败的结果
return 0;
}
测试:
当从线程已经分离了,发生错误,接受到信号,验证是否会影响主进程。
#include
using namespace std;
#include
#include
#include
void* ThreadRun(void* args)
{
while(1)
{
sleep(1);
printf("other thread running!\n");
int* p;
*p = 1024;
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRun,nullptr);
pthread_detach(tid);
sleep(3);
int ret = pthread_join(tid,nullptr);
cout << ret <<endl;//这里要看到失败的结果
return 0;
}
结果:
从线程分离后发生错误,依旧会影响到主进程。所以编码上若不想线程错误导致进程出错可以适当捕捉进程信号。
pending信号集是所有线程共享的,当向进程发送信号时,谁先切换上下文就由哪个进程来处理。通常能观测到主线程处理,但是这个是不一定的,因为信号屏蔽字是私有的,当屏蔽主线程的信号屏蔽字,就由从线程处理信号了。
若在主线程流先屏蔽了些许信号,再创建从线程,则从线程默认会将对应的信号屏蔽。反之先创建后屏蔽大家就看不到彼此的信号屏蔽字了。
线程的管理工作的结构体都在应用层由库帮我们处理。线程库不负责真正的操作系统创建线程,而是给用户提供线程的接口,描述线程属性的结构体字段,保存临时数据的区域。创建线程的方法则是用创建轻量级进程的接口。
用户层定义的pthread_t tid,实际上tid是一个很大的值,并且与LWP不相同,类似是一个指针。LWP是操作系统负责管理轻量级进程的编号。pthread_t 则是应用层管理库对应线程的标识符。
代码:
#include
#include
#include
#include
void* Thread1(void* arg)
{
int count = 5;
while (count--)
{
printf("ids[i]: %d ,count :%d\n", *(int*)arg, count);
sleep(1);
}
return nullptr;
}
int main()
{
#define NUM 5
pthread_t ids[NUM];
for (int i = 0; i < NUM; ++i)
{
printf("thread ids is:%p\n", ids[i]);
//解决方案 new int(i)
pthread_create(ids + i, nullptr, Thread1, &i);
}
while (1)
{
sleep(1);
}
return 0;
}
对应的结果:
观察下面结果,明显主线程跑的很快,其他的线程还没开始跑,主线程就已经全部创建出来,并且ids[i]全部是5,这明显不是我们想要的结果,这是因为程序在传参给线程运行的函数的时候,i已经跑到了5从线程才开始运行,由于传的是i的地址,此时从线程拿到i的地址打印i,i已经是5了,这也表现了线程的健壮性不强。
解决方案 new int(i)
更改过后的结果:
1.Linux操作系统是没有真正意义上的线程的,但是用进程模拟的!为轻量级进程。
2.Linux操作系统本身不会直接提供类似的线程创建,终止,等待,分离等相关的系统调用,但是会提供创建轻量级进程的接口,如vfrok。
3.但是用户需要所谓的线程创建,终止,等待,分离等相关接口,所以,为了更好的适配,系统基于轻量级进程的接口,模拟封装了用户层的原生线程库,pthread。
4.进程PCB是由OS去管理的。用户层至少也得直到线程id,状态,优先级,其他属性来进行用户级用户线程管理。即有tcb结构体由用户空间维护。
对于tid的理解
实际上动态链接时,pthread库就是通过页表映射到共享区,tid的值是一个指针,指向的就是有关一个线程的描述结构体,局部存储相关,线程栈,也就是供一个线程的空间的指针,用户层可以通过指针访问到线程。
图中struct pthread就是tcb结构体,一些线程的属性,局部存储是单独定义变量在线程中使用,可是又不是定义在栈上的,如一些静态的私有的变量。
主线程栈只给主线程使用(进程地址空间的栈),并且主线程不使用库中的栈结构。
从而线程可以单独运行。
用户级线程和LWP是1:1的关系,其他操作系统可能是1:n。
用户级线程要执行的代码给LWP,用户级线程有对应操作修改内核的LWP,删除线程就是删掉LWP,释放共享区里面的数据结构。