前面一章我们讲了进程。
程序运行起来以后,变成了进程。
但嵌入式 Linux 程序,大多数情况下不会只做一件事。
比如一个设备程序可能要同时处理这些任务:
读取串口数据解析通信协议定时采集传感器保存运行日志监控网络连接响应外部控制命令检测异常并自动恢复
如果所有任务都塞进 main() 里,代码就会变得又长又乱。
怎么解决呢?
线程。
一、为什么进程之后要讲线程
进程解决的是:
一个程序如何在 Linux 系统里运行起来
线程解决的是另一个问题:
一个进程内部如何同时执行多条任务线
一个进程可以只有一个线程。如:
int main(void){ while (1) { /* do something */ } return 0;}
一个进程也可以有多个线程。
比如:
主线程负责初始化和状态管理串口线程负责接收数据网络线程负责上传数据日志线程负责写文件看门狗线程负责周期性检测
这些线程都在同一个进程里,它们共享这个进程的大部分资源,但各自有自己的执行路径。
线程最核心的意义:
线程不是新的程序,而是同一个进程里不同的执行流。
二、线程到底是什么
可以先粗略理解成:
进程是资源容器线程是执行单位
一个进程拥有很多资源:
地址空间打开的 fd当前工作目录环境变量信号处理方式堆内存全局变量
每个线程有自己的:
线程 ID栈寄存器上下文执行位置调度状态
线程不是凭空存在的,它一定属于某个进程。
如果进程退出,里面的线程也会跟着结束。
三、线程和进程有什么区别
进程之间默认是隔离的。
一个进程的全局变量,另一个进程不能直接访问。
但线程不同。
同一个进程里的多个线程共享地址空间。
也就是说:
一个线程改了全局变量,另一个线程马上就可能看到
线程是一个双刃剑。
优势是通信方便。多个线程之间传递数据,不一定需要管道、socket 或共享内存。因为大家本来就在同一个地址空间里。
但如果一个线程把数据改坏了,其他线程可能立刻跟着出问题。
四、pthread_create:创建一个线程
Linux 用户态里最常见的线程接口是 pthread。
如:
#include <pthread.h>#include <stdio.h>#include <unistd.h>static void *worker_thread(void *arg){ (void)arg; while (1) { printf("worker thread running\n"); sleep(1); } return NULL;}int main(void){ pthread_t tid; int ret; ret = pthread_create(&tid, NULL, worker_thread, NULL); if (ret != 0) { printf("pthread_create failed: %d\n", ret); return 1; } while (1) { printf("main thread running\n"); sleep(2); } return 0;}
编译时通常需要链接 pthread:
gcc main.c -o app -pthread
运行后,你会看到两个打印。
一个是主线程。一个是 worker_thread。
注意,pthread_create() 并不是启动一个新进程。它不是重新加载一个可执行文件,而是在当前进程里创建一条新的执行流。
五、mutex:互斥保护
如果多个线程要访问同一个共享资源,最常用的办法是加锁。
pthread 里常用的是 pthread_mutex_t。
示例:
#include <pthread.h>#include <stdio.h>static int g_count = 0;static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;static void *worker_thread(void *arg){ (void)arg; for (int i = 0; i < 100000; i++) { pthread_mutex_lock(&g_lock); g_count++; pthread_mutex_unlock(&g_lock); } return NULL;}int main(void){ pthread_t t1; pthread_t t2; pthread_create(&t1, NULL, worker_thread, NULL); pthread_create(&t2, NULL, worker_thread, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("g_count = %d\n", g_count); return 0;}
锁的意思很简单:
一个线程进入关键区域时,先拿锁其他线程想进入同一段关键区域,必须等待当前线程操作完成后释放锁
这里真正要保护是共享资源 g_count 的一致性。
六、不要把锁当成万能药
加锁能解决一类问题,但也会带来新问题。
最常见的是死锁。
比如线程 A 拿了锁 1,准备拿锁 2。线程 B 拿了锁 2,准备拿锁 1。
于是两个线程互相等待。谁也走不下去。
这就是死锁。
简化一下:
线程 A:拿 lock_a -> 等 lock_b线程 B:拿 lock_b -> 等 lock_a
程序没有崩溃,也没有报错。但就是卡住了。
所以锁要谨慎使用。
几个基本原则:
锁保护的范围尽量小不要拿着锁做耗时 IO多个锁要有固定的加锁顺序加锁和解锁要成对出现出错路径也必须释放锁
尤其要注意:
不要在持有锁的时候调用可能长期阻塞的函数。
七、线程退出与资源回收
线程能跑起来,只说明它开始工作了。线程能不能按预期退出、资源能不能按顺序回收这也很重要。
短命线程最常见的做法,是在线程函数里正常 return,或者显式调用 pthread_exit() 结束当前线程,然后由外部线程用 pthread_join() 等它结束:
return NULL;
或者:
pthread_exit(NULL);
再由别的线程回收:
pthread_join(tid, NULL);
如果一个线程从设计上就不需要别人等待,也不需要关心返回值,可以把它设成 detached:
pthread_detach(tid);
一旦 detach,其他线程后面就不能再 join 它,也没法拿到它的退出结果。所以它只适合那种职责单一、生命周期明确、失败后也不需要统一善后的线程。
线程退出前要处理的,也不只是 return NULL;。它必须把自己持有的资源按顺序交还出去:
释放内存关闭 fd停止定时器刷新日志或提交缓冲数据通知其他线程不要再投递任务恢复设备状态释放锁和同步对象引用
这里很容易出问题,不是漏掉一行 free(),而是回收顺序写反了。比如主线程先把共享上下文释放了,工作线程还在访问;或者 fd 已经关了,另一个线程还在使用。
所以比较稳妥的顺序是:
先停止接收新任务再通知工作线程退出然后等待线程真正结束最后回收这些线程共享的资源
八、总结
第一,线程属于进程。第二,进程是资源容器,线程是执行流。第三,同一进程里的线程共享地址空间、全局变量、堆内存和 fd。第四,每个线程有自己的栈、执行位置和调度状态。第五,共享资源必须考虑同步,否则会出现竞态条件。第六,mutex 用来保护共享状态,但锁用错会导致死锁和卡死。第七,线程创建以后要考虑退出、回收和资源释放。