之前的进程的创建经常画的图:
当创建1个进程时,系统会为它创建进程控制块(task_struct)、虚拟地址空间(mm_struct)、页表等的一些数据结构,当把磁盘中的数据和代码加载进内存中后,虚拟地址和物理地址通过页表建立映射关系。
此时我们再创建一批"进程",并不创建地址空间,只创建task_struct,共用第1个进程的地址空间,分别执行代码的某个部分,创建的效果如下:
此时创建的就是3个线程,每个task_struct就是1条执行流,
因为是共享进程的地址空间所以线程在进程的地址空间内运行。
重新理解进程
此时从内核角度看进程:进程时承担分配资源的基本实体
。所以当创建1个进程时,系统会创建task_struct、虚拟地址空间、页表,和物理内存构建映射关系,打开进程默认打开的文件、信号等等。
我们之前学的进程都是只有1个task_struct,也就是只有1条执行流。
CPU如何看待task_struct
CPU不管有多少条执行流,我只看task_struct,你task_struct有1条执行流就是单执行流的task_struct,有多执行流,你就是多执行流的task_struct。如下图:
所以,CPU看到的PCB要比传统的进程更加轻量化。
在Linux中,没有真正意义的多线程,因为没有为多线程设计数据结构,而是用进程模拟的。
在操作系统中有很多进程,1个进程中再有几个线程,如果再为线程创建同样的数据结构,那么代价就太大了。反正多线程也是执行任务的,Linux就直接复用了进程控制块,所以Linux中的所有执行流都叫做轻量级进程。
1个task_struct后可能挂着好几个线程,而且共享地址空间,所以tastk_struct后可能是地址空间的一部分。
“pthread_”
打头的
“-lpthread”
选项错误检查:
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
参数说明:
返回值:成功返回0;失败返回错误码
下面用pthread_create来创建线程:
1 #include<stdio.h>
2 #include<pthread.h>
3 #include<unistd.h>
4 void* mythread(void* arg)
5 {
6 while(1)
7 {
8 printf("i am mythread %s\n",(char*)arg);
9 sleep(1);
10 }
11 }
12 int main()
13 {
14 pthread_t tid;
15
16 pthread_create(&tid,NULL,mythread,(void *)"mythread 1");
17 while(1)
18 {
19 printf("i am mian thrad\n");
20 sleep(2);
21 }
22 return 0;
23 }
主线程每隔2秒打印1句,新线程每隔1秒打印1句。看效果:
此时使用ps axj
的命令查看进程信息:
我们只看到了1个进程,进程中有2个线程,这2个线程属于同1个进程。
用ps -aL
显示轻量级进程:
它们的pid是一样的,LWP表示轻量级进程,LWP是不一样的。CPU根据LWP进行调度,系统根据PID来判断是不是同1个进程。
LWP
是不一样的进程调度
,因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程获取线程id
获取线程id有2种方法:
1.创建线程是通过输出型参数
2.使用pthread_self()函数获得
pthread_self函数原型:
pthread_t pthread_self(void);
例:新线程通过调用pthread_self函数获取自己的线程ID,主线程也通过调用pthread_self函数获取自己的线程ID,并通过输出型参数获取新线程的ID。
1 #include<stdio.h>
2 #include<pthread.h>
3 #include<unistd.h>
4 void* mythread(void* arg)
5 {
6 while(1)
7 {
8 printf("i am :%s,pid:%d,id:%p\n",(char*)arg,getpid(),pthread_self());
9 sleep(1);
10 }
11 }
12 int main()
13 {
14 pthread_t tid;
15
16 pthread_create(&tid,NULL,mythread,(void *)"mythread 1");
17 while(1)
18 {
19 printf("i am mian thrad pid:%d,my id: %p,newthread id:%p\n",getpid(),pthread_self(),tid);
20 sleep(2);
21 }
22 return 0;
23 }
如下图:
主线程获得新线程ID和新线程通过pthread_self获得的ID是一样的。
pthread_t是什么类型呢?
Linux中没有意义上的线程,是用进程模拟的,只提供LWP,内核只需要管理LWP,用户使用的线程要由线程库自己来管理。如何管理?先描述,在组织,所以就在线程库中管理。
ldd查看线程依赖的库:
看到依赖的是动态库。前面学动态库的时候,我们知道动态库是当进程运行时被加载到共享区,此时该进程内的所有线程都可以看到这个动态库。
每个线程都有自己的私有栈,主线程用的地址空间的原生栈,其余的线程用的栈就在共享区中开辟的。每个线程都有自己的struct_pthread,里面包含了线程的各种属性,每个线程都有线程局部存储,当中有线程切换时的上下文数据。
所以,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。也就是每个线程在库里的起始地址。
通过这些地址可以找到每个线程。
为什么需要线程等待?
函数原型:
int pthread_join(pthread_t thread, void **value_ptr);
参数说明:
返回值:成功返回0,失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
PTHREAD_ CANCELED。
如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
我让新线程运行5秒后返回,为了便于查看将将退出码设置成特殊值,主线程在死循环打印。
3 #include
4 void* mythread(void* arg)
5 {
6 int n = 0;
7 char* msg=(char*)arg;
8 while(n <= 5)
9 {
10 printf("i am :%s,pid:%d\n",msg,getpid());
11 ++n;
12 sleep(1);
13 }
14 return((void*)11);
15 }
16 int main()
17 {
18 pthread_t tid;
19
20 pthread_create(&tid,NULL,mythread,(void *)"mythread 1");
21 void* ret = NULL;
22 pthread_join(tid,&ret);
23 while(1){
24 printf("i am mian thrad pid:%d,exit code: %d\n",getpid(),(long int)ret);
25 sleep(1);
26 }
27 return 0;
28 }
从这里可以得出主线程是在阻塞式等待的。
之前进程时有3种状态:
1.代码跑完,结果正确
2.代码跑完,结果错误
3.代码异常终止
线程也是跟进程一样,而关于线程的等待,只默认退出码是正确的,出错整个线程就挂了,出现错误就是进程的问题。我们不知道是具体哪个线程出现问题,只知道整个进程退出了。
例:我让新线程除0,整个进程都挂了
这也说明了线程的健壮性不强。
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
return退出
先让新线程运行5秒后退出:
1 #include
2 #include
3 #include
4 void* mythread(void* arg)
5 {
6 printf("i am :%s,pid:%d,id:%p\n",(char*)arg,getpid(),pthread_self());
7 sleep(5);
8 return (void*)1;
9 }
10 int main()
11 {
12 pthread_t tid;
13
14 pthread_create(&tid,NULL,mythread,(void *)"mythread 1");
15 while(1)
16 {
17 printf("i am mian thrad pid:%d,my id: %p,newthread id:%p\n",getpid(),pthread_self(),tid);
18 sleep(1);
19 }
20 return 0;
21 }
看到新线程退出后,主线程还在跑。那我们让主线程退出,看看新线程会不会继续跑:
1 #include<stdio.h>
2 #include<pthread.h>
3 #include<unistd.h>
4 void* mythread(void* arg)
5 {
6 while(1)
7 {
8 printf("i am :%s,pid:%d,id:%p\n",(char*)arg,getpid(),pthread_self());
9 sleep(1);
10 }
11 //return (void*)1;
12 }
13 int main()
14 {
15 pthread_t tid;
16
17 pthread_create(&tid,NULL,mythread,(void *)"mythread 1");
18 printf("i am mian thrad pid:%d,my id: %p,newthread id:%p\n",getpid(),pthread_self(),tid);
19 sleep(5);
20 return 0;
21 }
pthread_exit函数
函数原型:
void pthread_exit(void *value_ptr);
参数说明:
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
例:让新线程运行5秒后退出
1 #include<stdio.h>
2 #include<pthread.h>
3 #include<unistd.h>
4 void* mythread(void* arg)
5 {
6 int n = 0;
7 char* msg=(char*)arg;
8 while(n <= 5)
9 {
10 printf("i am :%s,pid:%d\n",msg,getpid());
11 ++n;
12 sleep(1);
13 }
14 pthread_exit ((void*)11);
15 }
16 int main()
17 {
18 pthread_t tid;
19
20 pthread_create(&tid,NULL,mythread,(void *)"mythread 1");
21 void* ret = NULL;
22 pthread_join(tid,&ret);
23 while(1){
24 printf("i am mian thrad pid:%d,exit code: %d\n",getpid(),(long int)ret);
25 sleep(1);
26 }
pthread_cancel函数
函数原型:
int pthread_cancel(pthread_t thread);
参数说明:
thread:线程ID
返回值:成功返回0;失败返回错误码
例:让主线程取消新线程
1 #include<stdio.h>
2 #include<pthread.h>
3 #include<unistd.h>
4 void* mythread(void* arg)
5 {
6 int n = 0;
7 char* msg=(char*)arg;
8 while(n <= 5)
9 {
10 printf("i am :%s,pid:%d\n",msg,getpid());
11 ++n;
12 sleep(1);
13 }
14 pthread_exit ((void*)11);
15 }
16 int main()
17 {
18 pthread_t tid;
19
20 pthread_create(&tid,NULL,mythread,(void *)"mythread 1");
21 pthread_cancel(tid);
22 void* ret = NULL;
23 pthread_join(tid,&ret);
24 while(1){
25 printf("i am mian thrad pid:%d,exit code: %d\n",getpid(),(long int)ret);
26 sleep(1);
27 }
28 return 0;
29 }
1.默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
2.如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
3.可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离,joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
函数原型:
int pthread_detach(pthread_t thread);
参数说明:
thread分离线程的ID
例:我们让新线程分离
3 #include<unistd.h>
4 void* mythread(void* arg)
5 {
6 int n = 0;
7 char* msg=(char*)arg;
8 while(n <= 5)
9 {
10 printf("i am :%s,pid:%d\n",msg,getpid());
11 ++n;
12 sleep(1);
13 }
14 pthread_detach(pthread_self());
15 pthread_exit ((void*)11);
16 }
17 int main()
18 {
19 pthread_t tid;
20
21 pthread_create(&tid,NULL,mythread,(void *)"mythread 1");
22 while(1){
23 printf("i am mian thrad pid:%d\n",getpid());
24 sleep(1);
25 }
26 return 0;
27 }
新线程退出后系统自动回收线程对应的资源,不需要主线程join。
优点:
缺点
1.性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型
线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的
同步和调度开销,而可用的资源不变。
2.健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了
不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
3.缺乏访问控制:
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4.编程难度提高:
编写与调试一个多线程程序比单线程程序困难得多
线程异常:
1.单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
2.线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途:
1.合理的使用多线程,能提高CPU密集型程序的执行效率
2.合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)但不是越多越好。