wordpress 制作网站模板教程,全面的vi设计公司,移动选号码网上选号手机号,海南专业网站建设目录 1.线程控制
#xff08;1#xff09;pthread和thread库
#xff08;2#xff09;线程的创建、等待和分离
①线程创建
②线程等待
③线程分离
④线程替换#xff08;不可行#xff09;
#xff08;3#xff09;线程的终止和取消
①线程终止
②线程取消
2…目录 1.线程控制
1pthread和thread库
2线程的创建、等待和分离
①线程创建
②线程等待
③线程分离
④线程替换不可行
3线程的终止和取消
①线程终止
②线程取消
2.Linux下线程的底层
1线程ID
2资源划分和维护
①新线程栈的动态创建和销毁
②struct pthread的存储
③线性局部存储
④线性栈和mmap
3clone
4创建线程过程 1.线程控制
1pthread和thread库
线程的使用需要pthread库这是用户级别的、通用的、基于 POSIX 标准的线程库。这个库提供C语言接口并且具备跨平台性。很明显它基于操作系统底层的线程机制实现并进一步封装向上提供了统一的接口。
pthread库的实现是glibc的一部分所以pthread库是Linux系统自带的线程库。但由于线程库可能不是项目中必需的而且有可能和其它库引发冲突因此Linux有可能不会默认链接pthread动态库。如果要使用线程必须编译时选择-lpthread不需要选择查找目录、头文件位置系统能找到。
除此之外C11引入了thread这是C风格的线程实现同样具有跨平台性。Linux下使用thread需要链接原生线程库-lpthread
2线程的创建、等待和分离
①线程创建 当创建成功时返回0失败时返回错误码。
参数我们不关注第二个默认填nullptr。第一个参数pthread_t是线程的id用于唯一标识线程第三个是让线程线程执行的函数回调函数该函数返回值为void*参数为void*第四个参数是传入该回调函数的参数。
下面是简单的使用 我们可以看到引用线程库后就算还没有创建线程主程序的代码执行可被认为是主线程可以获取它的唯一标识pthread_t。虽然Linux下都是LWP但在C语言上封装后就出现了线程的概念。 我们可以认为线程的存在就是为了执行某一个函数主线程的存在就是为了执行main函数执行main函数的线程就是主线程当main函数结束后代表主线程执行完毕进而表示整个进程的结束。其余线程也都可以这么理解它们的生命周期从开始执行函数为始结束执行函数为终。 主线程也可以新建一个线程并让它运行代码。当主线程结束运行后程序退出。
但这里有个问题万一主线程比创建的线程更早结束就会导致创建的线程没有完成任务。那应该怎么保证主线程在最后退出呢sleep这太难控制了而且效率很低。
②线程等待 成功返回0错误返回错误码。
假如thread1调用该join函数其第一个参数是thread2这就意味着thread1会阻塞在这个函数直到thread2结束第二个参数是thread1接收thread2的返回值所用。
除此之外join之后线程的资源如线程的退出状态、栈等将会被释放。如果不及时join这些未释放的资源会一直保留在系统中直到整个进程结束就像僵尸进程占用系统资源一样。
下面是对刚才代码的修改 这样主线程就会在等待的线程退出后才退出并且能够获取到子线程的函数返回值。要特别注意子线程的返回值要求为void*参数为void*必须完全对应才能编译通过。如果不对子线程的返回值感兴趣可以传nullptr作为join函数的第二个参数。
线程等待类似回收僵尸进程只要join对应线程的id就算该线程已经结束了也都能获取到该线程的返回值并且这个返回值需要通过堆区创建空间。对于所有线程来说堆区是共享的谁拿着堆的入口地址谁就能访问空间。
在线程中虽然名义上进程地址空间被划分给了不同线程但线程之间可以互相访问只要有地址。
join成功了代表此刻数据一定处理完了这样就可以放心执行后面的代码了。 ③线程分离 join有个坏处就是join的线程必须等待被join的线程执行完后才会继续执行代码但有的时候线程要做自己的事情比如主线程承担分配任务的使命。想要不阻塞在join函数则要将目标线程设置为分离状态即detach状态。
线程分离出对应的线程后join会失败join的返回值不是0。之后该线程执行完毕后会自动释放空间而不需要join。同时除非极端情况就算主线程走完了分离的线程还能执行进程会在分离线程执行完毕后回收但要注意一些共享资源的访问问题有可能分离线程会去访问已被释放的线程的资源。
④线程替换不可行
线程能不能程序替换不可以。只有进程可以程序替换进程替换会把代码数据全部替换线程没这个能力它不能对进程做出操作。但我们可以在线程里面fork进一步实现程序替换。
3线程的终止和取消
①线程终止
主线程return表示整个进程结束此刻所有线程也都会退出不管是否执行完毕。新线程return表示该线程退出其余线程不受影响。
但注意任何线程在任何地方调用exit均表示进程退出即所有线程都会终止。相对的例如pthread_exit((void*)10)这种exit退出方式就相对温和只是结束相应调用的线程并且交出返回值给等待它的线程pthread_exit和return等价平时就用return就好。
②线程取消
线程可以被取消主线程可以在任何时候取消任何线程的执行这是被动式退出不由其它线程决定。 其参数就是被取消的线程id。取消成功返回0失败返回错误码。
取消一个线程的前提是目标线程已经被启动了即在create函数之后。但一般不推荐取消线程因为对应线程的状态不清楚处理的数据状态也不清楚因此盲目取消会导致程序混乱。
注意取消的线程依旧需要被join这就相当于回收僵尸进程被取消的线程会返回-1该返回值其实是宏PTHREAD_CANCELED。
2.Linux下线程的底层
1线程ID
线程id即pthread_t是一个地址这个地址本身也有唯一性。
pthread.so库加载到内存中这个库被经过页表映射到共享区。创建线程的其实是在调用库方法由于进程内的所有线程共享进程地址空间因此它们都能执行库的其它方法。
Linux中严格上只有LWP但用户要用线程想要用线程接口create、join等想要线程的属性线程的id、优先级、状态、栈大小等怎么办呢
用户不关心LWP的属性他们只关心线程的属性这就是pthread.so库单独提供的。pthread.so库要维护相关属性要存储相应的结构体来存储相关属性。这和glibc维护FILE一样所有文件的属性信息都是由glibc库来进行维护的当要对某个文件进行操作时都是调用glibc的函数对其维护下的结构体进行修改。注意这些结构体由动态库维护调用动态库的函数对这些结构体进行修改其结构体的物理内存也在库的里面下面的pthread.so也是如此。 pthread.so会维护该进程下所有线程的属性。对于一个线程来说其属性的集合就相当于结构体TCB线程的id、优先级、状态、栈大小、返回值都在里面保存着的。一般来说所有线程的属性的结构体整体形成的是一个数组。既然线程属性保存在一个物理内存中那么理所应当的其线程id的值就是相应结构体的地址也就是pthread_t类型pthread_create的第一个参数获取的值。 我们后续就可以用TCB来描述个由pthread.so库维护的结构体毕竟它是上层封装和LWP有区别了。这个时候我们就能理解为什么对线程操作都需要传入线程id本质上对线程的管理都是对维护线程属性的结构体的操作。
2资源划分和维护
①新线程栈的动态创建和销毁
当主线程开始执行时它就会保证自己有一个栈即主线程栈这个栈通常是在程序启动时由操作系统预先分配好的它的大小一般由操作系统和编译环境决定。当程序运行完毕后这个栈才会销毁。这个栈的位置就在进程地址空间的栈区。
除此之外其它所有子线程的栈都是动态创建的。为什么叫动态创建只有当调用create函数创建子线程时这个栈才会被创建。同理只要这个线程被join或分离线程执行完毕那么它的栈就会被销毁。因此子线程栈的动态性在于它可以在程序运行途中创建和销毁而主线程栈是一进程序就创建结束才销毁。
②struct pthread的存储
struct pthreadTCB被pthread.so库维护就意味着从物理内存的角度上看一个系统的所有线程的属性都存在pthread.so库的内部。当pthread.so被映射到虚拟内存中后就存放有所有线程的TCB当我们对某个线程进行操作时都是对这个库中某个线程属性的修改。同理FILE也是如此glibc这个库中维护了整个系统所有的FILE结构体。
struct pthread里面主要含有两个属性线性局部存储和线性栈的属性除此之外还一定包括LWP的属性task_struct就像FILE里面一定有文件描述符fd。
③线性局部存储
对于代码全局区的变量来说每个线程都可修改它们的值并且修改之后其它线程都能同时感受到这个变化。但有的时候我们不希望这样这就要用到编译性关键字__thread用它修饰变量后如__thread int a 10每个线程都会独立地得到该数据不会互相影响这就叫线程局部存储。
线性局部存储的数据是线程私有的数据错误码error的私有性质就是用线程局部存储实现的这样就能正确标识每个线程的错误了而不会相互影响。
__thread只能修饰内置类型结构体不行。局部存储的变量会单独存到TCB的一个属性里因此对一些高频访问效率较高不用到线性栈里面去找。
④线性栈和mmap
后面会提到的mmap区域就是指的共享区。
前面已经说过主线程栈在栈区可以动态增长。新线程的栈在共享区也就是mmap区域被开辟并且它有一个最大容量值即最多开辟8MB随系统用完就没了不像主线程栈可以向下增长这些栈的起始地址、容量信息都会被存入线程的属性结构体TCB中。我们还应知道mmap还是一个系统调用充当开辟空间的作用存在mmap区域的新线程栈就是由mmap系统调用申请的。malloc的底层也是mmap。
线程的栈区原则上是独立的每个线程各自用各自的但实际上是共享的因为没有权限限制加上所有线程看到的都是同一个进程地址空间所以只要拿到别的线程的栈空间地址就能访问。堆区、数据段同理。
到此我们可以总结一下线性栈和线性局部存储都由struct pthreadTCB管理都存在共享区其中栈是由mmap系统调用单独在mmap共享区开辟空间。这些TCB最终都由pthread.so维护事实上pthread.so维护了整个系统的线程属性struct pthread。
3clone 这个系统调用的几个参数需要声明
fn是函数入口地址arg是函数的参数包轻量级进程就会根据这两个参数执行对应函数。stack是LWP占用的独立的栈的地址LWP会在指定栈保存变量。flags是标志位区分不同的操作。
我们从参数就能理解调用pthread_create就需要先后调用mmap、clone。先是由mmap开辟空间更新TCB再由clone来执行。在系统层这是新建了一个LWP并执行代码而在用户层看来这就是创建了一个新的线程。
我们还可以了解一下clone它也是vfork和fork的底层调用clone是更底层的系统调用我们用户无法直接调用。
4创建线程过程
当程序开始运行时主线程栈在栈区创建并向下增长。当要创建新线程时会创建struct pthread会调用系统调用mmap在mmap共享区域动态创建栈且栈的大小不可增长之后由pthread.so维护的线程的属性struct pthread会进行更新之后通过clone系统调用创建好一个LWP并将pthread.so维护的struct pthread的地址返回给用户作为id。之后用户的所有操作本质都是调用pthread.so的函数对struct pthread的属性进行修改。