linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一
linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一如图:上述polkitd进程是多线程的,进程ID为731,进程内有6个线程,线程ID为731,764,765,768,781,791。但是获取该gettid系统调用接口并没有被封装起来,如果确实需要获取线程ID,可使用:如何查看一个线程的ID命令:ps -eLf
什么是线程?linux内核中是没有线程这个概念的,而是轻量级进程的概念:LWP。一般我们所说的线程概念是C库当中的概念。
线程是怎样描述的?线程实际上也是一个task_struct,工作线程拷贝主线程的task_struct,然后共用主线程的mm_struct。线程ID是在用task_struct中pid描述的,而task_struct中tgid是线程组ID,表示线程属于该线程组,对于主线程而言,其pid和tgid是相同的,我们一般看到的进程ID就是tgid。
即:
获取线程ID和主线程ID的值:
但是获取该gettid系统调用接口并没有被封装起来,如果确实需要获取线程ID,可使用:
如何查看一个线程的ID
命令:ps -eLf
上述polkitd进程是多线程的,进程ID为731,进程内有6个线程,线程ID为731,764,765,768,781,791。
如图:
多线程如何避免调用栈混乱的问题?工作线程和主线程共用一个mm_struct,如果都向栈中压栈,必然会导致调用栈出错。
实际上工作线程压栈是压了共享区,该共享区包含了许多线程独有的资源。如图:
每一个线程,默认在共享区中占有的空间为8M,可以使用ulimit -s修改。
线程独有资源
- 线程ID
- 一组寄存器
- errno
- 信号屏蔽字
- 调度优先级
线程共享资源和环境
- 文件描述符表
- 信号的处理方式
- 当前工作目录
- 用户id和组id
举个生活中的例子, 这就好比去银行办理业务。 到达银行后, 首先取一个号码, 然后坐下来安心等待。 这时候你一定希望, 办理业务的窗口越多越好。 如果把整个营业大厅当成一个进程的话, 那么每一个窗口就是一个工作线程。
线程带来的优势- 线程会共享内存地址空间。
- 创建线程花费的时间要少于创建进程花费的时间。
- 终止线程花费的时间要少于终止进程花费的时间。
- 线程之间上下文切换的开销, 要小于进程之间的上下文切换。
- 线程之间数据的共享比进程之间的共享要简单。
- 充分利用多处理器的可并行数量。(线程会提高运行效率,但当线程多到一定程度后,可能会导致效率下降,因为会有线程调度切换。)
- 健壮性降低:多个线程之中, 只要有一个线程不够健壮存在bug(如访问了非法地址引发的段错误) , 就会导致进程内的所有线程一起完蛋。
- 线程模型作为一种并发的编程模型, 效率并没有想象的那么高, 会出现复杂度高、 易出错、 难以测试和定位的问题。
- 并不是只有主线程才能创建线程, 被创建出来的线程同样可以创建线程。
- 不存在类似于fork函数那样的父子关系, 大家都归属于同一个线程组, 进程ID都相等, group_leader都指向主线程, 而且各有各的线程ID。
- 并非只有主线程才能调用pthread_join连接其他线程, 同一线程组内的任意线程都可以对某线程执行pthread_join函数。
- 并非只有主线程才能调用pthread_detach函数, 其实任意线程都可以对同一线程组内的线程执行分离操作。
线程的对等关系:
线程创建详解:
- 第一个参数是pthread_t类型的指针, 线程创建成功的话,会将分配的线程ID填入该指针指向的地址。 线程的后续操作将使用该值作为线程的唯一标识。
- 第二个参数是pthread_attr_t类型, 通过该参数可以定制线程的属性, 比如可以指定新建线程栈的大小、 调度策略等。 如果创建线程无特殊的要求, 该值也可以是NULL, 表示采用默认属性。
- 第三个参数是线程需要执行的函数。 创建线程, 是为了让线程执行一定的任务。 线程创建成功之后, 该线程就会执行start_routine函数, 该函数之于线程, 就如同main函数之于主线程。
- 第四个参数是新建线程执行的start_routine函数的入参。
pthread_create错误码及描述:
传入参数arg的选择不要使用临时变量传参,使用堆上开辟的变量可以。
例:
线程ID以及进程地址空间线程获取自身的ID:
判断两个线程ID是否对应着同一个线程:
返回为0时,则表示两个线程为同一个线程,非0时,表示不是同一个线程。
用户调用pthread_create函数时, 首先要为线程分配线程栈, 而线程栈的位置就落在共享区。 调用mmap函数为线程分配栈空间。 pthread_create函数分配的pthread_t类型的线程ID, 不过是分配出来的空间里的一个地址, 更确切地说是一个结构体的指针。
即:
线程注意点- 线程ID是进程地址空间内的一个地址, 要在同一个线程组内进行线程之间的比较才有意义。 不同线程组内的两个线程, 哪怕两者的pthread_t值是一样的, 也不是同一个线程。
- 线程ID就有可能会被复用:
线程创建的第二个参数是pthread_attr_t类型的指针, pthread_attr_init函数会将线程的属性重置成默认值。
线程属性及默认值:
如果确实需要很多的线程, 可以调用接口来调整线程栈的大小:
线程终止线程终止,但进程不会终止的方法:
- 入口函数的return返回,线程就退出了
- 线程调用pthread_exit(NULL),谁调用谁退出
pthread_exit和线程启动函数(start_routine) 执行return是有区别的。 在start_routine中调用的任何层级的函数执行pthread_exit() 都会引发线程退出, 而return, 只能是在start_routine函数内执行才能导致线程退出。
- 其它线程调用了pthread_cancel函数取消了该线程
如果线程组中的任何一个线程调用了exit函数, 或者主线程在main函数中执行了return语句, 那么整个线程组内的所有线程都会终止
线程等待线程等待接口调用该函数,该执行流在等待线程退出的时候,该执行流是阻塞在pthread_joind当中的。
线程等待和进程等待的不同- 第一点不同之处是进程之间的等待只能是父进程等待子进程, 而线程则不然。线程组内的成员是对等的关系, 只要是在一个线程组内, 就可以对另外一个线程执行连接(join) 操作。
- 第二点不同之处是进程可以等待任一子进程的退出 , 但是线程的连接操作没有类似的接口, 即不能连接线程组内的任一线程, 必须明确指明要连接的线程的线程ID。
pthread_join()错误码:
为什么要等待退出的线程?如果不连接已经退出的线程, 会导致资源无法释放。 所谓资源指的又是什么呢?
已经退出的线程, 其空间没有被释放, 仍然在进程的地址空间之内。
新创建的线程, 没有复用刚才退出的线程的地址空间。
如果不执行连接操作, 线程的资源就不能被释放, 也不能被复用, 这就造成了资源的泄漏。
纵然调用了pthread_join, 也并没有立即调用munmap来释放掉退出线程的栈, 它们是被后建的线程复用了。 释放线程资源的时候, 若进程可能再次创建线程, 而频繁地munmap和mmap会影响性能, 所以将该栈缓存起来, 放到一个链表之中, 如果有新的创建线程的请求, 会首先在栈缓存链表中寻找空间合适的栈, 有的话, 直接将该栈分配给新创建的线程。
例:
线程分离默认情况下, 新创建的线程处于可连接(Joinable) 的状态, 可连接状态的线程退出后, 需要对其执行连接操作, 否则线程资源无法释放, 从而造成资源泄漏。
如果其他线程并不关心线程的返回值, 那么连接操作就会变成一种负担: 你不需要它, 但是你不去执行连接操作又会造成资源泄漏。 这时候你需要的东西只是:线程退出时, 系统自动将线程相关的资源释放掉, 无须等待连接。
可以是线程组内其他线程对目标线程进行分离, 也可以是线程自己执行pthread_detach函数。
线程的状态之中, 可连接状态和已分离状态是冲突的, 一个线程不能既是可连接的, 又是已分离的。 因此, 如果线程处于已分离的状态, 其他线程尝试连接线程时, 会返回EINVAL错误。
pthread_detach错误码:
注意:这里的已分离不是指线程失去控制,不归线程组管,而是指线程退出后,系统会自动释放线程资源。若是线程组内的任意线程执行了exit函数,即使是已分离的线程,也仍会收到影响,一并退出。
线程安全线程安全中涉及到的概念:
什么是线程不安全?多个线程访问同一块临界资源,导致资源产生二义性的现象。
举一个例子- 假设现在有两个线程A和B,单核CPU的情况下,此时有一个int类型的全局变量为100,A和B的入口函数都要对这个全局变量进行–操作。
- 线程A先拿到CPU资源后,对全局变量进行–操作并不是原子性操作,也就是意味着,A在执行–的过程中有可能会被打断。假设A刚刚将全局变量的值读到寄存器当中,就被切换出去了,此时程序计数器保存了下一条执行的指令,上下文信息保存寄存器中的值,这两个东西是用来线程A再次拿到CPU资源后,恢复现场使用的。
- 此时,线程B拿到了CPU资源,对全局变量进行了–操作,并且将100减为了99,回写到了内存中。
- A再次拥有了CPU资源后,恢复现场,继续往下执行,从寄存器中读到的值仍为100,减完之后为99,回写到内存中为99。
上述例子中,线程A和B都对全局变量进行了–操作,全局变量的值应该变为98,但程序现在实际的结果为99,所以这就导致了线程不安全。
解决方案只需做到下述三点即可:
- 代码必须要有互斥的行为: 当一个线程正在临界区中执行时, 不允许其他线程进入该临界区中。
- 如果多个线程同时要求执行临界区的代码, 并且当前临界区并没有线程在执行, 那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行, 那么该线程不能阻止其他线程进入临界区。
则本质上,我们需要对该临界区加一把锁:
锁是一个很普遍的需求, 当然用户可以自行实现锁来保护临界区。 但是实现一个正确并且高效的锁非常困难。 纵然抛下高效不谈, 让用户从零开始实现一个正确的锁也并不容易。 正是因为这种需求具有普遍性, 所以Linux提供了互斥量。