目录
一、认识线程
1.认识线程V1
2.认识线程V2
3.认识线程V3
4.认识线程V4
5.认识线程V5
二、线程控制
1.前言
2.创建线程
3.线程等待
4.线程终止
5.线程分离
三、线程理解
一、认识线程
1.认识线程V1
借用大多数计算机教材的话,线程是进程的一个执行分支,线程是CPU调度的基本单位。
多进程的缺点在于,每创建一个进程,就要创建一个PCB对象,一份地址空间,一张页表,想办法把这部分消耗缩小,就只需新建一份"PCB",地址空间和页表共用一份,代码部分再均分,比如有5个进程就把所有调度函数均分为5份,如此一来,CPU并不知道自己是执行多进程,只是在调度一个又一个PCB,而用户看来,CPU却是通过“多线程的方式”提高了效率。
2.认识线程V2
既然如此,多个进程有多个线程,系统中存在大量线程,必然需要描述、组织,然而,Linux下,并没有再单独定义线程结构体,而是用进程的结构体来模拟线程。
3.认识线程V3
- 重新认识进程
以前认识进程,这个进程是单执行流,用现在的话说就是内部只有一个线程。
现在认识进程,这个进程内部有多个线程,多个执行分支。
总结的看,进程是系统分配资源的单位,而线程是系统调度的单位,因此,往后理解进程,都要站在系统分配资源的角度去看。
4.认识线程V4
-
cpu调度角度
现在来看,有的进程只有一个执行流,cpu调度可以称为调度进程,而有的进程有多个执行流,cpu调度时称为调度线程。
Linux下,为了统一这种含糊不清的概念,把cpu调度的执行流统称为轻量级进程。线程<= 执行流 <= 进程。因此,cpu不再区分自己到底是在线程调度还是进程调度,都称为执行流。
- 内核级虚拟机技术
我们现在看待进程,它是操作系统分配资源的基本单位,可以认为是一个容器,那么如果一个进程所对应的代码部分是一个操作系统,意味着这个操作系统支持内核级虚拟机技术。
5.认识线程V5
- 页表
抛出一个问题,操作系统是怎么给多线程均分代码的?
第一个结论,内存本质上是有限个4KB的内存块,定义为数组,方便增删查改。
第二个结论,页表并不是简单的K-V映射,虚拟地址是有划分的,比如32位的虚拟地址,前10位是一个整体,中间10位是一个整体,后12位一般是页内偏移地址,而页表其实是多张页目录和多张页内偏移表。
第三个结论,多线程划分代码,其实就是让每一个线程拿到自己代码所在的n张页内偏移表。
二、线程控制
1.前言
ps -aL指令查看LWP(light weight process)。
ps -aL
过去,要么是单进程单执行流,要么是多进程多执行流,而学习多线程后,变成了单进程多执行流。
在内核层面,cpu调度的是一个又一个的LWP,只有轻量级进程。
Linux的内核代码没有线程结构体,只有轻量级进程的结构体定义。而在用户层面,要严格区分进程和线程。因此需要对内核的系统调用封装,于是有一个库叫 pthread库,这个库不属于内核代码,但是安装Linux操作系统时必须安装这个库。
2.创建线程
pthread_create
通过man手册可以得知,pthread_create这个函数用来创建一个新的线程,使用这个函数需要包含头文件<pthread.h>,由于在3号手册查到了这个函数,因此说明这个函数不是系统调用,是用户层封装的函数。此外,man手册提示编译和链接时要加特定选项-pthread。
void* HandleTask(void* args)
{//新线程std::string threadname = (char*)args;
} int main()
{//主线程pthread_t tid;pthread_create(&tid,nullptr,HandleTask,nullptr);return 0;
}
3.线程等待
在学习进程时,父进程要等待回收子进程。而线程这快,主线程也要等待新线程。
- 运行成功的多线程程序,主线程一定是最后运行结束的。主线程退出=进程退出,如果主线程的主体代码运行完毕,而新线程还在运行,则主线程需要等待新线程的执行结果。
pthread_join
第二个参数是输出型参数。
void* HandleTask(void* args)
{//新线程std::string threadname = (char*)args;int cnt =5;while(cnt){sleep(1);cnt--;}return (void*)111;
} int main()
{//主线程//创建pthread_t tid;pthread_create(&tid,nullptr,HandleTask,(void*)"name");//等待void* ret = nullptr;int cnt =10;while(cnt){sleep(1);cnt--;}int abc = pthread_join(tid,&ret);std::cout << "abc->" << abc << "new thread ret->" << (long)ret << std::endl;return 0;
}
没有发生异常的情况下,新线程的运行结果是可以通过参数手动获取到的。 如果任何一个线程出现异常(div 0, 野指针),都会导致整个进程退出! 同时说明多线程代码往往健壮性不好。
4.线程终止
- 线程return
- pthread_exit
哪一个线程调用这个函数,哪一个线程就被终止。
pthread_exit((void*)111);
- pthread_cancel
这个函数由主线程调用,用来发送终止信号。
//终止测试sleep(1);pthread_cancel(tid);
返回结果为-1,表明不是正常退出。
5.线程分离
pthread_detach
新线程做线程分离
pthread_detach(pthread_self());
主线程也可以主动分离掉新线程。
pthread_detach(tid);
- 线程分离后,到底发生了什么?什么样的情况需要线程分离?
每一个新线程默认是需要让主线程等待的,即主线程需要调用pthread_join函数。如果主线程不需要关心新线程的执行状态,那么就可以将这个新线程分离。新线程分离后,仍旧和其他线程共享资源,并且保留自己原来的私有资源,但是!如果主线程调用pthread_join去等待这个新线程,是会出错的,即被分离的线程意味着不再需要主线程等待。不管怎么设计程序,都建议让主线程最后一个退出。
三、线程理解
- 多线程相比多进程的优点
创建一个新线程的开销要比一个新进程小得多。
与进程的切换相比,线程切换时操作系统所做的操作要少得多。
1.切换线程时,需要切换上下文的寄存器相对少一点,切换线程,只需切换保存是哪一个线程的寄存器,而切换进程,还要多切换用来保持虚拟地址空间、页表的寄存器。
2.由于cpu缓存技术, 加载内存中代码时是一次性加载多行,而切换进程时,前后的代码内容更大概率不是在相邻存储,更大概率可能要刷新缓存,而多线程共享代码,大概率不需要刷新缓存。
- 线程私有的数据
1.线程的硬件上下文数据,本质是cpu寄存器的值,(这部分数据调度线程)
2.每一个线程都有自己的独立栈结构,(用来保证线程的常规运行)
- 线程共享的数据
1.代码部分、全局数据
2.文件描述符表、页表、进程地址空间等等
- C++11的多线程
语言层的多线程,其实是对pthread库的进一步封装。
- 更深的理解pthread_t
如何标识一个唯一的线程,Linux下有这样两种设计,在内核一层,即操作系统层面,并没有定义线程结构体,而是定义了一个LWP结构体,名为轻量级进程,等同于线程,内核里面LWP是唯一的。但是,Linux内核对线程的各种操作控制并不同于我们理论上学习的线程操作控制,因此,Linux又做封装,即封装好的pthread库,编译链接多线程程序时,必须要链接这个库,本质是第三方库,因为它既不属于语言,也不属于操作系统调用。
pthread_t就是pthread库里面定义的概念。
加载动态库,加载到物理内存后,经页表映射到虚拟地址空间中的共享区,在代码区中执行到pthread_t tid这行代码后,cpu则跳转到共享区(动态库已经加载)对应的定义处执行这行代码。
pthread动态库对线程做了管理,即定义了线程结构体,也使用数据结构控制。
每一个线程都有对应的结构体,这个结构体有的地方也叫tcb,而结构体的起始地址,就是pthread_t tid的值。
上文提到了线程私有的数据之一,就是独立栈结构。也是在pthread库里面的线程结构体中定义的,因为每一个线程都有对应执行的代码,可能会创建局部变量,这些变量就保存在这部分栈结构中。
线程局部存储:全局变量归所有线程共有,如果希望只写一份定义全局变量的代码,但是实际上却是多个进程各有一份数据,就可以用像下面这样定义,这些值保存在线程局部存储空间中。
__thread int IngTime = 0;