制作一个网站界面设计图片,wordpress添加数据库表,自动做设计的网站,沈阳推广网站#x1f307;个人主页#xff1a;平凡的小苏 #x1f4da;学习格言#xff1a;命运给你一个低的起点#xff0c;是想看你精彩的翻盘#xff0c;而不是让你自甘堕落#xff0c;脚下的路虽然难走#xff0c;但我还能走#xff0c;比起向阳而生#xff0c;我更想尝试逆风… 个人主页平凡的小苏 学习格言命运给你一个低的起点是想看你精彩的翻盘而不是让你自甘堕落脚下的路虽然难走但我还能走比起向阳而生我更想尝试逆风翻盘。 C专栏Linux内功修炼 家人们更新不易你们的点赞和⭐关注⭐真的对我真重要各位路 过的友友麻烦多多点赞关注。欢迎你们的私信提问感谢你们的转发 关注我关注我关注我你们将会看到更多的优质内容 一、Linux线程概念
1、什么是线程 在一个程序里的一个执行路线就叫做线程thread。更准确的定义是线程是“一个进程内部的控制序列”。 一切进程至少都有一个执行线程。 线程在进程内部运行本质是在进程地址空间内运行。 在Linux系统中在CPU眼中看到的PCB都要比传统的进程更轻量化。所以Linux下的进程称之为轻量级进程。 透过进程虚拟地址空间可以看到进程的大部分资源将进程资源合理分配给每个执行流就形成了线程执行流。
根据我们先前的了解一个进程的创建实际上伴随着其进程控制块task_struct、进程地址空间mm_struct以及页表的创建虚拟地址和物理地址就是通过页表建立映射的。 每个进程都有自己独立的进程地址空间和独立的页表也就意味着所有进程在运行时本身就具有独立性。所以我们在创建进程时它要创建PCB页表建立代码和数据的映射关系…。所以创建一个进程的成本非常高。
如果我们在创建“进程”时只创建task_struct并要求创建出来的task_struct和父task_struct共享进程地址空间和页表那么创建的结果就是下面这样的 现在创建的进程不再给你独立分配地址空间和页表而是都指向同一块地址空间共享同一块页表。所以这四个task_struct看到的资源都是一样的我们后续可以通过某种方式把代码区拆分成4块让这四个task_struct执行不同的代码区域上述的区域数据区堆区栈区也是类似处理方式。换言之我们后续创建的3个task_struct都各自有自己的一小份代码和数据我们把这样的一份task_struct称之为线程。
其中每一个线程都是当前进程里面的一个执行流也就是我们常说的“线程是进程内部的一个执行分支”。同时我们也可以看出线程在进程内部运行本质就是线程在进程地址空间内运行也就是说曾经这个进程申请的所有资源几乎都是被所有线程共享的。线程比进程更细是因为其执行的代码和数据更小了线程的调度成本更低了是因为它将来在调度的时候核心数据结构地址空间和页表均不用切换了
上述谈的线程仅仅是在Linux下的实现原理不同平台对线程的管理可能是不一样的。Linux其实并没有真正的对线程创建对应的数据结构 线程本身是在进程内部运行的操作系统中存在大量的进程一个进程内又存在一个或多个线程因此线程的数量一定比进程的数量多线程 进程 一定是n : 1当线程的数量足够多的时候很明显线程的执行粒度要比进程更细。 对于这么多的线程我们OS需要对其做管理先描述再组织在大部分的OS中线程都有一个tcb。如果我们的系统实现的是真线程比如说windows平台它就要分别对进程和线程设计各自的描述的数据块结构体并且很多线程在一个进程内部所以还要维护线程tcb和进程pcb之间的关系。所以这样写出的代码其tcb和pcb两个数据结构之间的耦合度非常复杂。设计tcb和pcb的人认为这样的进程和线程在执行流层面上是不一样的。但是Linux不这样想在概念上没有进程和线程的区分只有一个叫做执行流。Linux的线程是用进程PCB模拟的。所以在Linux当中其PCB和TCB是一回事
Linux的线程用进程PCB模拟的好处很明显
不用单独设计tcb了Linux认为tcb和pcb的属性上很大部分重叠了不需要单独设计pcb不用维护tcb和pcb之间的关系了。不用在编写任何调度算法了。
答案是没有任何区别CPU调度的时候照样以task_struct为单位来进行调度只是这里task_struct背后的代码和页表只是曾经的代码和页表的一小部分而已。所以CPU执行的只是一小块代码和数据但并不妨碍CPU执行其它执行流。所以我们就可以把原本串行的所有代码而转变成并发或并行的让这些代码在同一时间点得以推进。总结如下以前CPU看到的所有的task_struct都是一个进程现在CPU看到的所有的task_struct都是一个执行流线程
总览如下 看此图对于页表的注释来分析下面的一份代码
char* msg hello world;
*msg H;问上述代码对吗
很明显是错的因为字符串常量不可被修改。这时根据我们先前的学习对此做出的解释。
字符串常量区在代码区和已初始化数据区之间的如果它不可被修改那它是如何加载到物理内存呢或者说是谁保证它不可被修改的
根本原因就是当你尝试进行修改时页表有对应的条目限制你的更改。比如说我字符串常量区经过页表的映射到物理内存当它从虚拟地址到物理地址转换的时候它是只读的所以RWX权限为R所以尝试在修改的时候直接在页表进行拦截并结合mmu内存管理单元识别到只读但尝试修改的异常发出信号随后OS把此进程直接干掉。
问有了线程的引入该如何重新理解之前的进程
曾经我们理解的进程 内核数据结构 进程对应的代码和数据现在的进程站在内核角度上看就是承担分配系统资源的基本实体进程的基座属性。所有进程最大的意义是向系统申请资源的基本单位。
因此所谓的进程并不是通过task_struct来衡量的除了task_struct之外一个进程还要有进程地址空间、文件、信号等等合起来称之为一个进程。换言之当我们创建进程时是创建一个task_struct、创建地址空间、维护页表然后在物理内存当中开辟空间、构建映射打开进程默认打开的相关文件、注册信号对应的处理方案等等。
我们之前接触到的进程内部都只有一个task_struct也就是该进程内部只有一个执行流即单执行流进程
而内部可以有多个执行流的进程我们称之为多执行流进程
所以Linux下没有真正意义上的线程而是用进程task_struct模拟实现的。所以CPU看到的实际上的task_struct实体是要比传统意义上的进程更轻量化的。所以Linux下的“进程” 其它操作系统的进程概念。线程就是调度的基本单位
2、二级页表
我们以32位平台为例在32位平台下一共有232个地址地址空间的单位就是232 * 1字节 4GB。此时如果做地址之间的映射每个虚拟地址都要有对应的物理地址。如果页表只有一张那么需要多少条目页表项呢答案是232个条目即这张表一共有232个映射表项。 每一个表项中除了要有虚拟地址和与其映射的物理地址以外实际还需要有一些权限相关的信息比如我们所说的用户级页表和内核级页表实际就是通过权限进行区分的。 注意
每一个条目可不是只有1个字节保守估计有8个字节那么保存一张页表需要维护2^32 * 8字节 32GB。现在光页表都32GB这么大了我物理内存才多大一张页表干下去我内存还剩什么呢
所以我们实际的页表并不是这样子的我们的页表是多级页表在32位平台下是二级页表。 我们的cpu通过地址空间访问物理内存的时cpu读取指定的数据和代码然后根据指定的地址返回物理内存的时候cpu出来的地址是虚拟地址我们的进程地址空间是2^32个我们的虚拟地址是32位。而虚拟地址在被转化的过程中不是直接转化的而是拆分成了10 10 12
32位平台下虚拟地址映射转化的过程如下 选择虚拟地址的前10个比特位在页目录当中进行查找找到对应的页表。 再选择虚拟地址的10个比特位在对应的页表当中进行查找找到物理内存中对应页框的起始地址。 最后将虚拟地址中剩下的12个比特位作为偏移量从对应页框的起始地址处向后进行偏移找到物理内存中某一个对应的字节数据。
物理内存在划分的时候是按4KB位单位进行划分的这里的4KB叫做页框可执行程序按照虚拟地址空间编译也划分号了4KB这里的4KB叫做页帧。我们的文件系统在和物理内存进行IO的时候其基本单位是块一般是4KB。
我们假设物理内存是4GB大概有4 * 1024 * 1024KB / 4KB 220个页大约100万个页。页框也就是有220个那么OS就要管理他们先描述再组织。因此OS内部用一个struct page这样的数据结构来进行描述通过struct page mem[1024*1024]来组织。此时对内存的管理就变成了对数组的增删查改。
虚拟地址映射过程图示如下 如果页表只有1张要占2^32 / 2^12 2^20条目即使一个条目10字节页表最大也就10M到20M。如果把整个页旋转一下把页目录放上面就相当于一颗多叉树。
上面所说的所有映射过程都是由MMUMemoryManagementUnit这个硬件完成的该硬件是集成在CPU内的。页表是一种软件映射MMU是一种硬件映射所以计算机进行虚拟地址到物理地址的转化采用的是软硬件结合的方式。 总结上述页表这样设计的好处 进程虚拟地址管理和内存管理通过页表 page进行了解耦页表分离了可以实现页表的按需获取没有用到的就不创建分页机制 按需创建页表 节省空间 3、线程优点 创建一个新线程的代价要比创建一个新进程小得多 与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多 线程占用的资源要比进程少很多 能充分利用多处理器的可并行数量 在等待慢速I/O操作结束的同时程序可执行其他的计算任务 计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现 I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作
注意
计算密集型执行流的大部分任务主要以计算为主。比如加密解密、大数据查找等。IO密集型执行流的大部分任务主要以IO为主。比如刷磁盘、访问数据库、访问网络等。
4、线程缺点
性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。健壮性降低 编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。缺乏访问控制 进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。编程难度提高 编写与调试一个多线程程序比单线程程序困难得多。
5、线程异常
单个线程如果出现除零、野指针等问题导致线程崩溃进程也会随着崩溃。线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该进程内的所有线程也就随即退出。
6、线程用途
合理的使用多线程能提高CPU密集型程序的执行效率。合理的使用多线程能提高IO密集型程序的用户体验如生活中我们一边写代码一边下载开发工具就是多线程运行的一种表现。
二、Linux进程VS线程
1、进程和线程
进程是资源分配的基本单位线程是调度的基本单位
线程共享进程数据但也拥有自己的一部分数据:
线程ID一组寄存器有独立的栈结构errno信号屏蔽字调度优先级
2、进程的多个线程共享
因为是在同一个地址空间因此所谓的代码段Text Segment、数据段Data Segment都是共享的 如果定义一个函数在各线程中都可以调用。 如果定义一个全局变量在各线程中都可以访问到。
除此之外各线程还共享以下进程资源和环境
文件描述符表。进程打开一个文件后其他线程也能够看到每种信号的处理方式。SIG_IGN、SIG_DFL或者自定义的信号处理函数当前工作目录。cwd用户ID和组ID。
三、线程控制
1、POSIX线程库
原生线程库pthread 在Linux中站在内核角度没有真正意义上线程相关的接口但是站在用户角度当用户想创建一个线程时更期望使用thread_create这样类似的接口而不是vfork函数因此系统为用户层提供了原生线程库pthread。 原生线程库实际就是对轻量级进程的系统调用进行了封装在用户层模拟实现了一套线程相关的接口。 因此对于我们来讲在Linux下学习线程实际上就是学习在用户层模拟实现的这一套接口而并非操作系统的接口。
pthread线程库是应用层的原生线程库
应用层指的是这个线程库并不是系统接口直接提供的而是由第三方帮我们提供的。原生指的是大部分Linux系统都会默认带上该线程库。与线程有关的函数构成了一个完整的系列绝大多数函数的名字都是以“pthread_”打头的。要使用这些函数库要通过引入头文件pthreaad.h。链接这些线程函数库时要使用编译器命令的“-lpthread”选项。
错误检查 传统的一些函数是成功返回0失败返回-1并且对全局变量errno赋值以指示错误。 pthreads函数出错时不会设置全局变量errno而大部分POSIX函数会这样做而是将错误代码通过返回值返回。 pthreads同样也提供了线程内的errno变量以支持其他使用errno的代码。对于pthreads函数的错误建议通过返回值来判定因为读取返回值要比读取线程内的errno变量的开销更小。
2、线程创建pthread_create
创建线程的函数叫做pthread_create其函数原型如下
#include pthread.h
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);参数说明
thread获取创建成功的线程ID该参数是一个输出型参数。attr用于设置创建线程的属性传入NULL表示使用默认属性。start_routine返回值和参数均为void*的函数指针。该参数表示线程例程即线程启动后要执行的函数。arg传给线程例程的参数。
返回值说明
线程创建成功返回0失败返回错误码。
注意
Linux不能真正意义上的帮我们提供线程的接口但是Linux有原生线程库使用此函数必须在编译时带上 -pthread 选项。
3、获取线程ID pthread_self
常见获取线程ID的方式有两种
创建线程时通过输出型参数获得。通过调用pthread_self函数获得。
pthread_self函数的函数原型如下
pthread_t pthread_self(void);4、线程等待pthread_join
首先需要明确的是一个线程被创建出来这个线程就如同进程一般也是需要被等待的。如果主线程不对新线程进行等待那么这个新线程的资源也是不会被回收的。所以线程需要被等待如果不等待会产生类似于“僵尸进程”的问题也就是内存泄漏。等待线程的函数叫做pthread_join函数原型如下
#include pthread.h
int pthread_join(pthread_t thread, void **retval);参数说明
thread被等待线程的ID。 retval线程退出时的退出码信息。 返回值说明
线程等待成功返回0失败返回错误码。
5、线程终止
如果需要只终止某个线程而不是终止整个进程可以有三种方法
从线程函数return。线程可以自己调用pthread_exit函数终止自己。一个线程可以调用pthread_cancel函数终止同一进程中的另一个线程
方法一从线程函数return
此法我们在上面已经见过就不做演示。
方法二pthread_exit
pthread_exit函数的功能就是终止线程pthread_exit函数的函数原型如下
#include pthread.h
void pthread_exit(void *retval);参数说明
retval线程退出时的退出码信息。
注意
该函数无返回值跟进程一样线程结束的时候无法返回它的调用者自身。pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的不能在线程函数的栈上分配因为当其他线程得到这个返回指针时线程函数已经退出了。
例如在下面代码中我们使用pthread_exit函数终止线程并将线程的退出码设置为1111
#include iostream
#include pthread.h
#include unistd.h
using namespace std;
static void printTid(const char *name, const pthread_t tid)
{printf(%s 正在运行, thread id: 0x%x\n, name, tid);
}
void *startRoutine(void *args)
{const char *name static_castconst char *(args);int cnt 5;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--)){break;}}cout 线程退出啦.... endl;//1、线程退出方式1: 从线程函数直接return/*return (void *)111;*///2、线程退出方式2: pthread_exitpthread_exit((void*)1111);
}
int main()
{pthread_t tid;int n pthread_create(tid, nullptr, startRoutine, (void *)thread1);(void)n;void *ret nullptr;pthread_join(tid, ret);cout main thread join success, *ret: (long long)ret endl;sleep(10);while (true){printTid(main thread, pthread_self());sleep(1);}return 0;
} 这段代码我们也能看出使用pthread_exit只能退出当前子线程不会影响其它线程。
问为何终止线程要用pthread_exit, exit 不行吗
看如下的代码
#include iostream
#include cstring
#include pthread.h
#include unistd.h
#include sys/types.h
#include sys/syscall.h
using namespace std;
__thread int global_value 100;
void *startRoutine(void *args)
{while (true){cout thread pthread_self() global_value: global_value Inc: global_value lwp: syscall(SYS_gettid) endl;sleep(1);break;}exit(1);
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(tid1, nullptr, startRoutine, (void *)thread 1);pthread_create(tid2, nullptr, startRoutine, (void *)thread 2);pthread_create(tid3, nullptr, startRoutine, (void *)thread 3);int n pthread_join(tid1, nullptr);cout n : strerror(n) endl;n pthread_join(tid2, nullptr);cout n : strerror(n) endl;n pthread_join(tid3, nullptr);cout n : strerror(n) endl;return 0;
}总结
exit是退出进程任何一个线程调用exit都表示整个进程退出。无论哪个子线程调用整个程序都将结束。 而pthread_exit的作用是只退出当前子线程记住是只。即使你放在主线程它也会只退出主线程其它线程有运行的仍会继续运行。
方法三pthread_cancel
线程是可以被取消的我们可以使用pthread_cancel函数取消某一个线程pthread_cancel函数的函数原型如下
#include pthread.h
int pthread_cancel(pthread_t thread);参数说明
thread被取消线程的ID。
返回值说明
线程取消成功返回0失败返回错误码。
线程是可以取消自己的取消成功的线程的退出码一般是-1。例如在下面的代码中我们让线程执行一次打印操作后将自己取消
#include iostream
#include pthread.h
#include unistd.h
using namespace std;
static void printTid(const char *name, const pthread_t tid)
{printf(%s 正在运行, thread id: 0x%x\n, name, tid);
}
void *startRoutine(void *args)
{const char *name static_castconst char *(args);int cnt 5;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--)){// break;}}
}
int main()
{pthread_t tid;int n pthread_create(tid, nullptr, startRoutine, (void *)thread1);(void)n;sleep(3);//代表main thread对应的工作cout new thread been canceled endl;pthread_cancel(tid);void *ret nullptr;pthread_join(tid, ret);cout main thread join success, *ret: (long long)ret endl;sleep(10);while (true){printTid(main thread, pthread_self());sleep(1);}return 0;
}为什么退出的结果是-1呢
线程和进程一样用的都是PCB退出时都有自己的退出码调用return或exit就是自己修改PCB中的退出结果退出码取消这个线程时是OS取消的就直接向退出码中写-1。这里的-1就是pthread库里头给我们提供的宏PTHREAD_CANCELED
上述我们做的测试是让main thread主线程去取消新线程new thread不推荐反过来。这里就不做测试了。
6、线程栈 pthread_t
pthread_t实际上就是地址。
线程是一个独立的执行流线程一定会在自己的运行过程中产生临时数据调用函数定义局部变量等线程一定需要有自己的独立的栈结构
线程的独立栈结构
我们使用的线程库是用户级线程库pthread。是因为Linux没有真线程没有办法提供真的线程调用接口只能提供创建子进程、共享地址空间的调用接口。但是进程的代码、数据……怎么划分这些都是由线程库自己维护的。注意此pthread库是动态库。 因为要把此动态库加载到物理内存所以我的磁盘中有如上libpthread.so动态库 mypthread.exe可执行程序。我们在运行时首先要把此可执行程序mypthread.exe加载到内存此程序内部的代码中一定有pthread_createpthread_join这些从libpthread.so动态库里调来的函数所以此时OS把该动态库加载到内存。随后把此动态库经过页表映射到进程地址空间的共享区当中我们的task_truct通过虚拟地址访问代码区然后跳转至共享区内执行相关的创建线程等工作执行后再返回至代码区。所以最终都是在地址空间中的共享区内完成对应的线程创建等操作的。所以在我们的代码中一定充斥着三大部分你的库的系统的。所有的代码都是在进程的地址空间当中进行执行的。
问pthread_t究竟是什么
既然我们已经知道此动态库会被加载到共享区那么我们把此共享区的libpthread.so动态库放大来讨论。线程的全部实现并没有全部体现在OS内而是OS提供执行流具体的线程结构由库来进行管理。如下
操作系统只提供轻量级进程对于用户他不管只要线程。所以在用户和OS之间设计了libpthread.so库用于创建线程等待线程……操作。用户创建一个线程库做了转换让你在系统帮你创建一个轻量级进程用户终止一个线程库帮你终止一个轻量级进程用户等待一个线程库帮你转换成等待一个轻量级进程并且把结果返回。此库起到的就是承上启下的作用。 库可以创建多个线程需要对这些线程进行管理先描述再组织。库里头通过类似struct thread_info的结构体注意里头是有私有栈的来进行管理
struct thread_info
{pthread_t tid;void *stack; // 私有栈...
}当你在用户层每创建一个线程时在库里头就会创建一个线程控制块struct thread_info描述线程的属性。给创建线程的用户返回的是该结构体的起始虚拟地址。所以我们的pthread_t实际上就是用户级线程的控制结构体的起始地址。 既然每一个线程都有struct thread_info结构体而此结构体内部又有私有栈所以结论如下
主线程的独立栈结构用的就是地址空间中的栈区新线程用的栈结构用的是库中提供的栈结构
7、线性的局部存储
我们的线程除了保存临时数据时可以有自己的线程栈我们的pthread给我们了一种能力如果定义了一个全局变量默认所有线程共享但是你想让每个线程各自私有那么我们就可以使用线程局部存储。
如下我们创建了3个线程创建一个全局变量默认情况下此全局变量所有线程共享现在我们来打印此全局变量以及地址来观察现象
#include iostream
#include pthread.h
#include unistd.h
using namespace std;
int global_value 100;
void *startRoutine(void *args)
{while (true){cout thread pthread_self() global_value: global_value global_value: global_value Inc: global_value endl;sleep(1);}
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(tid1, nullptr, startRoutine, (void *)thread 1);pthread_create(tid2, nullptr, startRoutine, (void *)thread 2);pthread_create(tid3, nullptr, startRoutine, (void *)thread 3);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}正常情况下我们观察到着三个线程打印的全局变量地址应该都是一样的且打印的变量是在累加的这是正常的因为共享全局变量我的修改别人也能拿到。 为了让此全局变量独属于各个线程所私有我们只需要给全局变量前假设__thread即可加了这个__thread就会默认把这个global_value再拷一份给每一个进程。
__thread int global_value 100;代码如下
#include iostream
#include pthread.h
#include unistd.h
using namespace std;
__thread int global_value 100;
void *startRoutine(void *args)
{while (true){cout thread pthread_self() global_value: global_value global_value: global_value Inc: global_value endl;sleep(1);}
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(tid1, nullptr, startRoutine, (void *)thread 1);pthread_create(tid2, nullptr, startRoutine, (void *)thread 2);pthread_create(tid3, nullptr, startRoutine, (void *)thread 3);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}如下可以看到创建的3个线程每个线程的全局变量的地址都是不一样的修改变量时互相之间没有影响各自独立。 8、分离线程pthread_detach
默认情况下新创建的线程是joinable的线程退出后需要对其进行pthread_join操作否则无法释放资源从而造成内存泄漏。但如果我们不关心线程的返回值join也是一种负担此时我们可以将该线程进行分离后续当线程退出时就会自动释放线程资源。一个线程如果被分离了这个线程依旧要使用该进程的资源依旧在该进程内运行甚至这个线程崩溃了一定会影响其他线程只不过这个线程退出时不再需要主线程去join了当这个线程退出时系统会自动回收该线程所对应的资源。可以是线程组内其他线程对目标线程进行分离也可以是线程自己分离。joinable和分离是冲突的一个线程不能既是joinable又是分离的。
分离线程的函数叫做pthread_detachpthread_detach函数的函数原型如下
#include pthread.h
int pthread_detach(pthread_t thread);参数说明
thread被分离线程的ID。
返回值说明
线程分离成功返回0失败返回错误码。
joinable和分离是冲突的一个线程不能既是joinable又是分离的。我们编写如下的代码进行验证
#include iostream
#include cstring
#include pthread.h
#include unistd.h
#include sys/types.h
#include sys/syscall.h
using namespace std;
__thread int global_value 100;
void *startRoutine(void *args)
{pthread_detach(pthread_self());cout 线程分离... endl;while (true){cout thread pthread_self() global_value: global_value Inc: global_value lwp: syscall(SYS_gettid) endl;sleep(1);}
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(tid1, nullptr, startRoutine, (void *)thread 1);pthread_create(tid2, nullptr, startRoutine, (void *)thread 1);pthread_create(tid3, nullptr, startRoutine, (void *)thread 1);int n pthread_join(tid1, nullptr);cout n : strerror(n) endl;n pthread_join(tid2, nullptr);cout n : strerror(n) endl;n pthread_join(tid3, nullptr);cout n : strerror(n) endl;return 0;
}不是说好一个线程不能既是joinable又是分离的吗下面我们对上述代码进行一次小改动仅仅多了一个sleep(1) 为什么我sleep(1)后才符合我们的预期呢 一个线程不能既是joinable又是分离的。有sleep之后join就会失败没有sleepjoin就会成功那么哪个才是正确的呢
有sleep(1)才是正确的。原因是当我们床架线程后新线程就跑去执行我的线程处理函数了而主线程继续向后执行新线程和主线程本质都是轻量级进程谁先被调度这个是不确定的那么就很有可能创建新线程后主线程直接进入join等待没有sleep(1)而新线程还没来得及进行线程分离pthread_detach主线程join后就被挂起了阻塞了当你再去分离的时候已经没有时间join了也不会唤醒你了。而加上sleep(1)后就是为了让新线程先去detach后再去分离
总结分离线程
线程分离了意味着不在关心这个线程的死活。所以这也相当于线程退出的第4种方式延后退出。立即分离或者延后分离都可以但是要保证线程活着。新线程分离但是主线程先退出进程退出所有线程就都退了。一般分离线程对应的主线程不退出常驻内存的进程