1. Linux线程概念
1-1 什么是线程
• 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是"一个进程内部的控制序列"
• 一切进程至少都有一个执行线程
• 线程在进程内部运行,本质是在进程地址空间内运行
• 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
• 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
Linux 内核视角:
在底层,Linux 内核并不严格区分进程和线程。它使用一种通用的结构 task_struct 来管理所有执行上下文。一个"线程"在内核看来就是一个与其他 task_struct 共享某些资源(特别是虚拟内存空间)的"进程"。创建线程使用的系统调用 clone()提供了丰富的标志位,用来控制哪些资源是共享的,哪些是独享的。

1-2 线程的优点
• 创建一个新线程的代价要比创建一个新进程小得多
• 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
◦ 最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
◦ 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲TLB (快表)会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。
• 线程占用的资源要比进程少很
• 能充分利用多处理器的可并行数量
• 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
• 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
• I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
1-3 线程的缺点
• 性能损失
◦ 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
• 健壮性降低
◦ 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
• 缺乏访问控制
◦ 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
• 编程难度提高
◦ 编写与调试一个多线程程序比单线程程序困难得多
1-4 线程异常
• 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
• 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
1-5 线程用途
• 合理的使用多线程,能提高CPU密集型程序的执行效率
• 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
2. Linux进程VS线程
进程 是资源分配的基本单位。每个进程都有自己独立的地址空间、文件描述符表、信号处理等。
线程 是 CPU 调度的基本单位 。一个进程可以包含多个线程,这些线程共享进程的绝大部分资源(进程中的线程共用一份虚拟地址空间),但每个线程拥有自己独立的:
◦ 线程ID
◦ 一组寄存器(保存线程的上下文数据)
◦ 栈
◦ errno
◦ 信号屏蔽字
◦ 调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
• 文件描述符表
• 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
• 当前工作目录
• 用户id和组id
进程和线程的关系如下图:

关于进程线程的问题: 如何看待之前学习的单进程?具有一个线程执行流的进程
3. Linux线程控制
3-1 POSIX线程库
• 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"打头的
• 要使用这些函数库,要通过引入头文 <pthread.h>
• 链接这些线程函数库时要使用编译器命令的"-lpthread"选项
Linux 遵循 POSIX 标准,其线程 API 通常称为 pthread 。使用时需要包含头文件 <pthread.h>,并在编译时链接线程库 -lpthread(现代编译器通常只需 -pthread 选项,它会同时处理编译和链接)。
3-2 pthread_xxx函数
创建线程
pthread_create函数用于创建一个新的线程,新线程与调用线程并发执行。
cpp
#include <pthread.h>
int pthread_create(pthread_t * thread,
const pthread_attr_t * attr,
void *(*start_routine)(void *),
void * arg);
参数详解
thread(输出参数)
作用:返回线程ID。这个ID后续可用于其他线程操作,如 pthread_join, pthread_cancel 等。
attr (输入参数)
作用:用于设置新线程的各种属性(如栈大小、调度策略、分离状态等)。如果传入 NULL,则使用默认属性创建线程。通常,对于初学者或不需要特殊配置的场景,直接传入 NULL 即可。
start_routine (函数指针)
指向一个参数、返回值均是(void*)的函数
作用:这是一个函数指针 ,指向新线程要执行的函数。这个函数必须满足以下签名:
cpp
void *thread_function(void *arg);
参数 arg:就是传递给该函数的第四个参数 arg。
返回值 void *:线程的退出状态。可以通过 pthread_join 来获取这个返回值。
arg (输入参数)
- 类型:void *
- 作用:传递给 start_routine 函数的参数。它是一个泛型指针,可以传递任何类型的地址(如基本类型的地址、结构体指针等)。如果不需要传递参数,可以传入 NULL。
返回值
成功 :返回 0。 失败 :返回一个错误码 (非零值),而不是设置 error。
错误检查:
• 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
• pthread_create函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
• pthread_create同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthread_create函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小
cpp
#include <pthread.h>
// 获取调用此函数的当前线程自身的线程 ID。
pthread_t pthread_self(void);
代码实践:
cpp
#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
using namespace std;
void* thread_function(void* arg)
{
string name=(const char*)arg;
while(true)
{
cout<<"我是新线程,name:"<<name<<" pid:"<<getpid()<<" tid:"<<pthread_self()<<endl;
sleep(1);
}
return (void*)0;
}
int main()
{
pthread_t tid;
int n=pthread_create(&tid,NULL,thread_function,(void*)"thread-1");
if(n!=0)
{
cout<<"pthread_create fail"<<endl;
exit(1);
}
sleep(1);
while(true)
{
cout<<"我是主线程,pid:"<<getpid()<<" 新线程tid:"<<tid<<endl;
sleep(1);
}
return 0;
}
在Linux中,ps -aL 命令用于查看当前系统的线程信息
| PID | 进程的ID |
|---|---|
| LWP | 轻量级进程ID,也是线程ID。对于主线程,LWP通常等于PID |
| TTY | 进程关联的终端 |
| TIME | 进程实际使用CPU运行的时间 |
| CMD | 进程或线程的命令名称 |

打印出来的 tid 是通过 pthread 库中有函数 pthread_self 得到的,它返回一个 pthread_t 类型的变量,指代的是调用 pthread_self 函数的线程的 "ID"。
怎么理解这个"ID"呢?这个"ID"是 pthread 库给每个线程定义的进程内唯一标识,是 pthread 库维持的。
由于每个进程有自己独立的内存空间,故此"ID"的**作用域是进程级而非系统级(内核不认识)**。
LWP 是什么呢?LWP 得到的是真正的线程ID。之前使用 pthread_self 得到的这个数实际上是一个地址(指向pthread库中描述线程的结构体),在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。
在 ps -aL 得到的线程ID,有一个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。
🎯 核心区别总结
| 特性 | pthread_t (线程库ID) | LWP (内核线程ID) |
|---|---|---|
| 层级 | 用户空间(线程库级别) | 内核空间(操作系统级别) |
| 作用域 | 进程内唯一 | 系统全局唯一 |
| 用途 | 线程库函数调用 | 系统调用、系统工具查看 |
| 可见性 | 仅线程库可见 | 系统全局可见 |
| 稳定性 | 可能被复用 | 生命周期内唯一不变 |
🎓 重要概念澄清
主线程的LWP = 进程的PID
其他线程的LWP ≠ 进程的PID
为什么需要两种ID?
pthread_t:为线程库提供抽象,隐藏不同系统的实现差异
LWP:为内核提供实际的调度实体
💡 总结
pthread_t 是pthread库中描述线程的结构体的首地址
LWP 是"内核空间的线程标识",用于系统调度和监控
终止线程
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
-
从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
-
线程可以调用pthread_ exit终止自己。
-
一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_cancel
功能:取消一个执行中的线程
cpp
#include <pthread.h>
int pthread_cancel(pthread_t thread);
//参数: thread:线程ID
//返回值:成功返回0;失败返回错误码
pthread_exit
cpp
#include <pthread.h>
void pthread_exit(void *retval);
//参数说明 retval:线程的退出状态值,可以被其他线程通过 pthread_join() 获取
功能说明
1.终止当前线程:调用该函数的线程会立即终止执行
2.资源清理:自动调用线程的清理函数
3.返回值传递:退出状态值可以通过pthread_join被其他线程获取
线程等待
为什么需要线程等待?
• 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
• 创建新的线程不会复用刚才退出线程的地址空间。
pthread_join是一个重要的线程同步函数,用于等待指定线程终止并回收其资源。
cpp
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
// 参数说明
// thread:要等待的线程ID
// retval:指向指针的指针,用于接收线程的返回值(输出型参数)
// 返回值
// 成功:返回 0 失败:返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
-
如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
-
如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
-
如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
-
如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
功能说明
1.阻塞等待:调用线程会阻塞,直到指定的目标线程终止
2.资源回收:回收已终止线程的资源,防止内存泄漏
3.获取返回值:可以获取目标线程的返回值
代码实践:
cpp
#include <stdio.h>
int snprintf(char *str, size_t size, const char *format, ...);
// 参数说明
// str: 目标字符串缓冲区
// size: 缓冲区大小(包括终止空字符)
// format: 格式字符串
// ...: 可变参数列表
// 返回值
// 成功时:返回本应写入的字符数(不包括终止空字符)
// 错误时:返回负值
//关键特性
// 缓冲区安全
// snprintf() 最多写入 size-1 个字符,并在末尾自动添加空字符,防止缓冲区溢出。
cpp
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;
void *thread_function(void *arg)
{
string name = (const char *)arg;
cout << "我是新线程,name:" << name << " tid:" << pthread_self() << endl;
sleep(1);
return arg;
}
int main()
{
pthread_t a[6];
for (int i = 1; i <= 5; i++)
{
//不能定义栈上的缓冲区 char buffer[64],栈上缓冲区较为稳定,不会改变
//这会导致所有线程都共享这同一个缓冲区且不断对它修改,结果会出错
pthread_t tid;
// 每个线程申请的堆空间都不一样
// 原则上堆空间也是共享的
// 但只让对应的线程知道这部分堆空间的起始虚拟地址,就可以叫做这个堆空间对是该线程拥有的
char *name = new char[64];
snprintf(name, 64, "thread-%d", i);
// 给每个线程传递它申请的堆空间地址,则相当于这个堆空间只有该线程使用,拥有
int n = pthread_create(&tid, NULL, thread_function, name);
a[i] = tid;
if (n != 0)
{
cout << "pthread_create fail" << endl;
exit(1);
}
}
sleep(2);
for (int i = 1; i <= 5; i++)
{
void *reval;
int n = pthread_join(a[i], &reval);
if (n != 0)
{
cout << "pthread_join fail" << endl;
exit(1);
}
cout << "线程thread-" << i << "等待成功,线程返回值:" << (long long)reval << endl;
}
cout << "主线程退出" << endl;
return 0;
}

分离线程
• 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
• 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
cpp
#include <pthread.h>
int pthread_detach(pthread_t thread);
//参数说明 thread: 要分离的线程ID
功能描述
pthread_detach 用于将指定的线程标记为"分离状态"(detached state)。分离状态的线程在终止时会自动释放其占用的系统资源,无需其他线程调用 pthread_join 来回收。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
cpp
pthread_detach(pthread_self());
- 分离状态 vs 可连接状态
可连接状态(joinable): 默认状态,需要其他线程调用 pthread_join 来回收资源
分离状态(detached): 线程终止时自动回收资源
注意事项
1.一次性操作: 线程一旦被分离,就不能再被连接
2.资源管理: 分离线程确保不会产生僵尸线程
3.无法获取返回值: 分离的线程不能使用 pthread_join 获取返回值
4.主线程退出: 如果主线程退出,所有分离的线程也会被终止
5.joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
6.线程分离一般用于软件中(软件都是死循环,主线程不会退出)
代码实践:
cpp
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void* thread_function(void* arg) {
printf("线程正在执行...\n");
sleep(2);
printf("线程结束\n");
return NULL;
}
int main() {
pthread_t thread_id;
// 创建线程
if (pthread_create(&thread_id, NULL, thread_function, NULL) != 0) {
perror("线程创建失败");
return 1;
}
// 分离线程
if (pthread_detach(thread_id) != 0) {
perror("线程分离失败");
return 1;
}
printf("主线程继续执行,无需等待分离的线程\n");
sleep(3); // 给分离线程时间执行
return 0;
}

4. 线程ID及进程地址空间布局
• pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

