目录
线程概念
什么是线程:
在程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线城是“一个进程内部的控制序列”。
一切进程至少有一个执行线程。
线程在进程内部运行,本质是在进程地址空间运行。
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理的分配给每个执行流,就形成了线程执行流。
①Linux操作系统中没有线程的概念,我们所说的创建线程,本质上是在Linux操作系统中创建轻量级进程,因此,在Linux操作系统中,轻量级进程等于线程。
曾经写的代码中也存在线程,就是执行main函数的执行流,我们称之为主线程,程序员创建的线程被称为工作线程。
②pid本质上是轻量级进程即线程的id
在task_struct结构体中,有两个成员变量,分别是pid和tgid。pid为轻量级进程id,也被称为线程id,不同的县城额拥有不同的pid;pgid是轻量级进程组id,也被称为进程id,一个进程当中的线程拥有相同的tgid。
本质上来讲,pid是线程id,但是在之前的代码中,由于只有一个主线程的执行流,因此主线程的pid和tgid是一样的。
线程的共享与独有:
共享的内容:文件描述符表、用户id、用户组id、信号处理方式、当前进程的工作目录
独有的内容:调用栈、寄存器、线程id、errno、信号屏蔽字、调度优先级
调用栈:多个线程共用同一个栈会出现调用栈混乱的问题。主线程使用的是进程虚拟地址空间的那块栈,而其他工作线程则是在进程虚拟地址空间的共享区中为其分配相应的栈空间。
寄存器:每一个线程都是独立被调度的,一个线程在拿到CPU资源后执行它的代码,但是总会有被调度器切换下来的时候,此时该线程就需要将它的执行相关信息通过寄存器保存下来。
errno:每个线程是独立地执行它的代码,如果说某一个线程执行过程中出现错误,操作系统应该将其错误码存放在该线程地errno当中。
信号屏蔽字:就是进程信号处的阻塞位图。每一个线程都是一个task_struct结构体,而阻塞位图就是存在于该结构体中;线程单独被调度,此时收到的信号并不是针对某个线程的。因此,每个线程都要有自己的信号屏蔽字。
调度优先级:既然每个线程是相互独立的,那么它的调度优先级也是独立的。
线程的优缺点:
优点:
1、多线程的程序,拥有多个执行流,合理使用,可以提高程序的运行效率。
注意要合理使用,确保正确的前提下使用多线程才会提高程序的运行效率,否则等于反向提高。
2、多线程程序的线程切换比多进程程序快。
由于线程是共用同一块虚拟地址空间,因此在线程切换的时候,有一些数据就不需要再被切换出去了,因此也就节省了切换所耗费的时间。
3、可以充分发挥多核CPU并行的优势,体现在两个方面:
①计算密集型的程序,可以进行拆分,让不同的线程计算不一样的事情。
②I/O密集型的程序,可以进行拆分,让不同的线程执行不同的I/O操作,可以不用串行运行,提高程序运行效率。
缺点:
1、编写代码的难度更高
线程是通过操作系统进行调度的,而每个线程都是抢占执行的。因此并不清楚线程的执行先后顺序。
2、代码的鲁棒性要求更高
需要合理分配资源,否则可能会由于某一个线程的崩溃导致整个进程随之崩溃,无法运行。
3、线程数量并不是越多好
对于同一台特定的机器,它的CPU的核数是固定的。如果线程过于多,反倒有可能导致过程序的运行效率低下。因为线程过多,线程切换耗费了大量的时间。
一个程序合适要启动多少个线程是要基于某一台机器的具体配置,经过测试后才能得到结论。
4、缺乏访问控制,可能导致程序产生二义性的结果。
5、一个线程崩溃,会导致整个进程退出。
线程控制:
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文件<pthread.h>
链接这些线程函数库时要使用编译器命令的“-lpthread”选项。
线程创建:
接口:
thread:获取i线程标识符(地址),本质上就是线程独有空间的首地址(独有空间指的是进程虚拟地址空间的共享区中为该线程分配的空间)
attr:线程的属性信息,一般传NULL,表示采用默认的线程属性。
start_routine:函数指针,指向的是线程执行的入口函数。(线程执行起来的时候,从该函数开始运行,并不是main函数)
arg:给线程入口函数传递的参数
返回值:成功返回0,失败返回值小于0
1、创建一个线程,并查看线程的相关信息:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4 //线程的入口函数
W> 5 void* pthread_start(void* arg){
6 while(1){
7 printf("i am work thread\n");
8 sleep(1);
9 }
10 }
11 int main(){
12 //创建一个线程
13 pthread_t thread;
14 int ret = pthread_create(&thread, NULL, pthread_start, NULL);
15 if(ret < 0){
16 perror("pthread_create");
17 return 0;
18 }
19 //主线程不退出,观察信息
20 while(1){
21 printf("i am main thread\n");
22 sleep(1);
23 }
24 return 0;
25 }
①使用pstack + 进程号的方式查看调用堆栈,进而来获取线程的相关信息:
②通过top -H -p + 进程号来查看线程的信息:
2、 创建多个线程,通过传变量的值来区分每一个线程。
结果:
我们期望打印的是0,1,2,3,4,可以看到结果和我们期待的不一样。
原因:我们是使用for循环内部的临时变量来作为区分线程的标号,该临时变量是主线程中mian函数栈帧上的一段空间,占4个字节,对于每个线程来说,被创建出来后是抢占式执行的它在访问该内存空间的时候,主线程的for循环已经执行完毕,i对应的空间为5。当然并不是说只会出现这种全5的情况,如果出现出现34555等数据也是正常的,因为线程是抢占式执行的,因此前面创建出来的线程已经拿到了CPU资源,对i进行了访问。
传递这样的变量给工作线程是不安全的。for循环退出后,临时对象i就被销毁了,创建出来的线程却还在访问呢这块内存。
解决方法:
①可以让主线程每隔一秒再打印:
②对于上述的不安全问题,可以在main函数中定义有效的临时变量、全局变量、堆上动态开辟。只推荐第三种:
对入口函数参数的结论:
不要传递临时变量给线程的入口函数
如果给线程入口函数传递了一个堆上开辟的空间,让线程自己释放。
线程终止:
接口1:
作用:谁调用谁
retval:线程退出时,传递给等待线程的退出信息。
1 #include<iostream>
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<pthread.h>
5
6
7 using namespace std;
W> 8 void* pthread_start(void* arg){
9 int count = 5;
10 while(count--){
11 cout << "I am work thread" << endl;
12 sleep(1);
13 }
14 //该函数具有谁调用谁退出的特性,因此工作线程调用后,工作线程退出
15 pthread_exit(NULL);
16 while(1){
17 cout << "work thread dont it exit new" << endl;
18 }
19 }
20 int main(){
21 //创建线程
22 pthread_t tid;
23 // int ret = pthread_create(&tid, NULL, thread_start, NULL);
24 int ret = pthread_create(&tid, NULL, pthread_start, NULL);
25 if(ret < 0){
26 perror("pthread_create");
27 }
28 //主线程不退出,观察状态信息
29 while(1){
30 cout << "I am main thread" << endl;
31 sleep(1);
32 }
33 return 0;
34 }
接口2:
作用:退出某个线程
thread:被终止线程的标识符
pthread_t pthread_self(void):谁调用谁获取自己的线程标识符
验证一:工作线程调用pthread_cancel接口终止自己
情况二:主线程调用pthread_cancel接口终止工作线程
情况三:主线程终止主线程自己
在查看进程状态信息的时候,在主线程退出前可以正常查看,在主线程推出后,工作线程依旧在执行,但是查不到任何相关信息。
在主线程退出前,拿到进程号,然后查看线程信息:
可以看到主线程变成了僵尸状态。但是,主线程在调用phtread_cancel接口后,主线程一定会变成僵尸状态吗?
①pthread_cancel接口后没有代码:
②工作线程可以正常退出:
在工作线程没有执行完毕,主线程终止自己的时候,主线程为僵尸状态。等到工作线程退出后,整个程序也就随之退出了。
线程等待:
从上面的线程终止我们可以得到一个结论:线程退出的时候也有可能变成僵尸状态,其原因是线程的退出信息没有被回收。因此我们就需要通过线程等待来回收线程的退出状态信息。
线程被创建出来的默认属性是joinable属性,退出的时候,以来其他线程来回收资源(主要是回收线程使用到的共享区中的空间)
接口:
thread:线程的标识符
retval:退出线程的退出信息
线程的入口函数代码执行完毕,线程退出的,就是入口函数的返回值。
pthread_exit退出的就是pthread_exit的参数。
pthread_cancel退出的,就是一个宏:PTHREAD_CANCELED
具有阻塞属性
测试:
线程分离:
设置线程的分离属性,一旦线程设置了分离属性,则线程退出的时候,不需要任何人回收资源,操作系统可以回收。
接口:
thread:设置线程分离的线程标识符,可以自己对自己设置分离属性,也可以由其他线程对自己设置分离属性。