快捷搜索:  汽车  科技

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。

即:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(1)

获取线程ID和主线程ID的值:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(2)

但是获取该gettid系统调用接口并没有被封装起来,如果确实需要获取线程ID,可使用:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(3)

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(4)

如何查看一个线程的ID

命令:ps -eLf

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(5)

上述polkitd进程是多线程的,进程ID为731,进程内有6个线程,线程ID为731,764,765,768,781,791。

如图:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(6)

多线程如何避免调用栈混乱的问题?

工作线程和主线程共用一个mm_struct,如果都向栈中压栈,必然会导致调用栈出错。

实际上工作线程压栈是压了共享区,该共享区包含了许多线程独有的资源。如图:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(7)

每一个线程,默认在共享区中占有的空间为8M,可以使用ulimit -s修改。

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(8)

线程独有资源

  1. 线程ID
  2. 一组寄存器
  3. errno
  4. 信号屏蔽字
  5. 调度优先级

线程共享资源和环境

  1. 文件描述符表
  2. 信号的处理方式
  3. 当前工作目录
  4. 用户id和组id
为什么要有多线程?

举个生活中的例子, 这就好比去银行办理业务。 到达银行后, 首先取一个号码, 然后坐下来安心等待。 这时候你一定希望, 办理业务的窗口越多越好。 如果把整个营业大厅当成一个进程的话, 那么每一个窗口就是一个工作线程。

线程带来的优势
  1. 线程会共享内存地址空间。
  2. 创建线程花费的时间要少于创建进程花费的时间。
  3. 终止线程花费的时间要少于终止进程花费的时间。
  4. 线程之间上下文切换的开销, 要小于进程之间的上下文切换。
  5. 线程之间数据的共享比进程之间的共享要简单。
  6. 充分利用多处理器的可并行数量。(线程会提高运行效率,但当线程多到一定程度后,可能会导致效率下降,因为会有线程调度切换。)
线程带来的缺点
  1. 健壮性降低:多个线程之中, 只要有一个线程不够健壮存在bug(如访问了非法地址引发的段错误) , 就会导致进程内的所有线程一起完蛋。
  2. 线程模型作为一种并发的编程模型, 效率并没有想象的那么高, 会出现复杂度高、 易出错、 难以测试和定位的问题。
注意
  1. 并不是只有主线程才能创建线程, 被创建出来的线程同样可以创建线程。
  2. 不存在类似于fork函数那样的父子关系, 大家都归属于同一个线程组, 进程ID都相等, group_leader都指向主线程, 而且各有各的线程ID。

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(9)

  1. 并非只有主线程才能调用pthread_join连接其他线程, 同一线程组内的任意线程都可以对某线程执行pthread_join函数。
  2. 并非只有主线程才能调用pthread_detach函数, 其实任意线程都可以对同一线程组内的线程执行分离操作。

线程的对等关系:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(10)

线程创建

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(11)

详解:

  1. 第一个参数是pthread_t类型的指针, 线程创建成功的话,会将分配的线程ID填入该指针指向的地址。 线程的后续操作将使用该值作为线程的唯一标识。
  2. 第二个参数是pthread_attr_t类型, 通过该参数可以定制线程的属性, 比如可以指定新建线程栈的大小、 调度策略等。 如果创建线程无特殊的要求, 该值也可以是NULL, 表示采用默认属性
  3. 第三个参数是线程需要执行的函数。 创建线程, 是为了让线程执行一定的任务。 线程创建成功之后, 该线程就会执行start_routine函数, 该函数之于线程, 就如同main函数之于主线程。
  4. 第四个参数是新建线程执行的start_routine函数的入参。

pthread_create错误码及描述:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(12)

传入参数arg的选择

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(13)

不要使用临时变量传参,使用堆上开辟的变量可以。

例:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(14)

线程ID以及进程地址空间

线程获取自身的ID:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(15)

判断两个线程ID是否对应着同一个线程:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(16)

返回为0时,则表示两个线程为同一个线程,非0时,表示不是同一个线程。

用户调用pthread_create函数时, 首先要为线程分配线程栈, 而线程栈的位置就落在共享区。 调用mmap函数为线程分配栈空间。 pthread_create函数分配的pthread_t类型的线程ID, 不过是分配出来的空间里的一个地址, 更确切地说是一个结构体的指针。

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(17)

即:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(18)

线程注意点
  1. 线程ID是进程地址空间内的一个地址, 要在同一个线程组内进行线程之间的比较才有意义。 不同线程组内的两个线程, 哪怕两者的pthread_t值是一样的, 也不是同一个线程。
  2. 线程ID就有可能会被复用:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(19)

线程创建出来的默认值

线程创建的第二个参数是pthread_attr_t类型的指针, pthread_attr_init函数会将线程的属性重置成默认值。

线程属性及默认值:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(20)

如果确实需要很多的线程, 可以调用接口来调整线程栈的大小:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(21)

线程终止

线程终止,但进程不会终止的方法:

  1. 入口函数的return返回,线程就退出了
  2. 线程调用pthread_exit(NULL),谁调用谁退出

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(22)

pthread_exit和线程启动函数(start_routine) 执行return是有区别的。 在start_routine中调用的任何层级的函数执行pthread_exit() 都会引发线程退出, 而return, 只能是在start_routine函数内执行才能导致线程退出。

  1. 其它线程调用了pthread_cancel函数取消了该线程

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(23)

如果线程组中的任何一个线程调用了exit函数, 或者主线程在main函数中执行了return语句, 那么整个线程组内的所有线程都会终止

线程等待线程等待接口

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(24)

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(25)

调用该函数,该执行流在等待线程退出的时候,该执行流是阻塞在pthread_joind当中的。

线程等待和进程等待的不同
  1. 第一点不同之处是进程之间的等待只能是父进程等待子进程, 而线程则不然。线程组内的成员是对等的关系, 只要是在一个线程组内, 就可以对另外一个线程执行连接(join) 操作。
  2. 第二点不同之处是进程可以等待任一子进程的退出 , 但是线程的连接操作没有类似的接口, 即不能连接线程组内的任一线程, 必须明确指明要连接的线程的线程ID

pthread_join()错误码:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(26)

为什么要等待退出的线程?

如果不连接已经退出的线程, 会导致资源无法释放。 所谓资源指的又是什么呢?

已经退出的线程, 其空间没有被释放, 仍然在进程的地址空间之内。

新创建的线程, 没有复用刚才退出的线程的地址空间。

如果不执行连接操作, 线程的资源就不能被释放, 也不能被复用, 这就造成了资源的泄漏。

纵然调用了pthread_join, 也并没有立即调用munmap来释放掉退出线程的栈, 它们是被后建的线程复用了。 释放线程资源的时候, 若进程可能再次创建线程, 而频繁地munmap和mmap会影响性能, 所以将该栈缓存起来, 放到一个链表之中, 如果有新的创建线程的请求, 会首先在栈缓存链表中寻找空间合适的栈, 有的话, 直接将该栈分配给新创建的线程。

例:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(27)

线程分离

默认情况下, 新创建的线程处于可连接(Joinable) 的状态, 可连接状态的线程退出后, 需要对其执行连接操作, 否则线程资源无法释放, 从而造成资源泄漏。

如果其他线程并不关心线程的返回值, 那么连接操作就会变成一种负担: 你不需要它, 但是你不去执行连接操作又会造成资源泄漏。 这时候你需要的东西只是:线程退出时, 系统自动将线程相关的资源释放掉, 无须等待连接。

可以是线程组内其他线程对目标线程进行分离, 也可以是线程自己执行pthread_detach函数。

线程的状态之中, 可连接状态和已分离状态是冲突的, 一个线程不能既是可连接的, 又是已分离的。 因此, 如果线程处于已分离的状态, 其他线程尝试连接线程时, 会返回EINVAL错误。

pthread_detach错误码:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(28)

注意:这里的已分离不是指线程失去控制,不归线程组管,而是指线程退出后,系统会自动释放线程资源。若是线程组内的任意线程执行了exit函数,即使是已分离的线程,也仍会收到影响,一并退出。

线程安全

线程安全中涉及到的概念:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(29)

什么是线程不安全?

多个线程访问同一块临界资源,导致资源产生二义性的现象。

举一个例子
  • 假设现在有两个线程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,所以这就导致了线程不安全。
如何解决线程不安全现象?

解决方案只需做到下述三点即可:

  1. 代码必须要有互斥的行为: 当一个线程正在临界区中执行时, 不允许其他线程进入该临界区中。
  2. 如果多个线程同时要求执行临界区的代码, 并且当前临界区并没有线程在执行, 那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行, 那么该线程不能阻止其他线程进入临界区。

则本质上,我们需要对该临界区加一把锁:

linux系统下多进程与多线程编程:一篇文章搞懂多线程难点一(30)

锁是一个很普遍的需求, 当然用户可以自行实现锁来保护临界区。 但是实现一个正确并且高效的锁非常困难。 纵然抛下高效不谈, 让用户从零开始实现一个正确的锁也并不容易。 正是因为这种需求具有普遍性, 所以Linux提供了互斥量

猜您喜欢: