【Linux 系统】多线程(线程控制、线程互斥与同步、互斥量与条件变量)-- 详解(中)

简介: 【Linux 系统】多线程(线程控制、线程互斥与同步、互斥量与条件变量)-- 详解(中)

【Linux 系统】多线程(线程控制、线程互斥与同步、互斥量与条件变量)-- 详解(上)https://developer.aliyun.com/article/1515712?spm=a2c6h.13148508.setting.30.11104f0e63xoTy

4、进程 ID 和线程 ID

  • 在 Linux 中,目前的线程实现是 Native POSIX Thread Libaray,简称 NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct 结构体)。
  • 多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符( task_struct)与之对应。进程描述符结构体中的 pid ,表面上看对应的是进程 ID ,其实不然,它对应的是线程 ID;进程描述符中的 tgid ,含义是 Thread Group ID, 该值对应的是用户层面的进程 ID。

现在介绍的线程 ID ,不同于 pthread_t 类型的线程 ID ,和进程 ID 一样,线程 ID pid_t 类型的变量,而且是用来唯一标识线程的一个整型变量。

ps 命令中的 -L 选项,会显示如下信息:

  • LWP:线程 ID,即 gettid() 系统调用的返回值。
  • NLWP:线程组内线程的个数。

Linux 提供了 gettid 系统调用来返回其线程 ID,可是 glibc 并没有将该系统调用封装起来,在开放接口来共程序员使用。如果确实需要获得线程 ID,可以采用如下方法:

#include <sys/syscall.h> pid_t tid; tid = syscall(SYS_gettid);

从上面可以看出,a.out 进程的 ID 28543,下面有一个线程的 ID 也 是28543,这不是巧合。线程组内的第一个线程,在用户态被称为主线程(main thread),在内核中被称为 group leader,内核在创建第一个线程时,会将线程组的 ID 的值设置成第一个线程的线程 IDgroup_leader 指针则指向自身,既主线程的进程描述符。所以,线程组内存在一个线程 ID 等于进程 ID,而该线程即为线程组的主线程。

// 线程组ID等于线程ID,group_leader指向自身
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);

至于线程组其他线程的 ID 则有内核负责分配,其线程组 ID 总是和主线程的线程组 ID 一致,无论是主线程直接创建线程,还是创建出来的线程再次创建线程,都是这样。

if (clone_flags & CLONE_THREAD)
    p->tgid = current->tgid;
if (clone_flags & CLONE_THREAD)
{
    P->group_lead = current->group_leader;
    list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}

强调 :线程和进程不一样,进程有父进程的概念,但在线程组里面,所有的线程都是对等关系。


5、线程ID及进程地址空间布局和对原生线程库的理解

pthread_t 到底是什么类型呢?

取决于实现,对于 Linux 目前实现的 NPTL 实现而言, pthread_t 类型的线程 ID ,本质就是一个进程地址空间上的一个地址。

  • Linux OS 没有真正意义上的线程,而是用进程 PCB 模拟的,这就叫作轻量级进程。其本身没有提供类似线程创建、终止、等待、分离等相关 System Call 接口,但是会提供轻量级进程的接口,如 clone。所以为了更好的适配,系统基于轻量级进程的接口,模拟封装了一个用户层的原生线程库 pthread。这样,系统通过 PCB 来进行管理,用户层也得知道线程 ID、状态、优先级等其它属性用来进行用户级线程管理。
  • pthread_create 函数会产生一个线程 ID,存放在第一个参数指向的地址中,该线程 ID 和前面说的线程 ID LWP 不是一回事。前面讲的线程 ID 属于进程调度的范畴,因为线程是轻量级进程,是 OS 调度器的最小单位,所以需要一个数值来唯一表示该线程。pthread_create 函数的第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程 ID,属于 NPTL 线程库的范畴,线程库的后续操作,就是根据该线程 ID 来操作线程。
  • 原生线程库是一个库,它在磁盘上就是一个 libpthread.so 文件,运行时加载到内存,然后将这个库映射到共享区,此时这个库就可以被所有线程执行流看到了。此时有两个 ID 概念,一个是在命令行上看到的 LWP,一个是在用户层上看到的 tid。前者是在系统层面上供 OS 调度的,后者是 pthread_create 获得的线程 ID,它是一个用户层概念,本质是一个地址,就是 pthread 库中某一个起始位置,也就是对应到共享区中的某一个位置。所以线程数据的维护全都是在 pthread 线程库中去维护的,上图所示,其中会包含每个线程的局部数据,struct pthread 就是描述线程的 TCB,线程局部存储可以理解是不会在线程栈上保存的数据,我们在上面说过线程会产生各种各样的中间数据,如上下文数据,此时就需要独立的栈去保存,它就是线程栈。而下图中拿到的 tid 就是线程在共享区中线程库内的相关属性的起始地址,所以只要拿到了用户层的 tid,就可以在库中找到线程相关的属性数据,很明显 tid 和 LWP 是 1 : 1 的,而主线程不使用库中的栈结构,直接使用地址空间中的栈区,称为主线程线

实际上在很多 OS 在设计线程时都是用户级线程,用户级线程就是把相关的属性数据放在用户层,真正的调度还是得由一个相关的执行流来处理的,这叫做 1 : 1,这是 Linux 所采用的。当然在用户层只有一个执行流,但 OS 为了完成你的这个任务,可能会在内核层创建多个执行流去做的,这就叫 1 : N。用户级线程是怎么和内核级线程关联的呢,可以简单的理解成用户级线程只要把代码交给内核级线程代码就可以跑了,创建用户级线程就是创建 LWP,退出用户级线程就是退出 LWP,再把库中的相关数据关掉,只要在用户层的操作可以和内核层对应起来就行了,就像白帮中一名警察派了一个卧底潜伏于黑帮,然后警察指派任务给卧底,警察是可以控制卧底的,警察就是用户级线程,卧底就是内核级线程,它们的关系是 1 : 1 的,内核级进程是与系统是强相关的,如果让用户直接去用它倒也可以,不过用户就要去了解它,成本较高,所以就需要存在用户级进程,让用户更好的使用,同时警察可以站在他的角度向老百姓解释的很清楚,而卧底站在他的角度就解释不清楚。所以 Linux 中要有原生线程库的原因是 Linux 本身没有提供真正意义上的线程,自然也就没有真正意义上的线程控制接口,只能是轻量级进程来模拟,而用户要操作轻量级进程,就得向用户解释更多东西,不是所有人都能理解这种现象的,而用户作为一个东西被偷的人,只需要你把东西找回来就行了,也就是用户仅仅需要知道怎么操作线程就行了。所以需要存在一个用户级线程库才供用户使用,就如同这个世界不是只有老百姓和恶人,而需要有一个警察的角色。


6、线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数 return,这种方法对主线程不适用,main 函数 return 相当于调用 exit
  2. 线程可以调用 pthread_ exit 终止自己。
  3. 一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程。

(1)pthread_exit 函数

A. 接口介绍

线程终止。

retval:用于传递线程的退出状态,在主线程中,pthread_join() 可以等待新线程结束,并将新线程的退出状态存储在 tret 指针。

注意 pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是用 malloc 分配的, 不能在线程函数的栈上分配, 因为当其它线程得到这个返回指针时线程函数已经退出了。


B. 代码


(2)pthread_cancel 函数

A. 接口介绍

取消一个执行中的线程。


B. 代码



__thread: 修饰全局变量,结果就是让每一个线程各自拥有一个全局变量 —— 线程的局部存储。


7、线程等待

为什么需要线程等待?

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

创建新的线程不会复用刚才退出线程的地址空间。


(1)接口介绍

  • thread:线程 ID。
  • value_ptr:它指向一个指针,后者指向线程的返回值。

调用该函数的线程将挂起等待, 直到 id thread 的线程终止。 thread 线程以不同的方法终止 , 通过 pthread_join 得到的终止状态是不同的,总结如下:

  1. 如果 thread 线程通过 return 返回, value_ ptr 所指向的单元里存放的是 thread 线程函数的返回值。
  2. 如果 thread 线程被别的线程调用 pthread_ cancel 异常终掉, value_ ptr 所指向的单元里存放的是常数 PTHREAD_CANCELED。
  3. 如果 thread 线程是自己调用 pthread_exit 终止的, value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。
  4. 如果对 thread 线程的终止状态不感兴趣, 可以传 NULL value_ ptr 参数。


跟进程一样,一般线程终止后必须进行等待,由 main thread 进行等待。因为要防止内存泄漏,资源浪费(通过线程等待,将曾经线程向进程在地址空间中申请的资源释放,在线程这里一般是说如果不释放相关线程,那么申请新线程时不会复用未释放的线程)。保证主线程最后退出,让新线程正常结束。获得新线程的退出码信息(而 pthread_create 时调用 start_routine 新线程传入的参数是 void* 且返回的类型是 void*,这样它就是一个通用接口,也意味着新线程可以返回任意类型的数据,此时 pthread_join 时,就会被 retval 拿到)。实际在底层就是 pthread_create 调用 start_routine 后,start_routine 将线程退出码写到对应 PCB 中,然后调用 pthread_join 时就可以从对应 PCB 中读取退出结果到 retval。

我们都知道不可能通过 fun 函数来把 10 拿出去的原因是因为它是值传递,而应该地址传递。同样,如果想在一个函数内部返回一个 void* 的值也很简单。pthread_create 中 start_routine 参数和返回值类型是 void*,它是要支持通用接口,而 pthread_join 中 retval 的类型是 void**,然后 pthread_join 会通过你传入的线程 id,去读取对应的 PCB 中的退出码信息,因为退出码信息可能是不同类型的地址,所以要用 void** 来接收,retval 是输出型参数,然后又由它返回 void* 到用户层。


四、分离进程

  • 默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join 是一种负担,这个时候我们可以告诉系统,当线程退出时,自动释放线程资源。

1、pthread_detach

(1)接口介绍

用于创建线程,它是一个回调函数。如果线程创建成功,则会执行 start_routine,编译和链接时需要引入 pthread。

  • thread:输出型参数,代表线程 id。

  1. 默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成内存泄漏。
  2. 如果不关心线程的返回值,join 则是一种负担,这个时候,可以使用分离,此时就告诉系统,当线程退出时,自动释放线程资源,这就是线程分离的本质。
  3. joinable 和 pthread_detach 是冲突的,也就是说默认情况下,新创建的线程是不用 pthread_detach。
  4. 就算线程被分离了,也还是会和其它线程影响的,因为它们共享同一块地址空间。

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

joinable 和分离是冲突的,一个线程不能既是 joinable 又是分离的。

注意:没有线程替换这种操作,但可以在线程中执行进程替换系列函数。这是因为新线程内部执行进程替换函数,这看起来像是把新线程中的代码替换了,但实际会把主线程中的代码也替换了,因为主线程和新线程共享地址空间,所以新线程内部进程替换后,所有的线程包括主线程都会被影响。所以轻易不要在多线程中执行进程替换函数。


pthread_join 返回的是 22,说明等待失败了,然后返回,进程终止。其实一个线程被设置为分离状态,则该线程不应该被等待,如果被等待了,结果是未定义的,至少一定会等待出错。

【Linux 系统】多线程(线程控制、线程互斥与同步、互斥量与条件变量)-- 详解(下)https://developer.aliyun.com/article/1515720?spm=a2c6h.13148508.setting.28.11104f0e63xoTy

相关文章
|
9月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
381 0
|
9月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
10月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
695 5
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
528 20
|
7月前
|
Linux 应用服务中间件 Shell
二、Linux文本处理与文件操作核心命令
熟悉了Linux的基本“行走”后,就该拿起真正的“工具”干活了。用grep这个“放大镜”在文件里搜索内容,用find这个“探测器”在系统中寻找文件,再用tar把东西打包带走。最关键的是要学会使用管道符|,它像一条流水线,能把这些命令串联起来,让简单工具组合出强大的功能,比如 ps -ef | grep 'nginx' 就能快速找出nginx进程。
805 1
二、Linux文本处理与文件操作核心命令
|
7月前
|
Linux
linux命令—stat
`stat` 是 Linux 系统中用于查看文件或文件系统详细状态信息的命令。相比 `ls -l`,它提供更全面的信息,包括文件大小、权限、所有者、时间戳(最后访问、修改、状态变更时间)、inode 号、设备信息等。其常用选项包括 `-f` 查看文件系统状态、`-t` 以简洁格式输出、`-L` 跟踪符号链接,以及 `-c` 或 `--format` 自定义输出格式。通过这些选项,用户可以灵活获取所需信息,适用于系统调试、权限检查、磁盘管理等场景。
465 137
|
7月前
|
安全 Ubuntu Unix
一、初识 Linux 与基本命令
玩转Linux命令行,就像探索一座新城市。首先要熟悉它的“地图”,也就是/根目录下/etc(放配置)、/home(住家)这些核心区域。然后掌握几个“生存口令”:用ls看周围,cd去别处,mkdir建新房,cp/mv搬东西,再用cat或tail看文件内容。最后,别忘了随时按Tab键,它能帮你自动补全命令和路径,是提高效率的第一神器。
1267 58
|
10月前
|
JSON 自然语言处理 Linux
linux命令—tree
tree是一款强大的Linux命令行工具,用于以树状结构递归展示目录和文件,直观呈现层级关系。支持多种功能,如过滤、排序、权限显示及格式化输出等。安装方法因系统而异常用场景包括:基础用法(显示当前或指定目录结构)、核心参数应用(如层级控制-L、隐藏文件显示-a、完整路径输出-f)以及进阶操作(如磁盘空间分析--du、结合grep过滤内容、生成JSON格式列表-J等)。此外,还可生成网站目录结构图并导出为HTML文件。注意事项:使用Tab键补全路径避免错误;超大目录建议限制遍历层数;脚本中推荐禁用统计信息以优化性能。更多详情可查阅手册mantree。
864 143
linux命令—tree
|
6月前
|
存储 安全 Linux
Linux卡在emergency mode怎么办?xfs_repair 命令轻松解决
Linux虚拟机遇紧急模式?别慌!多因磁盘挂载失败。本文教你通过日志定位问题,用`xfs_repair`等工具修复文件系统,三步快速恢复。掌握查日志、修磁盘、验重启,轻松应对紧急模式,保障系统稳定运行。
1154 2
|
7月前
|
缓存 监控 Linux
Linux内存问题排查命令详解
Linux服务器卡顿?可能是内存问题。掌握free、vmstat、sar三大命令,快速排查内存使用情况。free查看实时内存,vmstat诊断系统整体性能瓶颈,sar实现长期监控,三者结合,高效定位并解决内存问题。
663 0
Linux内存问题排查命令详解

热门文章

最新文章

下一篇
开通oss服务