Linux下pthread线程创建实验全解析:从原理到常见坑

📅 发布时间:2026/9/1 5:19:06
Linux下pthread线程创建实验全解析:从原理到常见坑 简介面向重庆大学操作系统课程实验二线程的创建的完整实验工程适合计算机专业本科生、操作系统课程学习者以及需要完成线程相关实验的读者。包内以 epos2-master 工程为主共 77 个文件以 C 源代码和头文件为主辅以汇编、Makefile 构建脚本、说明文档等压缩包整体约 275KB结构上划分出 kernel、lib、userapp 等模块便于按内核实现、库函数和用户程序对照阅读。核心内容围绕线程创建与生命周期管理展开涵盖线程属性初始化、创建、执行、暂停、恢复和终止等状态转换并针对同步互斥给出互斥锁、条件变量、信号量等机制的代码实现同时体现线程池在任务复用中的效率优势。已有 50 人学习下载可作为重庆大学该实验的参考实现也能帮助读者将线程理论落实到具体代码层加深对多线程编程和操作系统调度原理的理解。 操作系统实验二线程的创建看到这个标题的你多半正在操作系统课的实验周里挣扎。这个实验说不上难但确实容易把人绕晕——尤其是第一次接触 pthread光是弄懂传入回调函数的void *参数就够喝一壶了。我当年做这个实验的时候从编译报错到段错误基本把所有经典坑都踩了一遍所以这篇就把整个实验的思路、代码、编译命令和问题排查一次性聊透。内容以 Linux 下的 pthread 为主线适合正在做线程创建类实验的同学参考也适合想搞明白多线程底层逻辑的读者。1. 实验前必须搞清的概念线程和进程到底差在哪儿1.1 这个实验到底想让你学会什么先别急着写代码想清楚实验目的比什么都重要。操作系统课程里进程是“资源分配的基本单位”线程是“CPU 调度的基本单位”这句话背起来容易理解起来抽象。实验二安排线程创建本质上是想让你亲手创建几条“执行流”亲眼看到多个线程同时跑起来的时候CPU 是怎么在它们之间来回切换的。很多学校的操作系统实验包括重庆大学的这套实验前面一个实验刚做完进程创建fork 那一套紧接着就上线程创建。这一步的关键不是让你学会调 API而是帮你建立一种思维转变进程之间地址空间相互隔离通信要用管道、消息队列、共享内存而同一个进程里的线程呢它们共享代码段、数据段、堆、打开的文件描述符每一个线程只拥有自己独立的栈、寄存器上下文和线程 ID。这个共享与独立的分寸必须在实验中反复体会。搞懂了这一层你后面做线程同步互斥锁、条件变量、线程池才会真正有感觉。如果只是对着模板把代码抄一遍跑通那实验白做了。1.2 一张表看懂进程和线程的对比我习惯用一个类比来理解进程就像一家独立的公司有自己的办公室地址空间、资金资源、员工线程线程就是公司里的员工大家共享一间办公室但每个人有自己的工位栈和手头的工作状态寄存器。公司想开会直接在会议室喊一嗓子就能召集员工但如果是跨公司沟通就得发邮件、约时间麻烦得多。具体差异表现在下面这张表上对比项进程线程地址空间各自独立同进程内共享系统开销创建、切换成本高创建、切换成本低通信方式管道、消息队列、共享内存等直接读写共享变量独立性一个进程崩溃通常不影响其他进程一个线程崩溃可能导致整个进程退出调度单位早期系统以进程为调度单位现代系统以线程为调度单位这张表对应的代码表现很直观fork 出来的子进程里你改了一个全局变量父进程完全无感但线程里改一个全局变量所有线程都能看到。实验里你可以用这段差异来验证“线程共享数据”的特性这也是后面所有并发 bug 的源头。2. 实验环境与核心接口Linux pthread 怎么配2.1 环境搭建虚拟机、WSL 还是云主机做线程创建实验需要一个 Linux 环境。如果实验室已经提供了 Ubuntu 之类的虚拟机直接接着用就行。如果是自己电脑上做推荐用 WSLWindows Subsystem for Linux装起来快、系统资源占用小也有同学直接装 VMware 虚拟机都行。云服务器也可以把代码传上去编译运行但调试体验不如本地顺畅。装好系统之后打开终端确认三件事# 1. 确认系统是 Linux uname -a # 2. 确认 gcc 已安装 gcc --version # 3. 确认 pthread 库可用一般 glibc 自带不用单独装 ldconfig -p | grep pthread如果gcc提示找不到执行一下sudo apt-get install build-essentialUbuntu/Debian 系或者sudo yum install gcc glibc-develCentOS/RHEL 系。这一步别嫌啰嗦实验课上至少三分之一的问题都出在环境没齐。2.2 pthread_create 拆开讲每个参数都不是白给的线程创建的接口叫pthread_create是 POSIX 线程库的核心函数。原型长这样#include pthread.h int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void *), void *restrict arg);逐个参数说thread一个pthread_t类型的指针函数调用成功后会把新线程的 ID 写进去。之后pthread_join回收线程时要靠这个 ID。attr线程属性一般传NULL表示使用默认属性。想设置栈大小、调度策略、分离状态时才会单独构造属性对象。start_routine线程入口函数也就是新线程启动后要执行的函数。注意它的类型是void *(*)(void *)接收一个void *参数返回一个void *结果。这也是为什么你写的线程函数必须长成void *func(void *arg)这种形式。arg传给线程函数的参数。因为类型是void *你可以传整数、结构体指针甚至传NULL。但这里面坑不少后面专门讲。返回值是一个错误码成功返回 0失败返回非 0 的错误码比如EAGAIN表示资源不够EINVAL表示属性非法。注意它不像 fork 那样返回两个值所以判断失败要靠返回值而不是判断thread有没有被写入。除了创建线程这个实验里必然用到pthread_join和pthread_exit。pthread_join(tid, retval)的作用是阻塞等待某个线程结束并回收它的资源pthread_exit(void *retval)则是在线程内部主动结束。为什么不推荐在 main 里直接return因为 main 返回就等于进程退出不管其他线程是否执行完整个进程就没了。这一点在问题排查部分会展开。2.3 编译命令-lpthread 到底在干什么很多同学第一次编译就摔在链接上写好的代码 gcc 一编报错undefined reference to pthread_create。原因很简单pthread 是一个独立的库gcc 默认只链接 libc不会自动链接 libpthread。你得显式告诉编译器gcc -o thread_demo thread_demo.c -lpthread注意-lpthread要放在源文件后面。如果放在前面某些版本的链接器会因为链接顺序问题仍然报错。这个编译命令看起来不起眼但实验报告里多写一句注释老师会觉得你是真懂。另外提醒一下从 glibc 2.34 开始libpthread 的函数已经合并进 libc新版本系统上不写-lpthread也能编过。但为了兼容性和严谨建议统一加上养成好习惯。3. 完整实操从创建第一个线程到传参不翻车3.1 最小可运行版本先让一个线程跑起来我建议你的第一个程序不要花里胡哨就做一件事主线程创建一个子线程子线程循环打印几条消息主线程等它结束。代码写出来大概是这样#include stdio.h #include pthread.h void *thread_func(void *arg) { for (int i 0; i 5; i) { printf(子线程正在运行第 %d 次\n, i); } return NULL; } int main() { pthread_t tid; int ret pthread_create(tid, NULL, thread_func, NULL); if (ret ! 0) { fprintf(stderr, 线程创建失败错误码: %d\n, ret); return 1; } pthread_join(tid, NULL); printf(主线程结束\n); return 0; }编译命令gcc -o thread_demo thread_demo.c -lpthread ./thread_demo运行结果大致是子线程正在运行第 0 次 子线程正在运行第 1 次 子线程正在运行第 2 次 子线程正在运行第 3 次 子线程正在运行第 4 次 主线程结束如果你把pthread_join注释掉再运行一次会看到主线程可能先打印完就退出了子线程的输出只有一部分甚至完全没打印出来。这个现象非常关键main 结束意味着整个进程退出所有未执行完的子线程会被直接终止。这也解释了为什么pthread_join在多线程程序里几乎是必需的。3.2 给线程传参数三种常见姿势和两个大坑实验要求里通常不只是创建线程还要让每个线程处理不同数据。我第一次做的时候用了一个最直观的写法结果输出全是一样的当时还以为系统坏了。先看错误版本#include stdio.h #include pthread.h void *print_num(void *arg) { int num *(int *)arg; printf(线程收到: %d\n, num); return NULL; } int main() { pthread_t tids[5]; for (int i 0; i 5; i) { pthread_create(tids[i], NULL, print_num, i); } for (int i 0; i 5; i) { pthread_join(tids[i], NULL); } return 0; }问题出在哪循环变量i是一个局部变量每次循环创建线程时传进去的都是i这个地址。但线程不一定立即执行当线程真正去读*(int *)arg的时候for 循环可能已经跑完了i的值已经变成 5。于是所有线程打印的都是 5甚至会出现离谱的随机值。解决办法有两种主流姿势。第一种直接传值用intptr_t做中间转换#include stdio.h #include pthread.h #include stdint.h void *print_num(void *arg) { int num (int)(intptr_t)arg; printf(线程收到: %d\n, num); return NULL; } int main() { pthread_t tids[5]; for (int i 0; i 5; i) { pthread_create(tids[i], NULL, print_num, (void *)(intptr_t)i); } for (int i 0; i 5; i) { pthread_join(tids[i], NULL); } return 0; }这里把整数值强转成void *再在函数里转回来。注意整数和指针之间的转换要用intptr_t或者uintptr_t保证在 64 位系统上不会出问题。这种写法适合参数只是一个整数的情况。第二种传递结构体指针适合参数多于一个的场景#include stdio.h #include pthread.h #include stdlib.h #include string.h typedef struct { int id; char name[32]; } task_t; void *worker(void *arg) { task_t *t (task_t *)arg; printf(线程 %d 正在处理 %s\n, t-id, t-name); free(t); return NULL; } int main() { pthread_t tids[3]; for (int i 0; i 3; i) { task_t *t malloc(sizeof(task_t)); t-id i; snprintf(t-name, sizeof(t-name), 任务-%d, i); pthread_create(tids[i], NULL, worker, t); } for (int i 0; i 3; i) { pthread_join(tids[i], NULL); } return 0; }注意两点结构体必须用malloc在堆上分配不能传栈上局部变量的地址free要在线程函数里做或者等pthread_join返回后由主线程统一释放但千万别在pthread_create之后立刻free否则线程还没拿到数据内存已经被回收了。3.3 线程的两种归宿join 还是 detach前面反复提到pthread_join那有没有不 join 的合法用法有就是分离线程detached thread。当你的线程创建之后就不想让主线程拿它的返回值、也不想知道它什么时候结束可以调用pthread_detach让线程自己清理资源pthread_t tid; pthread_create(tid, NULL, thread_func, NULL); pthread_detach(tid);还有一种方式创建线程时通过属性设置为分离状态pthread_attr_t attr; pthread_attr_init(attr); pthread_attr_setdetachstate(attr, PTHREAD_CREATE_DETACHED); pthread_create(tid, attr, thread_func, NULL); pthread_attr_destroy(attr);那么问题来了一个已经 join 的线程还能 detach 吗不能二者只能选一种。实验里如果只是简单验证线程创建建议统一用pthread_join因为可以确认线程确实执行完了输出结果也好对照。如果写了pthread_detach记得在主线程加一个延时或者用其他机制保证进程不会提前退出。4. 实验中最容易踩的坑问题与排查实录4.1 编译报错 undefined reference to pthread_create这个错误我只能说十个做线程实验的人至少五个会遇到。报错长这样/tmp/ccXXXXXX.o: In function main: thread_demo.c:(.text0x2f): undefined reference to pthread_create collect2: error: ld returned 1 exit status原因就是前面说的没有链接 pthread 库。解决方案是把编译命令改成gcc -o thread_demo thread_demo.c -lpthread如果用的 CMake在 CMakeLists.txt 里加一句find_package(Threads REQUIRED) target_link_libraries(thread_demo PRIVATE Threads::Threads)这个报错本身不复杂但要注意链接顺序-lpthread放在源文件之后碰到一些旧版工具链时更稳妥。4.2 子线程还没打印完程序就结束了现象是明明创建了线程控制台只输出了主线程的内容子线程的打印一条都没有或者偶尔打了一两条程序就退出了。原因几乎都是 main 函数先执行完并返回了。main 是进程的入口main 一旦 return操作系统会结束整个进程所有还活着的线程都会被强制终止。解决方式有两个要么用pthread_join等待所有线程结束要么用pthread_exit(NULL)让主线程自己结束同时保持进程一直存活直到所有非分离线程也执行完毕。这里有个容易忽略的细节即使你在最后调用printf刷新输出也不代表线程执行完了因为线程调度是独立于主线程的主线程只是创建了子线程这个“任务”不代表任务已经完成。多线程程序里“主线程退出”不等于“程序逻辑完成”这个观念得尽早建立。4.3 传参数后所有线程打印同一个值上一节代码里已经演示过错误写法。我再强调一遍本质pthread_create传递的是arg这个指针的值而不是指针指向的内容。如果你传入的是循环变量i每个线程拿到的虽然是不同时刻的同一个栈地址但最后读到的是同一个内存位置里最新的值。遇到这种输出重复的现象先看代码是不是把局部变量的地址传给了多个线程。如果是改成值传递或堆分配。另外补充一个技巧调试线程参数时可以在线程函数开头先把参数打印出来如果发现所有线程打印的地址都一样那基本就是共享栈变量的问题不需要再往下查了。4.4 创建大量线程时报 Resource temporarily unavailable有同学实验做到后面想试试创建几百上千个线程结果pthread_create返回了错误码 11EAGAIN提示资源不够。这通常有两种原因一是进程可创建的线程/进程数受到限制二是系统内存不足以分配那么多线程栈默认线程栈大小是 8MB 左右创建 1000 个线程理论上预留 8GB 虚拟内存。先看系统限制# 查看当前用户可创建的最大线程/进程数 ulimit -u # 查看系统全局线程数上限 cat /proc/sys/kernel/threads-max如果是开发机临时把 ulimit 调大一点ulimit -u 4096另外也可以用pthread_attr_setstacksize把线程栈调小再创建大线程pthread_attr_t attr; pthread_attr_init(attr); pthread_attr_setstacksize(attr, 1024 * 1024); // 1MB 栈不过从实验学习的角度我不建议一开始就粗暴调大限制。你更应该想的是是不是自己的设计有问题真的需要这么多线程线程不是越多越好创建和切换都有成本。这也是为什么工业级代码里普遍用线程池而不是每来一个任务就新建一个线程——线程池的创建本来就是线程创建实验的自然延伸。4.5 内存问题段错误和泄漏线程实验里的段错误最常见的一个来源就是线程函数内部访问了已失效的栈内存。传局部变量地址这种就不说了还有一种情况是主线程在 join 之前就把结构体内的指针给free了子线程再去访问就成了悬空指针。排查这类问题用valgrind很方便valgrind --toolmemcheck ./thread_demo它能告诉你哪一行访问了非法地址哪块内存没有被释放。多线程程序里还有一个坑数据竞争不会导致崩溃但会导致输出结果不确定。比如多个线程同时用printf打印字符可能交织在一起。这是线程共享标准输出流导致的后面学互斥锁时会专门处理但实验报告里最好能主动提一句你观察到了这种乱序说明你真的运行过。最后说点实验之外的体会我做完这个实验最大的感受是多线程编程不是“代码跑起来”就算会了而是要时刻清楚每个线程的生命周期、参数的内存归属、以及线程什么时候在并发执行、什么时候其实是被锁住的。建议你在交实验前把上面几个例子的代码都编译运行一遍记录输出试着解释为什么输出顺序是不确定的然后把编译命令、运行结果、遇到的问题和解决过程写进报告。这些真实的过程比把代码抄一遍有用得多。如果你学有余力可以沿着这个实验继续往下扩展试着给线程函数传一个任务队列然后固定创建 4 个线程去共同处理这就是线程池的雏形了。你很快会发现光有创建线程还不够线程调度、任务分发、结果回收每一个环节都是一堆新问题。这也是“线程的创建”这个实验最有魅力的地方——它是你进入并发世界的第一扇门推开之后后面的路长着呢。本文还有配套的精品资源点击获取