在上一篇文章中,我们详细介绍了线程的基本概念,这一章我先把上一节剩余的内容给补充完,线程的周边,接着我们学习线程的控制;
一.线程的周边:
线程与进程切换的对比
线程切换和进程切换最大的区别在于地址空间变不变。
- 线程切换地址空间不变,所以TLB和Cache不失效,数据继续可用,页表也不需要切换。
- 进程切换地址空间改变,TLB和Cache全部失效,必须重新加载,页表也得换。
- 共享资源方面,线程之间共享文件描述符和信号处理器,进程各自独立需要重新加载。上下文保存的量也不一样,线程只需要保存寄存器、栈指针、程序计数器这些少量数据,进程要保存完整映像。
- 是否进内核方面,线程看实现,用户级不进内核级进,进程必须进。速度上线程快得多,主要开销是保存恢复私有数据,进程那边是地址空间切换加上TLB Cache刷新再加上内核模式切换。
用户级线程和内核级线程的对比
用户级线程切换不进内核,完全在用户态完成,不涉及系统调用,所以速度极快。
内核级线程切换要进内核,需要内核调度器参与,涉及系统调用,速度较快但还是比进程快得多。
共同点是两者地址空间都不变,TLB和Cache都不失效。典型例子,用户级线程比如协程和早期Java的绿色线程,内核级线程比如Linux的pthread和Windows线程。
用户态和内核态的对比
- 用户态权限受限,不能直接操作硬件,跑的是应用程序代码,切换开销低。
- 内核态权限全开,能访问所有资源,跑的是操作系统核心代码,切换开销高,因为要保存上下文、权限检查、切换堆栈。
- 用户态切换到内核态由系统调用、中断或异常触发。
每个线程独有的资源
寄存器组是独立的,包括程序计数器、栈指针和通用寄存器。
栈也是独立的,每个线程有自己的用户栈,内核级线程还有独立的内核栈。
线程控制块存储线程ID、优先级和当前状态。
线程的状态变化
新建状态是线程刚创建还没运行;
就绪状态是等待CPU调度;
运行状态是正在CPU上执行;
阻塞状态是等待I/O、锁或信号;
终止状态是执行完毕或被强制结束。
Cache的关键特性
Cache对程序员不可见,完全透明。进程切换时Cache全部失效,需要重新缓存。线程切换时Cache不失效,继续使用已有缓存。
二. Linux进程VS线程 — 哪些资源共享,哪些独占
- 进程间具有独⽴性
- 线程共享地址空间,也就共享进程资源
2.1 进程和线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也拥有⾃⼰的⼀部分"私有"数据:
- 线程ID
- ⼀组寄存器,线程的上下⽂数据
- 栈
- errno
- 信号屏蔽字
- 调度优先级
线程ID
每个线程在系统里都有一个唯一的标识符,用来区分同一个进程里的不同线程。不管是用户级线程还是内核级线程,都有自己的ID,方便操作系统或线程库进行管理和调度。
一组寄存器(线程的上下文数据)
线程在运行过程中,CPU里的寄存器保存着当前执行状态,包括程序计数器(记录执行到哪条指令)、栈指针(指向栈顶)和各种通用寄存器。切换线程的时候,这些寄存器内容要保存起来,等切回来的时候再恢复,这就是线程上下文切换的核心工作。
栈
每个线程都有自己独立的栈空间,用来存放函数调用信息、局部变量、返回地址等。为什么必须独立?因为如果共用栈,函数调用就会互相干扰,A线程调用一个函数,返回地址被B线程覆盖了,程序直接崩溃。用户级线程有自己的用户栈,内核级线程还会有独立的内核栈。
errno
errno这个变量比较特殊,它看起来像是全局变量,但实际上在多线程环境下每个线程都有一份独立副本。原因是系统调用或库函数出错时会设置errno,如果所有线程共用同一个errno,A线程报错了还没来得及处理,B线程又发起系统调用把errno覆盖掉了,A线程拿到的是错误信息就是错的。所以必须线程私有,各管各的。
信号屏蔽字
信号屏蔽字决定了当前线程对哪些信号暂时不处理、哪些信号可以接收。同一个进程里的不同线程可以有不同的屏蔽策略,比如工作线程可以屏蔽大部分信号,专心处理计算任务,主线程专门负责响应信号。这样设计让线程对信号的处理更加灵活可控。
调度优先级
同一个进程的不同线程可以设置不同的调度优先级。优先级高的线程更容易被CPU选中执行,优先级低的后执行。这给了程序员控制线程调度顺序的能力,比如把处理用户交互的线程设高优先级,把后台数据同步的线程设低优先级,提升用户体验。
2.2 进程的多个线程共享
进程的多个线程共享同一地址空间,因此 Text Segment、Data Segment 都是共享的。如果定义一个函数,各线程都可以调用,如果定义一个全局变量,各线程都可以访问到。
除此之外,各线程还共享以下进程资源和环境:
-
文件描述符表:一个线程打开的文件,其他线程可以直接使用同一个文件描述符来读写,不需要各自重新打开。因为文件描述符表属于进程,所有线程共用这张表。
-
每种信号的处理方式:信号的处理函数是进程级别的设置。比如进程对SIGINT设置了自定义处理函数,或者设置为忽略或默认动作,所有线程都共用这个设置,不能各线程自己改。注意,这和信号屏蔽字不同,屏蔽字是线程私有的,但处理方式(忽略、默认、自定义函数)是进程全局的。
-
当前工作目录:进程只有一个当前工作目录,所有线程共享。任何一个线程调用chdir修改了工作目录,其他线程的当前目录也跟着变了。
-
用户ID和组ID:进程的身份标识属于进程本身,所有线程共用相同的用户ID和组ID。内核检查权限时看的是进程级别的ID,不会区分是哪个线程发起的操作。
知识补充:
文件描述符表是共享的,但每个线程有自己独立的文件偏移指针吗?不,文件偏移也保存在文件描述符表里,所以也是共享的。一个线程用read读了一部分数据,文件偏移往前移了,另一个线程再读就从新位置开始读了。如果要独立的偏移,得各自打开同一个文件拿到不同的文件描述符。
信号处理方式是进程共享的,但信号屏蔽字是线程私有的。这俩容易搞混,记清楚:怎么处理信号听进程的,当前阻塞哪些信号听线程自己的。
当前工作目录只有一个,一个线程改了全进程都受影响,这个好理解,因为目录是进程的上下文属性。
用户ID和组ID是进程级别的,所有线程一视同仁,权限检查不区分线程。
进程和线程的关系如下图:

2.3 关于进程线程的问题
如何看待之前学习的单进程?
之前学的单进程,本质上就是只有一个线程执行流的进程。进程创建后,从main函数开始,沿着一条执行路径顺序往下走,调用函数、分支跳转、循环,整个过程只有一条控制流。
有了多线程的概念之后再回头看单进程,可以这样理解:单进程就是进程里只跑了一个线程,这个线程拥有进程的全部资源,独占地址空间、文件描述符表、信号处理方式等,不存在资源共享和竞争的问题,所以编程模型简单直接,不用考虑锁、互斥、同步这些复杂的东西。
但它也有局限:单核上跑单进程,CPU执行到I/O操作时就得阻塞等待,CPU就闲着,浪费资源。多线程的好处就是可以在一个线程阻塞等待I/O时,让另一个线程继续使用CPU,提升利用率。另外在多核机器上,单进程单线程只能用一个核,其他核白白空着,性能天花板很低。
所以可以这么说:单进程单线程是基础模型,简单可靠,适合逻辑清晰、I/O密集度低或者不需要并行计算的场景。多线程是在这个基础上把执行流拆开,让一个进程能干多件事,但同时也引入了资源竞争和同步的复杂性。两者是取舍关系,没有绝对的好坏,看场景选。
概括一下,方便记忆:单进程单线程 = 一个进程里只有一个执行流,从头跑到尾。多线程 = 同一个进程里分出多条执行流,共享资源各跑各的。
三.Linux线程控制
3.1 POSIX线程库
3.2 创建线程
功能:创建⼀个新的线程
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
参数:
- thread:返回线程ID
- attr:设置线程的属性,attr为NULL表⽰使⽤默认属性
- start_routine:是个函数地址,线程启动后要执⾏的函数
- arg:传给线程启动函数的参数
- 返回值:成功返回0;失败返回错误码
接下来讲的是pthread线程库的两个核心知识点:错误检查和线程ID。
1.错误检查
传统系统调用和pthreads函数的错误处理方式不同。传统函数比如open、read这些,成功返回0,失败返回-1,并且设置全局变量errno来记录具体错误原因,事后用perror或strerror(errno)来查。但pthreads函数不这么干,它失败时不会碰errno,而是直接把错误码通过返回值告诉调用者。
虽然pthreads也提供了线程内的errno变量来兼容老代码,但官方建议直接判断返回值,因为读返回值的开销比访问线程内errno要小。
配套的示例代码演示了正确的错误处理写法:pthread_create的返回值赋给ret,判断ret不等于0就是出错,然后用strerror(ret)把错误码转成可读的字符串输出,而不是用perror。
代码本身的功能很简单,主线程创建一个子线程,两个线程各自无限循环,每秒打印一条消息,演示了线程的创建和并发执行。
2.线程ID
pthread_self函数返回调用线程的ID,类型是pthread_t。这个ID是pthread库在进程范围内维护的唯一标识,只在当前进程内部有效,内核不认它。它和内核的线程ID是两回事——pthread底层通过clone等系统调用创建线程时,内核会给每个线程分配一个系统全局唯一的ID,也就是ps命令里看到的LWP(轻量级进程ID)。
用ps -L命令查看线程信息时,可以看到同一进程里的多个线程拥有相同的PID,但LWP各不相同。主线程的LWP通常和PID相等,子线程的LWP是另一个独立的值。
三个关键点:
-
pthreads函数检查错误看返回值,不看errno
-
pthread_t是用户态线程ID,进程内唯一,内核不认识
-
LWP是内核态线程ID,系统全局唯一,ps -L能看到
代码如下:
这个程序演示了最简单的多线程编程。主线程启动后创建一个子线程,然后两个线程各自独立运行,每隔一秒打印一条消息,直到用户按Ctrl+C终止程序。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg) {
while (1) {
printf("I'm thread 1\\n");
sleep(1);
}
return NULL;
}
int main(void) {
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, rout, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create error: %s\\n", strerror(ret));
exit(EXIT_FAILURE);
}
while (1) {
printf("I'm main thread\\n");
sleep(1);
}
return 0;
}
-
pthread_create 创建线程,成功返回0,失败直接返回错误码(不设 errno)
-
错误处理用 strerror(ret) 转成可读信息,不用 perror
-
编译必须加 -pthread 选项
-
两个线程各自独立运行,调度顺序由操作系统决定
打印出来的 tid 是通过 pthread 库中有函数 pthread_self 得到的,它返回⼀个 pthread_t 类型的变量,指代的是调⽤ pthread_self 函数的线程的 “ID”。
怎么理解这个“ID”呢?这个“ID”是 pthread 库给每个线程定义的进程内唯⼀标识,是 pthread 库维持的。由于每个进程有⾃⼰独⽴的内存空间,故此“ID”的作⽤域是进程级⽽⾮系统级(内核不认识)。
其实 pthread 库也是通过内核提供的系统调⽤(例如clone)来创建线程的,⽽内核会为每个线程创建系统全局唯⼀的“ID”来唯⼀标识这个线程。

使⽤PS命令查看线程信息

-
ps -aL:显示所有进程的线程信息,-a 显示所有终端下的进程,-L 显示线程信息,每一行代表一个线程
-
| head -1:取第一行,也就是表头(PID LWP TTY TIME CMD 这些列名)
-
&&:前面的命令执行成功后才执行后面的命令
-
ps -aL | grep mythread:在所有线程信息里过滤出包含 mythread 的行
有以上基础那就开始我们今天的实验
实验一:C++11 std::thread 跨平台线程代码
Linux 线程基础:LWP、线程特性与 pthread 编译报错
1.实验目的
通过编写简单多线程代码,理解 Linux 下线程的内核实现,验证多线程程序的特点,同时解决 pthread 编程经典编译错误。
2.核心理论知识点
a.Linux 线程本质:轻量级进程 LWP
Linux 内核不存在专门的线程结构,进程、线程统一使用task_struct描述。线程也被叫做LWP(Light Weight Process,轻量级进程)。
我们可以使用命令查看线程信息:
ps -aL

观察现象:
简单区分: 进程:拥有独立资源的 task_struct 线程:和其他 task_struct 共享资源的轻量级进程
b. 多线程程序重要特性
CPU 时间片分配 操作系统分配给进程的时间片,会分给进程内部所有线程,线程之间轮流调度执行。
健壮性较差 进程中任意一个线程崩溃触发异常,整个进程都会直接退出。 线程之间没有内存隔离,一旦某个线程出错,操作系统回收整个进程资源,所有线程一起结束。写多线程代码时需要考虑更多并发安全问题。
多线程输出混杂问题 主线程和子线程同时调用 cout 打印信息时,输出内容容易交错乱序。 原因:std::cout 输出没有原子保护,多个线程竞争输出缓冲区,导致打印信息穿插。想要规范输出需要借助互斥锁。
3.实验代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
void* threadrun(void* args)
{
std::string name = (const char*)args;
while(true)
{
std::cout << "我是新线程: name: " << name << ", pid: " << getpid() << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadrun, (void*)"thread-1");
while(true)
{
std::cout << "我是主线程… " << ", pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
代码解析:
这个程序用 pthread 库创建了一个子线程,主线程和子线程各自无限循环打印消息,演示最基本的线程创建和并发执行。
整体功能
主线程创建子线程后,两个线程各自独立运行,每隔一秒输出一条信息,直到按 Ctrl+C 终止。
逐部分拆解
头文件引入 pthread.h 提供线程相关函数,iostream 提供 cout 输出,string 用来处理传入的字符串参数。
threadrun 是子线程的入口函数,参数是 void* 类型,可以接收任意指针。函数内部把 args 强转成 const char* 再构造为 std::string 对象,拿到传入的 "thread-1"。然后进入死循环,每次打印线程名字后休眠一秒,return nullptr 实际上永远执行不到。
main 函数里声明 pthread_t 类型的变量 tid 用来存线程 ID。pthread_create 创建线程,第一个参数传 tid 的地址让函数填充线程 ID,第二个参数传 nullptr 表示使用默认线程属性,第三个参数是线程入口函数,第四个参数是传给入口函数的参数,这里传了字符串 "thread-1" 并强转成 void* 类型。创建成功后新线程立即开始执行 threadrun。主线程也进入死循环,打印自己的消息后休眠一秒。

大家会发现我这个加了static,原因是起初我想验证这个不加-pthread,make进行编译的时候会报错,但我的没有发生报错,因为我用的,时ubuntu22.04,加这个static就是版本高的配置进行报错的,但发现还能运行,原因如下:
Ubuntu 系统 glibc 版本 ≥ 2.34
- glibc 2.33 及更早版本:pthread_create 在独立 libpthread.so,不加 -pthread 一定会报 undefined reference
- glibc 2.34 重大改动:把 pthread 的所有函数整合进 shturl.(系统标准 C 库) libc 默认都会链接,所以链接器自动找到符号,不会报错!
我加了之后,但是我的这个还可以运行:
因为的 Ubuntu 系统 glibc 版本 ≥ 2.34。 glibc2.34 做了重大改动:把所有 pthread 函数全部合并进 libc 主库,废除独立的 libpthread 库。 所以: 动态链接、甚至带上-static静态链接,不加 -pthread 也能找到 pthread_create 符号,可以编译运行。
错误 Makefile(无 – pthread,用来复现链接问题)
mythread:mythread.cc
g++ -O0 -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f mythread
正确 Makefile(带上 – pthread)
mythread:mythread.cc
g++ -O0 -o $@ $^ -pthread -std=c++11
.PHONY:clean
clean:
rm -f mythread
补充实验说明
mythread:mythread.cc
g++ -O0 -static -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f mythread
但大家在演示的时候一般正常报错时,显示这个:
/usr/bin/ld: mythread.o: in function `main':
mythread.cc:(.text+0x45): undefined reference to `pthread_create'
collect2: error: ld returned 1 exit status
make: *** [mythread] Error 1
- undefined reference to 'pthread_create' 核心错误:链接器找不到 pthread_create 函数实现;
- /usr/bin/ld:代表链接阶段出错,不是编译阶段;
- make: *** [mythread] Error 1 make 捕获 g++ 异常,终止构建。
编译运行:

实验现象与结论
从编译问题深挖:pthread 线程库底层原理
面我们做实验发现一个关键问题:使用线程相关接口,如果不规范添加编译参数,在不同 Linux 系统上表现不一样。想要弄懂背后原因,就要搞清楚 pthread 库到底是什么、Linux 线程底层如何实现。
1.Linux 内核视角:没有 “线程”,只有轻量级进程 LWP
Linux 内核中统一使用task_struct描述所有执行流,内核不存在专门的线程结构。 内核只提供底层系统调用 clone(),用来创建新执行流。通过传入不同 flags 参数,可以控制新执行流是否共享地址空间、文件描述符等资源:
- 资源完全不共享 –>普通进程(fork 底层也是依靠 clone)
- 共享地址空间等资源 –> 轻量级进程 LWP
我们日常所说的 “线程”,本质就是依靠 LWP 模拟出来的概念。
视角区分: 内核视角:只有进程、LWP,没有线程; 用户 / 应用视角:我们直接使用线程进行并发开发。
直接调用clone()参数繁杂、使用门槛极高,不适合上层开发者,于是诞生了 pthread 库(POSIX 原生线程库)。 pthread 属于用户层库,它封装了clone()系统调用,自动完成线程栈分配、线程局部存储 TLS 管理等复杂工作,对外提供pthread_create、pthread_join这类简洁易用的线程接口。
2.延伸:C++11 std::thread 和 pthread 的关系
C++11 新增了std::thread,目的是实现语言层面跨平台并发。
- 在 Linux 平台:std::thread底层依旧封装 pthread 库;
- 在 Windows 平台:编译器内部封装 Windows 原生线程 API。
实现思路:编译器针对不同操作系统编写两套底层代码,通过条件编译区分平台,向上统一暴露std::thread接口,以此保证 C++ 代码可移植。 这也侧面说明:Linux 下但凡原生线程,底层离不开 pthread。
简易代码如下:
代码解析:
整体功能
这个程序用C++11的std::thread创建了一个子线程,主线程和子线程各自每隔一秒打印一条消息和进程PID,无限循环,直到按Ctrl+C终止。验证了线程共享同一个进程ID。
逐部分拆解
头文件iostream提供std::cout输出,thread提供std::thread类,unistd.h提供sleep和getpid函数。hello函数是子线程的入口函数,无参无返回值。内部死循环,每次打印"新线程:hello world"和当前进程PID,然后休眠一秒。
main函数里用std::thread t(hello)创建线程,hello是函数名,传入后新线程立即开始执行hello函数。主线程进入死循环,打印"我是主线程…"和进程PID,每秒一次。t.join()这行永远执行不到,因为主线程在死循环里出不来。return 0同样执行不到。

运行结果:

注意:哪怕使用 C++11 std::thread,Linux 环境编译依旧建议带上 -pthread,底层依赖 pthread!
现象观察
解读一下:
1.PID 相同代表什么?
Linux 内核视角:线程本质是轻量级进程 LWP。 多个 LWP 属于同一个进程,共享同一个进程地址空间、同一个 PID。 内核内部靠不同 LWP 标识区分执行流,对外展示的进程 ID 一致。 这直接验证理论:Linux 不存在独立线程内核对象,线程就是共享资源的 LWP。
2.打印内容混乱交错的原因
std::cout 输出没有加同步保护。 两个线程同时向终端输出,CPU 调度来回切换,输出语句被打断、穿插,出现打印乱序。
拓展结论:多线程并发访问同一个输出流 / 共享资源,存在竞争问题,想要有序输出需要互斥锁同步。
3.ID为啥那么长?
两个线程同时调用 std::cout 输出,输出中途线程被切换,两段打印内容拼接在一起了。
拆解这次错乱:
原因回答:
真实 PID 只有 1005197。 出现超长数字,是多线程并发输出竞争导致的打印穿插现象。 两个线程不加任何锁保护,同时向标准输出打印内容。操作系统随时可能中断当前线程、切换另一个线程运行。 一条 cout 还写完,另一条 cout 开始输出,两段文本直接拼接在一块,看起来 PID 变成一串超长数字。
延伸结论: std::cout 本身不具备线程同步能力。多线程同时打印,输出内容乱序、穿插是典型并发竞争问题。想要有序输出,必须使用互斥锁(mutex)保护打印代码段。
和前面知识点串联
总结
运行程序后可以看到:主线程与新建线程打印的 PID 一致。 证明多个线程隶属于同一个进程,共享进程资源,印证 Linux 线程基于轻量级进程 LWP 实现。 同时终端打印信息杂乱交错,说明多线程并发输出存在竞争,缺少同步机制时输出会互相穿插。
补充区分易混接口 clone () 与 vfork ()
vfork()同样让子进程共享父进程地址空间,但和线程完全无关: vfork 创建子进程后会阻塞父进程,设计初衷只为快速执行 exec 替换程序,不要和 LWP 线程概念混淆。


实验总结
现象解释
实验 二:拓展实验 —— 线程崩溃特
基于实验一的代码改造,验证:一个线程崩溃,整个进程全部退出
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
void* threadrun(void* args)
{
std::string name = (const char*)args;
while(true)
{
sleep(1);
std::cout << "我是新线程: name: " << name << ", pid: " << getpid() << std::endl;
// 触发除零错误,线程崩溃
int a = 10;
a /= 0;
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadrun, (void*)"thread-1");
while(true)
{
std::cout << "我是主线程…, pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
代码解析:
整体功能
这个程序用 C 风格的 pthread 库创建线程,然后在子线程里故意触发除零错误,用来验证“线程崩溃会导致整个进程终止”这个结论。
代码功能
主线程创建一个子线程,传入参数 "thread-1"。子线程每隔一秒打印自己的名字和进程PID,然后执行 a /= 0 触发除零异常。主线程每隔一秒打印自己的消息和PID。子线程崩溃后整个进程退出,主线程也跟着一起终止。
逐部分拆解
头文件引入了 iostream 用于 cout 输出,pthread.h 提供线程相关函数,unistd.h 提供 sleep 和 getpid,string 用于处理字符串参数。
threadrun 是子线程入口函数,参数是 void* 类型,传进来的是字符串 "thread-1",强转成 const char* 后构造为 std::string。进入死循环后先休眠一秒,然后打印线程名字和当前进程PID。紧接着声明 int a = 10,执行 a /= 0,除零操作会触发硬件异常,CPU 产生陷阱,操作系统向进程发送 SIGFPE 信号,默认动作是终止进程。
main 函数里声明 pthread_t tid,调用 pthread_create 创建线程,第四个参数传入 "thread-1" 作为线程入口函数的参数。创建成功后主线程进入死循环,每秒打印 "我是主线程…" 和进程PID。
运行结果:

现象解读一下:
- 线程与新线程打印 pid 相同 再次验证:Linux 线程是轻量级进程 LWP,同进程内所有线程共享同一个进程 ID。
- 新线程执行 a /= 0,触发除零硬件异常(SIGFPE) Linux 信号机制:信号针对整个进程生效。 异常在新线程产生,信号递送给整个进程,操作系统销毁全部执行流。 核心实验结论:进程中任意一个线程触发致命异常,整个进程直接退出,所有线程一同消失。线程之间没有独立故障隔离
总结:所有线程属于同一个进程,进程资源共享。线程没有独立的信号隔离,任意线程触发致命错误,整个进程都会被操作系统回收。
这也是线程健壮性很低的一个缺点。
实验三:pthread 线程共享资源、线程等待与返回值观察
实验代码
#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <pthread.h>
int flag = 100;
void showtid(pthread_t &tid)
{
printf("tid: 0x%lx\\n", tid);
}
std::string FormatId(const pthread_t &tid)
{
char id[64];
snprintf(id, sizeof(id), "0x%lx", tid);
return id;
}
void *routine(void *args)
{
std::string name = static_cast<const char *>(args);
pthread_t tid = pthread_self();
int cnt = 5;
while (cnt)
{
std::cout << "我是一个新线程: 我的名字是: " << name << " 我的Id是: " << FormatId(tid) << std::endl;
sleep(1);
cnt–;
flag++;
}
return (void*)123;
}
int main()
{
pthread_t tid;
int n = pthread_create(&tid, nullptr, routine, (void *)"thread-1");
showtid(tid);
int cnt = 5;
while (cnt)
{
std::cout << "我是main线程: 我的名字是: main thread" << " 我的Id是: "
<< FormatId(pthread_self()) << ", flag: " << flag << std::endl;
sleep(1);
cnt–;
}
void *ret = nullptr;
pthread_join(tid, &ret);
std::cout << "ret is : " << (long long int)ret << std::endl;
return 0;
}
代码解析:
这个程序比之前的demo多了几个新东西:获取线程ID、线程等待回收、线程退出码、以及共享全局变量的访问。
整体功能
主线程创建一个子线程,两个线程各自打印5次消息后退出,子线程修改全局变量flag,主线程等待子线程结束并回收它的退出码。
逐部分拆解
开头定义了一个全局变量 int flag = 100,所有线程共享,用来验证线程间可以访问同一份全局数据。
showtid函数用printf打印线程ID,格式是16进制。FormatId函数把pthread_t类型的线程ID转成16进制字符串,内部用snprintf格式化到字符数组再转成string返回。pthread_t在不同系统上可能是整数也可能是结构体,用16进制打印是常见做法。
routine是子线程入口函数,参数是void*,传进来后强转成字符串作为线程名字。pthread_t tid = pthread_self()获取当前线程自己的ID。定义cnt=5作为循环次数,每次循环打印线程名字、线程ID,然后sleep(1),cnt减1,flag加1。循环5次后退出,返回(void*)123作为线程退出码。
main函数里声明pthread_t tid,调用pthread_create创建线程,传入"thread-1"作为名字。创建成功后调用showtid打印子线程的ID。主线程同样循环5次,每次打印自己的信息,包括用FormatId(pthread_self())获取自己的ID,以及读取flag的值。循环结束后调用pthread_join(tid, &ret)等待子线程结束,ret接收子线程返回的退出码。最后打印ret的值,强转成long long int输出。
makefile
mythread:mythread.cc
g++ -O0 -o $@ $^ -std=c++11 -pthread
.PHONY:clean
clean:
rm -f mythread
运行结果:

程序运行现象
运行程序后,终端打印出了两条不一样的线程 ID: 主线程 Id:0x7fc8d6d313c0 新线程 Id:0x7fc8d6d2d640
全局变量 flag 一开始是 100,随着程序不断运行,可以看到主线程打印的 flag 慢慢变成 102、103、104,数值持续上涨。 等到两个线程循环全部跑完之后,程序最后打印 ret is : 123。
现象分析
每个线程都有自己独有的 tid,通过pthread_self()可以获取。 这里的 tid 是 pthread 库在用户层给线程设置标识,用来区分同一个进程里不同线程。要注意,这个 tid 不是内核里面的 LWP 编号。之前实验也看到,所有线程调用 getpid 拿到的进程 ID 都是相同的。
新线程修改全局变量 flag,主线程能够看到修改之后的数据。 这说明同一个进程下所有线程共享地址空间,全局变量在线程之间是共享可见的。这也带来一个问题,如果多个线程同时修改同一个变量,不加保护就会出现数据竞争问题。
主线程调用pthread_join阻塞等待新线程运行结束。 新线程正常执行完循环,return 返回 (void*) 123,主线程通过 join 拿到了这个返回值,最后打印出来。 能够看出,pthread_join 的作用就是等待目标线程执行完毕,并且接收线程正常退出时的返回值。
和之前实验串联得到重要结论
pthread_join 只有在线程正常跑完代码才有用。 如果新线程内部出现除零错误、段错误这类崩溃,系统会发送信号,直接干掉整个进程,代码根本运行不到 pthread_join。 也就是说 join 不能捕获线程崩溃,线程异常崩溃属于进程信号层面的问题,依靠 join 无法处理。
总结:
网硕互联帮助中心





评论前必须登录!
注册