免费做电子目录的网站,wordpress不加载样式表,和创客贴类似的网站,网站跳转域名不变提示#xff1a;文章写完后#xff0c;目录可以自动生成#xff0c;如何生成可参考右边的帮助文档 目录 
文章目录 
前言 
线程的概念 
线程的理解(Linux系统为例) 
在Linux系统里如何保证让正文部分的代码可以并发的去跑呢#xff1f; 
为什么要有多进程呢#xff1f; 
为… 提示文章写完后目录可以自动生成如何生成可参考右边的帮助文档 目录 
文章目录 
前言 
线程的概念 
线程的理解(Linux系统为例) 
在Linux系统里如何保证让正文部分的代码可以并发的去跑呢 
为什么要有多进程呢 
为什么要这么设计Linux线程 
线程的优点 
线程的缺点 
线程异常 
线程用途 
Linux进程 VS 线程 
进程和线程 
进程的多个线程共享 
进程和线程的关系 
关于调度的问题 
再次谈谈进程地址空间 
多个执行流是如何进行代码划分的如何理解 
OS如何管理内存呢 
线程的控制 
POSIX线程库 
创建线程 
PID和LWP 
Linux中有没有真线程呢 
线程ID及进程地址空间布局 
线程终止 
线程等待 
分离线程 
面试题 
多线程创建 
总结 前言 
世上有两种耀眼的光芒一种是正在升起的太阳一种是正在努力学习编程的你!一个爱学编程的人。各位看官我衷心的希望这篇博客能对你们有所帮助同时也希望各位看官能对我的文章给与点评希望我们能够携手共同促进进步在编程的道路上越走越远 提示以下是本篇文章正文内容下面案例可供参考 
线程的概念 
线程是进程内部的一个执行分支线程在进程的地址空间内运行。线程是CPU调度的基本单位CPU在调度的时候会有很多进程和线程混在一起但是CPU不管这些在调度的时候都是让task_struct进行排队的CPU只调度task_struct所以说线程是CPU调度的基本单位是对的。 
线程的理解(Linux系统为例) 
在一个程序里的一个执行路线就叫做线程thread。更准确的定义是线程是“一个进程内部的控制序列”一切进程至少都有一个执行线程线程在进程内部运行本质是在进程地址空间内运行在Linux系统中在CPU眼中看到的PCB都要比传统的进程更加轻量化透过进程虚拟地址空间可以看到进程的大部分资源将进程资源合理分配给每个执行流就形成了线程执行流 正文代码段(区)我们的代码在进程中全部都是串行调用的。就一个进程正文部分有很多对应的函数但我们在执行的时候所有的函数调用都是串行调用的。比如main()函数中有a、b、c、d四个函数我们单进程执行main()函数时所有的函数都是串行跑的那么今天我们想办法将代码拆成两部分a、b函数一部分c、d函数一部分让一个执行流执行a、b让另一个执行流执行c、d函数如果a、b和c、d函数没有明显的前后关系的话分成两个执行流让它能跑那么此时我们的函数调用过程就是并行跑了。 
无论是多进程还是多线程它的核心思想把串行的东西变成并行的东西。 
在Linux系统里如何保证让正文部分的代码可以并发的去跑呢 
以前再创建进程PCB、进程地址空间、页表再从磁盘中向物理内存中加载新的程序经过新创建进程的页表与物理内存建立映射关系此时就有了独立的代码和数据独立的内核数据结构所以这两个进程是独立的。但是我们发现按照之前的做法进程创建的成本(时间和空间)是非常高的。用户想要的是多执行流所以Linux创建了一个线程假设正文部分有很多的代码想办法将代码分为若干份区域比如三份区域进程地址空间中的其它区域可以都看到再创建一个执行流的时候不用创建地址空间和页表只需要在地址空间内创建两个新的task_struct让两个新的task_struct指向同一块进程地址空间那么它们就能看到同一份地址空间的资源让A进程用第一个区域让B进程用第二个区域让C进程用第三个区域那么CPU调度的时候拿着三个task_struct把当前进程的串行执行的三份代码变成了并发式执行这三份代码了所以我们把这种在地址空间内创建的进程把它叫做线程。 
进程地址空间上布满了虚拟地址进程地址空间以及上面的虚拟地址的本质是一种资源。 
我们之前说的代码可以并行或并发的去跑比如父子进程的代码是共享的数据写实拷贝各自一份所以可以让父子执行不同的代码块这样就可以将代码块进行两个各自调度运行了。 
为什么要有多进程呢 
目标不是为了多进程是为了多执行流并发执行为了让多个进程之间可以并发的去跑相同或不同的代码。 
为什么要这么设计Linux线程 
线程跟进程一样也是要被调度的。 线程在一个进程内部就意味着一个进程内部可能会存在很多个线程。 如果我们要设计线程OS也要对线程进行管理先描述再组织。描述线程线程控制块(struct TCB)要保证线程被OS管理比如用链表将线程管理还要保证进程PCB和这些线程进行关联PCB中的对应的指针指向对应的线程但是这样是非常复杂的。 管理线程的策略和进程是非常像的OS要求我们对应的线程在进程内运行是进程内的执行分支只要符合这个特点就都是线程并不一定必须上面的实现。管理进程已经设计数据结构设计调度算法还写了创建、等待、终止等各种接口那么可以把进程的数据结构和调度算法等代码复用起来。Linux的设计者认为进程和线程都是执行流具有极度的相似性没有必要单独设计数据结构和调度算法直接复用代码。使用进程来模拟线程。Windows是单独的设计了线程模块。Linux用的是复用进程的代码来设计的。 
线程的优点 
创建一个新线程的代价要比创建一个新进程小得多与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多线程占用的资源要比进程少很多能充分利用多处理器的可并行数量在等待慢速I/O操作结束的同时程序可执行其他的计算任务计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作 
线程的缺点 
性能损失 
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的 同步和调度开销而可用的资源不变。 
健壮性降低 
编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。 
缺乏访问控制 
进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。 
编程难度提高 
编写与调试一个多线程程序比单线程程序困难得多 
线程异常 
单个线程如果出现除零野指针问题导致线程崩溃进程也会随着崩溃线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该 进程内的所有线程也就随即退出 
线程用途 
合理的使用多线程能提高CPU密集型程序的执行效率合理的使用多线程能提高IO密集型程序的用户体验如生活中我们一边写代码一边下载开发工具就是 多线程运行的一种表现 Linux进程 VS 线程 
进程和线程 
以前的进程一个内部只有一个线程的进程。今天的进程一个内部至少有一个线程的进程。我们以前的讲的进程是今天讲的进程的一种特殊情况。 什么是进程呢 内核的数据结构进程的代码和数据(也就是一个或者多个执行流、进程地址空间、页表和进程的代码和数据)线程(task_struct)叫做进程内部的一个执行分支。线程是调度的基本单位。进程的内核角度承担分配系统资源的基本实体。不要站在调度角度理解进程而应该站在资源角度理解进程。 线程共享进程数据但也拥有自己的一部分数据 线程ID一组寄存器栈errno信号屏蔽字调度优先级 讲一个故事 
我们的社会就是一个大的系统在社会中承担分配社会资源(汽车、彩电等)的基本实体是家庭家庭中的每一个人都是一个执行流各自都做着不同的事情但每一个人都会互相协作起来完成一个公共的事情把日子过好。家庭中的每一个人就是线程家庭就是一个进程。 
进程的多个线程共享 共享同一地址空间因此代码段(Text Segment)、数据段(Data Segment)都是共享的 如果定义一个函数在各线程中都可以调用如果定义一个全局变量在各线程中都可以访问到 除此之外各线程还共享以下进程资源和环境 文件描述符表每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)当前工作目录用户id和组id 进程和线程的关系 关于调度的问题 
CPU在选择执行流去调度的时候用不用区分一个进程内部唯一的执行流呢还是一个进程内部某一个执行流呢 不需要管。因为线程也有PCB、进程地址空间、页表、进程代码和数据与进程一致都是执行流不需要区分。 
CPU调度的执行流线程  执行流  进程Linux是用进程模拟的线程所以Linux系统里严格意义上讲不存在物理上的真正的线程因为没有单独为线程创建struct TCB。Linux中所有的调度执行流都叫做轻量级进程。 
再次谈谈进程地址空间 
多个执行流是如何进行代码划分的如何理解 
如果进程执行的代码也是一个操作系统这就相当于使用了进程的壳子完成了一种内核级虚拟机的技术。OS要不要管理内存呢大部分OS中的内存在系统角度是以4KB为单位的内存块。一个可执行程序加载是以平坦模式把整个代码进行编址的对应的可执行程序也在内部按地址划分成4KB的数据块。可执行程序里的二进制代码只要写到文件系统里天然就是4KB的。从此磁盘和物理内存进行交互时就以数据块为单位这就叫做4KB数据块。内存中的4KB数据块叫做空间磁盘中文件的4KB数据块叫做内容所谓的加载就是将内容放入空间当中在OS的术语里一般我们把4KB的空间或内容叫做页框或者页帧。 
OS如何管理内存呢 
先描述再组织用struct page结构体来描述内存中的4KB数据块假设有万4GB4GB内存中有100多个4KB的数据块用struct page mem[100多万]数组来组织天然的每一个4KB就有了它的编号(物理地址)编号就是下标对内存进行管理就是对该数组的增删查改。未来加载程序时有多少个4KB的数据块要加载我们就在内存当中申请多少个数据块将程序数据块中的内容加载到数组下标中的数据块空间中。 
OS进行内存管理的基本单位是4KB。所以以前讲过的父子进程代码共享数据各自私有一份内存块中保存的代码它里面配置的引用计数就是2(父子进程都指向它)所以子进程退出了不影响父进程引用计数--写实拷贝是以4KB为单位进行的不是只以变量为单位进行的像new、malloc申请对象的时候在OS也是以4KB为单位申请空间的。可执行程序没有被加载之前就已经有虚拟地址了加载到物理内存之后程序内部用虚拟地址定位我们的程序用物理地址。 以32位平台下为例 将虚拟地址转换成物理地址 
虚拟地址(32byte)不是铁板一块虚拟地址被OS看成10、10、12三个子区域OS在进程创建、加载时根本就不需要搞一个大页表而只需要从左往右数的前10个比特位(第一个子区域)这10个比特位从全0到全1的取值范围为[01023]/[0~2^10-1]第二个子区域的范围[01023] 
在刚开始创建进程的时候必须给进程创建页表这句话没错但刚开始创建的不是完整的页表我们只需要创建第一个子区域的页表页表中有1024个项查页表时需要先拿虚拟地址的前10个比特位检索这张表这张表叫做页目录。第二个子区域也要创建一张或者多张表这些表才是页表我们把页表和页目录里面的条目叫做页表项页目录里面保存的是二级页表的地址在查页表时先拿虚拟地址的前10个比特位做第一张表的索引再拿虚拟地址的中间10个byte来查页表OS当中最多会存在1024张页表(不可能的)页表中存放的是物理内存中每一个4KB数据块的起始地址假设访问一个页表中存放4KB数据块的起始地址0x1234访问的并不是4KB的数据块而是访问的是4KB里面的某一个区域或字节因为线性地址地址空间的基本单位是1字节的可以0x1234  虚拟地址后的12位(第三个子区域)对应的数据  访问到4KB数据块的全部内容。为什么是12位呢因为2^12就是4KB。虚拟地址的后12位我们称之为页内偏移。所以我们查页表只是用虚拟地址的前20位btye。页表里面保存的是页框的物理地址。页目录占4KB空间页表最多占4MB空间所以整个页表内容我们用4MB就能表示完了。页表中也可以加一些标志位表示对应的数据块是内核用的还是用户用的还有权限等。 
结论给不同的线程分配不同的区域本质就是给让不同的线程各自看到全部页表的子集。就是让不同的线程看到不同的页表。 
线程的控制 
POSIX线程库 
与线程有关的函数构成了一个完整的系列绝大多数函数的名字都是以“pthread_”打头的要使用这些函数库要通过引入头文pthread.h链接这些线程函数库时要使用编译器命令的“-lpthread”选项 
创建线程 
功能创建一个新的线程
原型
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine)(void*), void* arg);
参数参数1输出型参数创建成功会带出新线程id参数2 设置线程的属性attr为NULL表示使用默认属性参数3返回值为void* 参数为void* 的函数指针让新线程来执行这个函数方法参数4传递给线程函数的参数参数会传递到参数3中去
返回值成功返回0失败返回错误码 
内部创建线程之后将来会有两个执行流一个是主线程一个是新创建的线程新创建的线程会回调式的调用参数3(函数指针)。 
错误检查 
传统的一些函数是成功返回0失败返回-1并且对全局变量errno赋值以指示错误。pthreads函数出错时不会设置全局变量errno而大部分其他POSIX函数会这样做。而是将错误代码通过返回值返回pthreads同样也提供了线程内的errno变量以支持其它使用errno的代码。对于pthreads函数的错误 建议通过返回值业判定因为读取返回值要比读取线程内的errno变量的开销更小 
#include iostream
#include pthread.h  // 在Liinux中使用线程要包含头文件
#include unistd.h
#include sys/types.h// 新线程
void* newthreadrun(void* args)
{while (true){std::cout  I am new thread, pid:   getpid()  std::endl;sleep(1);}
}int main()
{pthread_t tid;// 线程id// 创建新线程pthread_create(tid, nullptr, newthreadrun, nullptr);while (true){std::cout  I am main thread, pid:   getpid()  std::endl;sleep(1);}
} 
PID和LWP ps -aL中的L能查看真实存在的多线程(轻量级进程)OS在进行调度的时候用轻量级进程(LWP)的id来进行调度。单进程和多进程在调度的时候也在看LWP因为当每一个进程内部都只有一个执行流时LWP  PID此时调用那个都是一样的。LWP和PID说明PCB里面每一个都包含PIDPID表示PCB属于哪一个进程LWP表明PCB是进程中的那个执行流。getpid()获得进程的pid而不是获取的是LWP的idOS没有直接提供获取LWP的id的系统调用。函数编译完成是若干行代码块每一行代码都有地址(虚拟地址/磁盘上-逻辑地址)函数名是该代码块的入口地址。所有的函数都要按照地址空间统一编址。ps -aL查看的轻量级进程所看到的LWP是线程的id与pthread_create()函数中参数1所得到的新线程的id两者的表现形式是不同的因为LWP是在内核当中来标识一个执行流的唯一性的所以只在OS内使用但是创建线程pthread_create()用的线程是属于线程库所以pthread_create()函数的参数1得到的id是线程库来维护的。这两个id是一对一的一个是在用户层的一个是在内核层的。 
Linux中有没有真线程呢 
没有。Linux中只有轻量级进程。为了保证自己的纯洁性和简洁性所以Linux系统不会有线程相关的系统调用只有轻量级进程的系统调用。为了让用户选择Linux系统为了让用户能正常的使用对应的线程功能Linux设计者在用户和Linux系统之间设计了一个中间的软件层软件层叫做pthread库(原生线程库)任何的Linux内核里面你在安装的时候pthread库必须在Linux系统里自带在系统里默认就装好了pthread库的作用是将轻量级进程的系统调用进行封装转成线程相关的接口语义提供给用户底层其实还是轻量级进程。 用户知道轻量级进程这个概念吗 
没有。用户只认进程和线程。其实轻量级进程就是线程。 
pthread库不属于OS内核只要是库就是在用户级实现的所以Linux的线程也别叫做用户级线程。所以编写多线程时都必须要链接上这个pthread库-lpthread 
testthread:testThread.ccg - o $ $^ -stdc11 -lpthread
.PHONY:clean
clean:rm - f testthread 
线程ID及进程地址空间布局 
pthread_ create函数会产生一个线程ID存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程是操作系统调度器的最小单位所以需要一个数值来唯一表示该线程。pthread_ create函数第一个参数指向一个虚拟内存单元该内存单元的地址即为新创建线程的线程ID 属于NPTL线程库的范畴。线程库的后续操作就是根据该线程ID来操作线程的。线程库NPTL提供了pthread_ self函数可以获得线程自身的ID 
man pthread_self // 那个线程调用pthread_self()函数就获取那个线程的id
pthread_t pthread_self(void); 
pthread_t 到底是什么类型呢取决于实现。对于Linux目前实现的NPTL实现而言pthread_t类型的线程ID本质 就是一个进程地址空间上的一个地址。 线程终止 
如果需要只终止某个线程而不终止整个进程可以有三种方法 
从线程函数return。这种方法对主线程不适用从main函数return相当于调用exit。线程可以调用pthread_ exit终止自己。一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。 功能线程终止 原型     void pthread_exit(void* value_ptr);// 那个线程调用该函数就终止那个线程 参数     value_ptr: value_ptr不要指向一个局部变量。 返回值无返回值跟进程一样线程结束的时候无法返回到它的调用者自身 需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函 数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。 功能取消一个执行中的线程 原型     int pthread_cancel(pthread_t thread); 参数     thread : 线程ID 返回值成功返回0失败返回错误码 线程等待 
为什么需要线程等待 
已经退出的线程其空间没有被释放仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。 man pthread_join   int pthread_join(pthread_t thread, void **value_ptr); 等待一个已经结束的线程 参数1等待指定的一个线程如果该线程没有退出会阻塞式等待若该线程退出了则返回等待的结果参数2输出型参数拿到的是新线程对应的返回值 返回值成功返回0失败返回错误码 调用该函数的线程将挂起等待直到id为thread的线程终止。thread线程以不同的方法终止通过pthread_join得到的终止状态是不同的总结如下 
如果thread线程通过return返回value_ ptr所指向的单元里存放的是thread线程函数的返回值。如果thread线程被别的线程调用pthread_ cancel异常终掉value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED就是-1。如果thread线程是自己调用pthread_exit终止的value_ptr所指向的单元存放的是传给pthread_exit的参数。如果对thread线程的终止状态不感兴趣可以传NULL给value_ ptr参数。 
分离线程 
默认情况下新创建的线程是joinable的线程退出后需要对其进行pthread_join操作否则无法释放资源从而造成系统泄漏。如果不关心线程的返回值join是一种负担这个时候我们可以告诉系统当线程退出时自动释放线程资源。 把一个线程设置成分离 man pthread_detach   int pthread_detach(pthread_t thread); 参数要分离哪一个线程的id 可以是线程组内其他线程对目标线程进行分离也可以是线程自己分离 pthread_detach(pthread_self());  
joinable和分离是冲突的一个线程不能既是joinable又是分离的。 
// 同一个进程内的线程大部分资源都是共享的. 地址空间是共享的
// 比如初始化和未初始化区域、还有正文部分没有被线程分走的其它代码也是这个进程所有的线程共享的。
int g_val  100;// 将新的线程id转换成16进制的形式
std::string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), 0x%lx, tid);return id;
}// 线程退出// 1. 代码跑完结果对// 2. 代码跑完结果不对// 3. 出异常了 --- 重点 --- 多线程中任何一个线程出现异常(div 0 野指针), 都会导致整个进程退出 // ---- 多线程代码往往健壮性不好void *threadrun(void *args){// 将函数的参数传递过来字符串的地址来用于初始化新线程的名字std::string threadname  (char*)args;int cnt  5;while (cnt){printf(new thread, g_val: %d, g_val: %p\n, g_val, g_val);// std::cout  threadname   is running:   cnt  , pid:   getpid()//       mythread id:   ToHex(pthread_self())//      g_val:  g_val   g_val:   g_val  std::endl;g_val;sleep(1);// int *p  nullptr;// *p  100; // 故意一个野指针cnt--;}// 1. 线程函数结束 法1(return)// 2. 法2pthread_exit()// pthread_exit((void*)123);// 终止新线程// exit(10); // 不能用exit终止线程因为它是终止进程的.return (void*)123; // warning}// 主线程退出  进程退出  所有线程都要退出(资源都被释放)// 1. 往往我们需要main thread最后结束// 2. 线程也要被wait, 要不然会产生类似进程哪里的内存泄漏的问题(线程是需要被等待的)int main(){// 1. idpthread_t tid;// pthread_t就是一个无符号的长整型pthread_create(tid, nullptr, threadrun, (void*)thread-1);// 参数1输出型参数得到的是新线程的id   // 法3 // 在主线程中你保证新线程已经启动// sleep(2);// pthread_cancel(tid);// 取消tid线程那么pthread_join()函数拿到的就是线程的退出码-1-1就是宏-1表示这个线程是被取消的// 2. 新和主两个线程谁先运行呢不确定由调度器决定int cnt  10;while (true){std::cout  main thread is running:   cnt  , pid:  getpid()   new thread id:   ToHex(tid)     main thread id:   ToHex(pthread_self()) g_val:  g_val   g_val:   g_val  std::endl;printf(main thread, g_val: %d, g_val: %p\n, g_val, g_val);sleep(1);cnt--;}// 如果主线程比新线程提前退出了呢void* ret  nullptr;// void是不能定义变量的void*能定义变量指针变量是已经开辟了空间的// PTHREAD_CANCELED; // (void*)-1// 我们怎么没有像进程一样获取线程退出的退出信号呢只有你手动写的退出码// 所等的线程一旦产生信号了线程所在的进程就被干掉了所以pthread_join没有机会获得信号。// 所以pthread_join()函数不考虑线程异常情况int n  pthread_join(tid, ret); std::cout  main thread quit, n  n   main thread get a ret:   (long long)ret  std::endl;return 0;} 
新线程所产生的异常由父进程去考虑。 std::string ToHex(pthread_t tid){char id[64];snprintf(id, sizeof(id), 0x%lx, tid);return id;}__thread uint64_t starttime  100;// __thread int tid  0;// 全局变量g_val属于已初始化区域是所有线程共享的资源// __thread让这个进程中所有的线程都私有一份g_val全局变量// __thread编译器在编译时将g_val变量拆分出来放到了每个线程的局部存储空间内int g_val  100;// 主线程一直在等待新线程在等待期间不会创造价值所以有类似于非阻塞等待// 线程是可以分离的: 默认线程是joinable(需要被等待)的。// 如果我们main thread不关心新线程的执行信息我们可以将新线程设置为分离状态:// 你是如何理解线程分离的呢底层依旧属于同一个进程只是不需要等待了// 一般都希望mainthread 是最后一个退出的无论是否是join、detachvoid *threadrun1(void *args){starttime  time(nullptr);// pthread_detach(pthread_self());// 该线程自己分离自己则主线程不会再等待新线程std::string name  static_castconst char *(args);while(true){sleep(1);printf(%s, g_val: %lu, g_val: %p\n, name.c_str(), starttime, starttime);}return nullptr;}void *threadrun2(void *args){sleep(5);starttime  time(nullptr);// pthread_detach(pthread_self());std::string name  static_castconst char *(args);while(true){printf(%s, g_val: %lu, g_val: %p\n, name.c_str(), starttime, starttime);sleep(1);}return nullptr;}int main(){pthread_t tid1;pthread_t tid2;pthread_create(tid1, nullptr, threadrun1, (void *)thread 1);pthread_create(tid2, nullptr, threadrun2, (void *)thread 2);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);// pthread_detach(tid);// 可以由主线程来进行使新线程进行分离// std::cout  new tid:   tid  , hex tid:   ToHex(tid)  std::endl;// std::cout  main tid:   pthread_self()  , hex tid:   ToHex(pthread_self())  std::endl;// int cnt  5;// while (true)// {//     if (!(cnt--))//         break;//     std::cout  I am a main thread ...  getpid()  std::endl;//     sleep(1);// }// std::cout  main thread wait block  std::endl;// 主线程要等待新线程否则会出现类似于僵尸进程的问题// 若是新线程是分离的状态等待的话会出错返回int n  pthread_join(tid, nullptr);std::cout  main thread wait return:   n  :   strerror(n)  std::endl;return 0;} 
面试题 
与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多 
当一个进程被CPU调度时CPU内部一定存在非常多的寄存器寄存器当中保存的都是当前进程运行的上下文数据比如把CPU内部寄存器的值保存到PCB当中下次再调用时再恢复过来寄存器CR3保存了页表的起始地址CPU要切换一个进程的话我们只需要把PCB、进程地址空间、页表切换就行了今天再加了一个线程线程也是调度的实体当切换线程时CPU内部的寄存器中也会有各种的数据那么线程切换也要进行上下文的保护将数据保存到线程PCB当中需要再恢复出来线程切换时进程地址空间和页表不用换进程切换的时候进程地址空间和页表要切换PCB指向进程地址空间只要PCB切换了对应的进程地址空间也就切换了切换一个页表就是切换了CPU中存储页表地址的寄存器CR3两者比较一下线程切换不切换页表进程切换切换一下页表就可以了线程切换需要保存的上下文数据只比进程少一点。(页表不需要切换  寄存器CR3不需要改变) 
在CPU内部每一次我们读取当前进程的代码和数据时CPU上硬件上有一个cachecache是一个集成在CPU内部的一段比寄存器容量大的多的一段缓存区当CPU将虚拟地址转物理地址进行寻址的时候找到物理内存中的代码只找到了一行代码但是下一次大概率还需要这一行代码的下一行代码所以会将这块相关的代码全部搬到CPU内部的cache中缓存起来从此CPU访问代码数据的时候不用从内存中读取了而直接从CPU中较近的cache中读取从而大大提高CPU寻址的效率。 
进程间切换时假设A进程被切换下去那么CPU内部cache中的数据就被清空由新切换上来的B进程来重新填充cache中的代码和数据这个过程很漫长因此进程间的切换成本很高。对于线程切换来说因为进程地址空间、页表、进程的代码和数据都是共享的所以CPU中cache的缓存区中的数据不需要被丢弃所以线程切换的成本要比进程要低。 
一组寄存器 
每个线程都是独立的被单独调度的执行流每个线程都要有一组自己独立的上下文数据。 
线程都有自己的临时变量在C语言中在函数中的临时变量都是在栈区上保存的比如主线程要形成自己的临时变量新线程也要形成自己的临时变量函数调用要压栈和出栈如果两个线程使用的是同一个进程地址空间上的栈区两个都在访问这个栈区如果一个栈区被多个线程共享的话每个线程都要向栈区中压栈入自己的临时数据那么在栈中压入的临时变量无法分清是那个线程的所以库在设计的时候都必须保证给每个线程都要有自己独立的用户栈。每个线程都有自己独立的栈结构。 
哪些属于线程私有的 
线程的硬件上下文(CPU寄存器的值)(调度)线程的独立栈结构(常规运行) 
线程共享 
代码和全局数据进程的文件描述符表 
一个线程出问题导致其它线程也出问题导致整个进程退出---线程安全问题。 
多线程中公共函数如果被多个线程同时进入---该函数被重入了。 
多线程创建 #include iostream
#include string
#include vector
#include cstdio
#include unistd.h
#include cstdlib
#include pthread.h // 原生线程库的头文件const int threadnum  5;class Task
{
public:Task(){}void SetData(int x, int y){datax  x;datay  y;}// 执行的任务int Excute(){return datax  datay;}~Task(){}
private:int datax;int datay;
};class ThreadData : public Task
{
public:ThreadData(int x, int y, const std::string threadname) :_threadname(threadname){_t.SetData(x, y);}std::string threadname(){return _threadname;}int run(){return _t.Excute();}
private:std::string _threadname;Task _t;
};
// 结果
class Result
{
public:Result() {}~Result() {}void SetResult(int result, const std::string threadname){_result  result;_threadname  threadname;}void Print(){std::cout  _threadname   :   _result  std::endl;}
private:int _result;std::string _threadname;
};// 每个线程都会执行这个函数
void* handlerTask(void* args)
{ThreadData* td  static_castThreadData*(args);std::string name  td-threadname();Result* res  new Result();int result  td-run();res-SetResult(result, name);std::cout  name  run result :   result  std::endl;delete td;sleep(2);return res;// 这个函数没有使用全局变量在函数中定义的threadname变量在自己的独立栈上所以多个线程并不影响// 虽然该函数重入了但是函数并不会出问题// // std::string threadname static_castchar*(args);// const char *threadname  static_castchar *(args);// while (true)// {//     std::cout  I am   threadname  std::endl;//     sleep(2);// }// 虽然对于线程来说堆空间是共享的但是每个线程都只能拿到自己堆空间的起始地址其它线程的堆空间看不到// delete []threadname;// return nullptr;
}
// 1. 多线程创建
// 2. 线程传参和返回值我们可以传递基本信息也可以传递其他对象(包括你自己定义的)
// 3. C11也带了多线程和我们今天的是什么关系 
int main()
{std::vectorpthread_t threads;// 创建5个线程for (int i  0; i  threadnum; i){char threadname[64];// 第二次循环时第一次循环时的缓冲区中的数据就被释放掉或者被后来的数据覆盖snprintf(threadname, 64, Thread-%d, i  1);// 将线程名为参数传递给线程函数// 我们不能让每一个线程的threadname的变量都指向同一块缓冲区我们要给每一个线程申请一个属于自己的空间ThreadData* td  new ThreadData(10, 20, threadname);pthread_t tid;pthread_create(tid, nullptr, handlerTask, td);threads.push_back(tid);// 将线程id保存到vector中}std::vectorResult* result_set;// 结果void* ret  nullptr;// 循环等待线程for (auto tid : threads){pthread_join(tid, ret);result_set.push_back((Result*)ret);}for (auto res : result_set){res-Print();delete res;}
} 
新线程处于分离状态新线程无线循环的跑下去主线程5秒之后就退出会发生什么事情呢 
5秒之后主线程和新线程都会退出。因为主线程(man thread)退出代表进程结束那么进程曾经所申请的进程地址空间、页表、代码和数据也会被释放虽然新线程是分离的但是依旧是和主线程共享资源的所谓分离仅仅是主线程不需要再等待新线程了不需要关心新线程的执行结果但资源依旧是共享的。 
新线程处于分离状态新线程无线循环的跑下去但是新线程中会出现异常主线程5秒之后就退出会发生什么事情呢  
异常之后整个进程都会退出。 总结 
好了本篇博客到这里就结束了如果有更好的观点请及时留言我会认真观看并学习。 不积硅步无以至千里不积小流无以成江海。