在之前学习嵌入式linux系统编程时,相信很多朋友和我一样,是不是都有这种感觉:明明代码都是这些代码,为什么突然就能够“多线并行”了?比如:一个线程负责采集数据,一个线程负责界面显示,一个线程负责网络通信。程序看起来好像是在同时做很多个任务,背后依赖的就是———“线程机制”。别看它们只有“创建”和“等待”这两个动作,实际上这里面已经包括了线程最关键的一些基础逻辑:谁来创建线程、线程从哪里开始执行、参数怎么传进去、线程结束后怎么拿到结果、主线程为什么要等待。
一、先别急着背函数,先搞懂线程到底是什么?
如果把一个进程理解成“一个正在运行的程序”,那线程就可以理解成: 平时我们写的普通 C / C++程序,默认只有一条执行路线,也就是从main() 开始,一行一行往下走。但有了线程以后,程序就不再只有主线程这一条路了。我可以额外创建新的执行单元,让它和主线程一起工作。
二、一个完整线程的基本流程
主线程先准备好线程 ID、参数和返回值变量主线程调用 pthread_create 创建一个子线程子线程从指定函数 thread_func 开始执行子线程拿到主线程传入的参数,循环打印 5 次子线程执行结束后返回一个结果值主线程通过 pthread_join 等待子线程结束主线程拿到线程返回值,并把它打印出来
三、pthread_create:线程是怎么被“拉起来”的?
先看线程创建这个动作。在单线程程序里,函数只有被当前执行流主动调用时才会运行。而在多线程程序里,pthread_create 的作用就是:告诉系统,再开一条新的执行流,让它从某个函数开始跑。它的本质不是“调用一个函数”,而是:你可以把它理解成:主线程像一个项目经理,安排了一个新的工人去干活,这个工人一上岗,就从你指定的那个函数开始工作。void* thread_func(void* arg)也就是说,线程一旦创建成功,子线程就会从 thread_func 开始执行,而不是从 main 开始。
四、为什么线程函数长这样:void* thread_func(void* arg)
其实这是 POSIX 线程库为了通用性做的统一设计。因为线程库没法提前知道我们想传什么数据。比如我们有时候传一个整数,有时候传一个结构体,有时候传一个字符串。参数先用通用指针void*接收,具体是什么类型,由我们自己在线程函数里转换。原因类似,线程结束时,线程库也不知道我们想返回什么类型,所以统一用 void* 表示。意思就是线程执行结束时,返回一个值 666,主线程再通过 pthread_join 把这个值取回来。所以这里的设计重点不是“void* 很奇怪”,而是:线程接口故意做成通用格式,让你能传各种参数、拿各种结果。
五、主线程传进去的到底是什么?
这部分特别值得展开,因为这是很多人写线程最容易犯错的地方。int arg = 1;pthread_create(&tid, NULL, thread_func, &arg);
这里传给线程的不是 1,而是 arg 的地址,也就是 &arg。这意味着子线程拿到的不是一个独立的整数副本,而是主线程变量的内存地址。因为线程函数参数只有一个 void*,不能直接传普通整型值,所以一般要把数据放到某个内存位置,再把地址传过去。但这也正好提醒读者一个非常重要的线程传参原则:传给线程的参数,在线程使用期间必须保证有效。如果传的是一个临时变量地址,或者循环里反复复用同一个变量地址,就很容易出问题。
六、子线程到底是怎么运行起来的?
线程创建成功后,子线程就开始执行 thread_func。线程一开始,先拿到主线程传来的上下文信息。这一步在实际项目里特别常见。比如:传设备号、传任务编号、传配置结构体、传资源句柄实际项目里,比如:读取传感器、接收网络数据、播放音频、刷新界面、写日志这说明线程本身也是有“出生—运行—结束”完整过程的,不是一个抽象概念。
七、pthread_join:为什么主线程必须等它?
pthread_join 其实做了两件非常重要的事:也就是说,主线程执行到这里以后,不会继续往下走,而是停下来等子线程跑完。线程虽然执行完了,但它占用的某些系统资源并不会自动马上清理。pthread_join 的另一个作用,就是把线程“善后”处理掉,并把线程函数的如果你只让线程结束,却不做回收,那么资源管理就可能出问题。pthread_join 不只是“等线程”,还是“接收线程结果并完成回收”的动作。
八、总结线程创建与等待 背后的四个意识
线程不是从 main 开始跑,而是从你指定的线程函数开始跑。假如你传的是地址,线程用的时候就必须保证这块地址有效。很多时候主线程必须等待子线程结束,否则程序流程就不完整。如果你需要结果,或者要规范回收资源,就要通过 pthread_join 处理。
九、把这节内容用一句话讲明白
pthread_create 负责把子线程启动起来,pthread_join 负责等它干完活,并把结果收回来。线程创建解决的是“让谁去做事”,线程等待解决的是“做完以后怎么衔接主流程”。
十、给读者的学习建议:别只看懂,要自己跑一遍
这类知识点最怕“看懂了,但没真懂”。真正有效的学习方式不是只背函数原型,而是自己动手改几个版本:把参数从 1 改成别的值,看看输出怎么变把循环次数改大一点,观察主线程确实会等待把返回值改成别的数字,看看 join 是否能接收到,尝试再创建第二个线程,感受多个线程同时执行时的变化,因为线程这个知识,只有真正看到“主线程创建—子线程执行—主线程等待—回收结果”这个完整过程,脑子里才会形成稳定模型。
结尾
vi thread_create_join.c#include<stdio.h>//用于打印 printf#include<pthread.h>//线程库#include<unistd.h>//sleep()//线程函数 thread_funcvoid* thread_func(void* arg){ //把传进来的参数当成int的地址,然后把里面的参数取出来,存到num int num = *(int*)arg; //循环打印5次 for(int i = 0; i < 5; i++) { printf("Thread %d is running %d\n",num,i); sleep(1); } return (void*)100; }intmain(){ //声明一个线程ID变量 pthread_t tid; //定义一个整型参数,将这个参数传给线程函数 int arg = 1; //定义一个变量,接收线程返回值 void* ret; //&tid 保存线程 NULL 默认线程属性 thread_func 线程执行函数 NULL 线程参数 pthread_create(&tid,NULL,thread_func,&arg); //等待线程tid结束 pthread_join(tid,&ret); //打印线程返回值结果 printf("Thread exit code:%ld\n",(long)ret); return 0;}编译: gcc thread_create_join.c -o thread_create_join -lpthread ./thread_create_join运行结果:Thread 1 is running 0Thread 1 is running 1Thread 1 is running 2Thread 1 is running 3Thread 1 is running 4Thread exit code:100总结:主线程先定义线程 ID、参数和返回值变量。然后主线程创建一个子线程,并把 arg 的地址传给它。子线程拿到参数后循环打印 5 次,每次间隔 1 秒。打印完后返回 100。主线程用 pthread_join 等待子线程结束,并拿到返回值,最后打印出来。