一、线程概念理解
1. 何为线程——线程与进程的区别
进程简单说就是一个程序的执行过程,有了这个概念,就可以覆盖系统编程的大部分情景了。那么线程的出现又有什么意义呢?其实程序的一段执行过程确实可以叫做进程,但是真正的,而是。
Linux 线程是进程内的执行流,作为调度的基本单位,共享进程资源但拥有独立栈空间。相比进程,线程创建开销小、切换快,适合并发任务,但也存在资源共享导致的健壮性降低问题。文章介绍了 pthread 库的核心接口,包括创建线程的 pthread_create、获取 ID 的 pthread_self、终止线程的 pthread_exit 和 pthread_cancel,以及等待回收的 pthread_join。同时解析了底层 clone 系统调用如何通过 CLONE_VM 等标志实现线程的资源共享机制,帮助开发者理解 Linux 下多线程的实现原理与最佳实践。

进程简单说就是一个程序的执行过程,有了这个概念,就可以覆盖系统编程的大部分情景了。那么线程的出现又有什么意义呢?其实程序的一段执行过程确实可以叫做进程,但是真正的,而是。
大体来说,进程相当于资源分配的基本单位,而线程是调度的基本单位。一个进程拥有虚拟地址空间、信号资源、文件描述符表等等资源,线程只是进程一部分,是进程真正的执行流。对于 CPU 而言,它并不认识进程,它只认执行流,每个线程被创建后可以执行指定代码片段,这样一来,一个进程就可以被拆分为多个执行流,既能并发处理任务,又能大幅降低上下文切换的开销,从而提升整体运行效率。进程和线程的对应关系是 1:n,在之前所谓的进程其实大部分指的是单线程进程。
联系前置知识:所谓的进程切换(时间片)、调度算法、调度队列等,实质都是对线程而言而不是进程。
线程作为进程的一部分,共享了整个进程的虚拟地址空间。信号处理方法等资源,这样的设计大大降低了线程切换时的开销(无需进行大量资源载入),这也是线程的核心设计特色。
作为进程真正的执行流,线程的执行遵循调度队列、时间片轮切等原则,因此需要通过上下文的保存进行线程之间的轮切,**同时也就意味着线程具有独立的 PCB 结构体进行执行上下文保存。**栈私有是线程独立执行函数调用的基础。
由于同进程内线程具有共享进程资源的特性,也就意味着线程间不存在进程间通信的最大问题:看到同一份资源,因此线程间很容易实现通信。
当某一个线程发生异常的时候,会触发硬件向进程发送信号导致进程中断退出,进程退出也就意味着线程全部退出,导致所有线程崩溃。
• 创建一个新线程的代价要比创建一个新进程小得多 • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多 ◦ 最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。 ◦ 另一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲 TLB(快表)会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件 cache。 • 线程占用的资源要比进程少 • 能充分利用多处理器的可并行数量 • 在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务 • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现 • I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作。
• 过多线程创建可能导致频繁线程轮切,反而降低效率。 ◦ 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。 • 由于线程间共享虚拟空间,健壮性降低 ◦ 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 • 缺乏访问控制 ◦ 进程是访问控制的基本粒度,在一个线程中调用某些 OS 函数会对整个进程造成影响。 • 编程难度提高 ◦ 编写与调试一个多线程程序比单线程程序困难得多

功能:创建一个新的线程
原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数:
thread: 返回线程 IDattr: 设置线程的属性,attr 为 NULL 表示使用默认属性start_routine: 是个函数地址,线程启动后要执行的函数arg: 传给线程启动函数的参数返回值:成功返回 0;失败返回错误码
注意:使用 pthread 库时需要手动链接(-lpthread)
代码示范
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <string>
void *handler(void *th) {
std::string name = static_cast<const char *>(th);
while (true) {
sleep(1);
std::cout << "I'm " << name << std::endl;
}
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, handler, (void *)"thread -1");
while (true) {
std::cout << "I'm main thread" << std::endl;
sleep(1);
}
}

代码进行编译运行后可以看到命令行按照预期进行了输出,同时进行进程资源调取可以看到只有一个进程运行,那么如何进行线程信息查看呢?
ps -aL

再次运行上次的进程后输入该指令就可以看到目标进程下出现了两个 LWP 不同的线程。
LWP 是指轻量化进程,是线程的内核层实现载体。其实在 Linux 中并不存在所谓线程,线程严格意义上是属于进程的子单元,无独立的内核数据结构,完全依赖进程的资源容器,自身仅保存执行上下文。Linux 的 LWP 有独立的 task_struct(和进程同等地位),内核调度时完全把它当成「独立单元」对待 —— 只是它和其他 LWP 共享资源而已。Linux 设计之初就简化了线程与进程之间的差异,采用统一管理的方式。因此 Linux 中不存在所谓线程,而是 LWP(轻量化进程),在内核层就叫做 LWP,而在用户层通过封装抽象成了线程。并且每个线程有自己独特的 LWP 标识符,图中所示就是 LWP 标识符。

在进程内部,glibc 同时为我们提供了线程 ID 的获取方法:pthread_self
可以在之前代码中加上该函数进行实验
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <string>
void *handler(void *th) {
std::string name = static_cast<const char *>(th);
pthread_t ID;
ID = pthread_self();
std::cout << "ID-1 : " << ID << std::endl;
while (true) {
sleep(1);
std::cout << "I'm " << name << std::endl;
}
}
int main() {
pthread_t thread;
pthread_t mID;
pthread_create(&thread, NULL, handler, (void *)"thread -1");
mID = pthread_self();
std::cout << "I'm main thread" << std::endl;
sleep(1);
}

可以发现打印出了两个很大的数字,而且跟我们刚刚通过 ps -aL 查询到的:LWP 标识符大小完全不一样,其实通过这个 pthread_self 函数获取到的 ID 并不是 LWP 提供的标识符,而是 pthread 库给每个线程维护的进程内唯一标识符,由于每个进程都有自己独立的空间,故而该标识符的作用域是进程内而非系统全局,这个标识符其实对应的是一个进程虚拟空间上的地址,通过这个地址可以找到该线程相关信息。


这两个函数均用于线程终止,区别在于 pthread_exit 函数用于终结自身线程,而 pthread_cancel 函数可以终结其他线程。
该函数没有返回值,进行调用时可以通过 retval 参数带出线程返回值(等同于线程执行函数 return void*)。注意:不要传入局部变量的地址,局部变量存储在栈上,线程结束后栈空间会被销毁
pthread_cancel(pthread_t thread) 以目标线程的 ID(pthread_t类型)为唯一入参,核心作用是向指定线程发送取消信号,触发其在取消点终止执行。
在进程层面,子进程退出后若父进程未调用 wait/waitpid 回收,会产生僵尸进程(占用 PID、进程表项等内核资源),造成系统级资源泄漏;线程层面虽不存在「僵尸线程」,但如果主线程先退出且未通过 pthread_join/pthread_detach 回收子线程,会导致子线程的用户态资源(如线程栈、TLS 线程局部存储)和内核态 task_struct 中部分私有数据无法及时释放,进而造成进程内的内存泄漏。

该函数调用时需要传入两个参数:
thread: 通过 pthread_self 获取的进程内线程唯一标识符,传入需要进行等待回收的标识符即可。retval: 输出型参数,指向存储子线程返回值的地址,若不需要接收返回值可传 NULL。pthread_exit(void *ret) 或子线程执行函数 return void* 设定退出返回值;pthread_join;retval 拿到子线程的返回值(*retval 就是子线程的返回值)。
pthread 库调用 pthread_create() 创建线程时,底层会调用 clone(),并传入核心参数:
CLONE_VM: 新线程(LWP)与父进程共享虚拟地址空间(包括堆、全局变量),这是线程与独立进程的核心区别;CLONE_FILES: 共享文件描述符表,线程间可复用打开的文件、套接字等;CLONE_SIGHAND: 共享信号处理函数,避免重复注册信号逻辑;CLONE_THREAD: 将新线程加入父进程的线程组,统一管理 PID/LWP;这些参数组合让新创建的执行流成为'共享大部分资源的轻量级进程',即用户态感知的'线程'。pthread 库的封装通过 clone() 的参数控制资源共享粒度,将内核的轻量级进程(LWP)转化为用户态的'线程'概念,并提供统一的线程创建、同步、管理接口,既复用了 Linux 内核的进程调度逻辑,又满足了多线程开发的易用性需求。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online