Linux多线程概念与控制
8.1 Linux线程概念
线程本质上就是进程内部的执行流,多个线程共享同一进程的地址空间,但它们拥有独立的执行状态(如寄存器、栈等)。在 Linux 系统中,线程的实现方式使得它们比传统进程更加轻量级,CPU 看到的是线程的 PCB(task_struct),但 Linux 通过轻量级进程(LWP, Lightweight Process)机制,使线程调度更加高效。

在 Linux 内核中,task_struct 是进程/线程的核心数据结构,CPU 在调度时只关心 task_struct,而不区分是单线程还是多线程。CPU 只关注 task_struct,而不关心进程/线程的概念。
每个进程或线程都有一个 task_struct,无论是单执行流还是多执行流。
多线程程序的每个线程本质上都是一个 task_struct 实例,在 CPU 眼中,这些线程与普通进程无异,只是共享某些资源(如地址空间)。
在 Linux 中,线程和进程的区别并不明显,因为 Linux 线程实际上是轻量级进程
只有一个 task_struct,独占虚拟内存,运行时,CPU 看到一个 task_struct 在调度。有多个 task_struct,但它们共享同一个虚拟内存,每个线程在 CPU 眼中就是一个独立的 task_struct,并由调度器管理。
单线程:
task_struct(进程)
├──进程的地址空间
├── files_struct(文件描述符表)
├── signal_struct(信号处理)
├── 线程私有的 kernel_stack(内核栈)
├── 线程私有的 thread_info(寄存器等)
多线程:
task_struct(线程1)
task_struct(线程2)
task_struct(线程3)
├── 共享 地址空间
├── 共享 files_struct
├── 共享 signal_struct
├── 独立的 kernel_stack(内核栈)
├── 独立的 thread_info(寄存器等)
线程的优点:
(1) 线程创建开销小
进程创建时,操作系统需要分配独立的虚拟内存、页表、文件描述符表、信号处理表等,开销较大。
线程创建时,由于共享地址空间、文件描述符,只需要分配独立的栈和寄存器,开销远小于进程。
对比:
进程创建:fork(),需要拷贝页表(Copy-on-Write)。
线程创建:pthread_create(),本质是 clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND),直接共享资源。
(2) 线程切换开销小
进程切换涉及地址空间切换,需要重新加载页表,成本高。
线程切换不涉及地址空间切换,只需保存/恢复寄存器和栈指针,操作更少,效率更高。调度器调度线程的代价小于调度进程。
(3) 线程占用资源少
进程独立拥有数据段、代码段、堆、栈,占用较多资源。
线程共享代码段、数据段、堆,只需独立栈和寄存器,开销小。
(4) 多核 CPU 上充分利用并行性
进程默认是独立的,只有通过 IPC(进程间通信)才能协同。
线程可以直接共享数据,在多核 CPU 上可以并行执行多个任务,提高 CPU 利用率。
示例:
Web 服务器:每个请求分配一个线程,提高吞吐量。
数据库系统:查询、事务、缓存等操作分开多个线程执行。
(5) I/O 操作时不阻塞整个进程
进程如果执行 I/O 操作,整个进程可能会阻塞,导致 CPU 资源浪费。
线程可以让一个线程等待 I/O,而其他线程继续执行计算任务,提高吞吐量。
示例:
浏览器:一个线程负责页面渲染,另一个线程下载资源,避免界面卡死。
视频播放器:解码线程和音视频同步线程并行工作,提高播放流畅度。
(6) 计算密集型任务的加速
计算密集型程序(如矩阵计算、图像处理、深度学习)可以拆分为多个线程,让不同 CPU 核心并行处理。
示例:
OpenMP 多线程加速科学计算。
机器学习训练使用多个线程并行计算梯度下降。
(7) I/O 密集型任务的吞吐量提升
在 数据库服务器、Web 服务器 这类 I/O 密集型应用中,线程可以同时等待多个 I/O 操作,避免 CPU 空闲等待,提高并发能力。
示例:
Nginx 服务器:使用异步 I/O 和多线程处理大量网络请求。
数据库引擎:多个线程同时处理查询、索引、缓存更新等。
线程的缺点:
(1) 计算密集型任务:线程数过多可能会降低性能
现象:
线程调度器会频繁切换线程,导致额外的上下文切换。线程同步(如锁、互斥量)会导致 线程间竞争,降低性能。
示例:
假设 CPU 只有 4 核,但创建了 100 个计算密集型线程,操作系统会花费大量时间在线程调度 上,而不是执行实际的计算任务。
(2) 线程之间共享资源,容易引发难以察觉的 Bug
线程共享全局变量、堆内存,如果没有合适的同步机制,可能会导致数据竞争(Race Condition)。
非确定性:线程执行顺序不确定,导致难以复现 Bug。
死锁(Deadlock):多个线程等待对方释放资源,导致程序卡死。
(3) 线程对整个进程的影响
线程共享进程的地址空间、文件描述符,一个线程如果出错,可能会影响整个进程。线程不能像进程那样独立崩溃,如果一个线程崩溃,整个进程可能都会崩溃。
(4) 多线程程序更难写、更难调试
由于线程执行顺序不确定,程序调试难度增加。
数据竞争、死锁、线程同步 Bug 都比单线程程序更难发现。
线程异常
在 Linux 中,线程异常会影响整个进程,因为所有线程共享同一个进程地址空间。如果某个线程发生段错误、除零异常或非法指令,整个进程都会终止。
线程异常导致进程崩溃的原因
(1) 线程共享进程地址空间
所有线程共享进程的代码段、数据段、堆、全局变量、文件描述符。
如果一个线程在共享资源上出错,整个进程都会受到影响。
(2) 线程异常触发信号(signal)
Linux 内核会用 信号机制处理异常。如果一个线程触发了这些信号,默认行为是终止整个进程。
线程用途
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
进程是资源分配的基本单位
线程是调度的基本单位
每个线程独立的数据包括:
数据项 作用
线程 ID (TID) 用于标识线程,每个线程有唯一的 ID
一组寄存器 记录当前 CPU 的寄存器状态,包括 PC、SP 等
栈 每个线程都有自己的独立栈,存储局部变量、函数调用信息
errno 变量 每个线程的错误码独立,不会相互影响
信号屏蔽字 每个线程可以单独屏蔽某些信号
调度优先级 每个线程可设置不同的调度策略和优先级
重点:独立栈是最重要的,避免了多个线程修改同一栈上的局部变量,导致数据错误
所有线程共享以下进程资源:
代码段(Text Segment) 线程共享相同的可执行代码
数据段(Data Segment) 共享全局变量、静态变量
堆(Heap) 共享动态分配的内存(如 malloc、new 分配的内存)
文件描述符表 共享打开的文件,如 open() 打开的文件
信号处理方式 共享信号的处理方式,如 SIG_IGN、SIG_DFL 或者自定义处理函数
当前工作目录 线程改变进程的工作目录(chdir)会影响所有线程
用户 ID / 组 ID 线程共享同一个用户和组身份
重点:
多线程修改全局变量时,必须加锁(如 pthread_mutex_t),否则可能发生数据竞争。堆(Heap)共享意味着不同线程可以 malloc/free 彼此分配的内存,但需要小心同步问题。
8.2 Linux线程控制
POSIX线程库:在 Linux 系统中,POSIX 线程库(POSIX Threads,简称 pthread)提供了一整套以 pthread_ 开头的 API 来管理线程。
包含头文件:#include <pthread.h>
编译时添加 -lpthread:
gcc my_thread_program.c -o my_program -lpthread
POSIX 线程库包含多个重要的 API,主要分为线程创建、同步、控制、资源管理等类别。
创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
pthread_t *thread:线程 ID,用于存储新创建的线程的标识符。
const pthread_attr_t *attr :线程属性,NULL 表示默认属性。
void *(*start_routine) (void *):新线程执行的函数。
void *arg:传递给函数start_routine 的参数,可以是 NULL。
返回值
成功:返回 0。
失败:返回错误码(非 0 的值),表示线程创建失败。
线程退出
线程库(pthread)提供的线程终止函数,用于让当前线程安全地退出并返回一个值给 pthread_join()
终止当前线程,但不会影响整个进程(与 exit() 不同)。
允许返回一个指针(retval),这个值可以被 pthread_join() 获取。
线程退出后,资源不会自动回收,需要 pthread_join() 或 pthread_detach() 释放资源
#include <pthread.h>
void pthread_exit(void *retval);
retval 是线程终止时的返回值,它可以是:
用来指示线程执行结果的数据,例如计算结果、错误码等。
pthread_join() 可以获取这个值,进行后续处理。
线程等待
pthread_join() 是一个用于等待线程终止的函数,它确保主线程或其他线程能够等待某个线程结束,并且可以获取该线程的退出状态(即 pthread_exit() 中传递的 retval 值)
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
thread:要等待的目标线程的 ID,类型为 pthread_t,由 pthread_create() 创建线程时返回。
retval:指向 void* 类型的指针,用于接收线程的返回值,即该线程通过 pthread_exit() 传递的 retval。
返回值:
0:表示成功,线程成功被 join。
非 0 值:表示失败,返回错误码。
示例:
|
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> void *funtest(void *arg) { int id = *(int *)arg; printf("线程TID:%lu,传入的值:%d\n", pthread_self(), id); int *retval = (int *)malloc(sizeof(int)); *retval = id + 10; pthread_exit((void *)retval); return NULL; } int main() { // 创建五个线程ID数组 pthread_t Tid[5]; int nums[] = {1, 2, 3, 4, 5}; for (int i = 0; i < 5; i++) { if (pthread_create(Tid, NULL, funtest, (void *)&nums[i]) != 0) { perror("创建线程失败!"); return 1; } void *retval; pthread_join(Tid[0], &retval); printf("传回的值:%d\n", *(int *)retval); free(retval); sleep(1); } return 0; } |
线程分离
默认情况下,新创建的线程是可连接(joinable)的,即它的资源不会自动释放,必须调用 pthread_join() 才能回收资源。如果不调用 pthread_join(),线程的资源会一直占用,导致内存泄漏,类似于僵尸进程的问题。如果不关心线程的返回值,pthread_join() 可能会成为一种额外的开销。在这种情况下,可以让线程在退出时自动释放资源,避免手动 pthread_join(),这称为线程分离。
设置线程分离有两种方法:
在线程外部(由其他线程设置):使用 pthread_detach(thread_id)
在线程内部(自己设置自己):使用 pthread_detach(pthread_self())
#include <pthread.h>
int pthread_detach(pthread_t thread);
pthread_t thread:要分离的线程的 线程 ID(由 pthread_create() 生成)
成功:返回 0。
失败:返回错误码(非零),并不会设置 errno
线程分离 vs 线程连接
pthread_join() 等待线程结束,并获取线程的返回值 需要线程返回结果时
pthread_detach() 让线程资源自动释放,线程退出后系统自动回收,无需 join() 不关心返回值,只关心执行完成
注意事项:
已经分离的线程不能再 pthread_join(),避免对同一线程多次 pthread_detach();由于分离的线程无法通过 pthread_join() 确保它执行完毕,主线程可能会提前退出,导致分离线程来不及执行完:
|
#include <pthread.h> #include <stdio.h> #include <unistd.h> void *funtest(void *arg) { int num = *(int *)arg; printf("线程TID:%lu,传入的值:%d\n", pthread_self(), num); sleep(1); sleep(2); // 模拟工作 printf("线程 %lu 结束\n", pthread_self()); pthread_exit(NULL); } int main() { int num = 10; pthread_t Tid; if (pthread_create(&Tid, NULL, funtest, (void *)&num) != 0) { printf("创建线程失败!"); return 1; } if (pthread_detach(Tid) != 0) { printf("线程分离失败!"); return 1; } printf("主线程执行,不等待子线程\n"); sleep(3); // 确保子线程有足够时间运行 printf("主线程退出\n"); } |
8.3线程ID及进程地址空间布局
线程ID: 在 Linux 中,每个线程都有一个唯一的 ID,通常由 内核线程 ID(TID) 和 POSIX 线程 ID(pthread_t) 组成。
线程 ID 的类型:
pthread_t POSIX 线程库 pthread_create() 返回的 ID,用户级标识
TID 线程在内核中的唯一标识 (gettid() 获取)
PID 进程 ID,在 Linux 内核中,每个进程的主线程的 TID = PID
先创建的线程可能为PID+1。
|
#include <pthread.h> #include <stdio.h> #include <sys/syscall.h> #include <unistd.h> #include <sys/types.h> void* thread_func(void *arg) { sleep(2); printf("线程 TID(pthread_self):%lu\n", pthread_self()); printf("线程 TID(syscall gettid):%ld\n", syscall(SYS_gettid)); return NULL; } int main() { pthread_t tid; pthread_create(&tid, NULL, thread_func, NULL); printf("主线程 TID(syscall gettid):%ld\n", syscall(SYS_gettid)); printf("进程PID:%ld\n",getpid()); pthread_join(tid, NULL); return 0; } |
多线程进程的地址空间
在 Linux 下,多线程的进程共享大部分地址空间,但每个线程也有自己的私有部分。理解这一点,可以从进程的内存布局和线程间的共享 vs 私有两个方面展开。
线程共享的部分
共享部分 说明
代码段 可执行指令,所有线程都可以执行相同代码。
数据段 全局变量、静态变量,所有线程都可访问和修改。
堆 (Heap) 通过 malloc() 分配的内存,所有线程都可以访问。
文件描述符表 进程打开的文件(open()),所有线程可共享。
信号处理方式 SIG_IGN, SIG_DFL 以及自定义信号处理函数。
当前工作目录 chdir() 改变进程的当前目录,所有线程都会受影响。
用户 ID / 组 ID setuid() 影响整个进程,所有线程都会变更。
线程私有的部分
私有部分 说明
寄存器 线程调度时,每个线程都有自己的寄存器状态。
栈 (Stack) 局部变量、函数调用的返回地址,不同线程互不影响。
线程 ID 每个线程都有唯一 pthread_t 线程 ID。
线程局部存储 (TLS)使用 __thread 或 thread_local 定义的变量,每个线程独立。
错误码 (errno) errno 是线程局部存储的,每个线程都有自己的 errno 变量。


104

被折叠的 条评论
为什么被折叠?



