一、多线程基础概念
各位同学,今天我们正式进入多线程的世界。说实话,多线程编程是C语言系统编程里最考验功底的一块。我见过太多项目,功能都写好了,一上多线程就崩得稀里哗啦。所以这一章,咱们先把地基打牢。
1.1 进程与线程的区别
先问一个问题:你写了一个程序,双击运行,操作系统里发生了什么?
它会创建一个进程。进程是资源分配的最小单位,它有自己的地址空间、文件描述符、信号处理器等等。说白了,进程就像一个独立的工厂,有自己的厂房和设备。
而线程呢?它是CPU调度的最小单位。一个进程里可以跑多个线程,它们共享进程的资源——内存、文件、信号。还是那个工厂的比喻:线程就是工厂里的工人,共用厂房和设备,但每个人干自己的活。
核心区别一句话:进程是资源容器,线程是执行单元。
我在项目中遇到过一个问题:一个服务进程开了10个线程处理请求,结果某个线程不小心把全局指针给free了,其他线程全崩了。这就是共享内存带来的风险——后面讲线程安全时我们再细聊。
| 对比项 | 进程 | 线程 |
|---|---|---|
| 地址空间 | 独立 | 共享 |
| 资源开销 | 大(创建、切换都重) | 小(轻量级) |
| 通信方式 | IPC(管道、消息队列等) | 直接读写共享内存 |
| 健壮性 | 一个进程挂了不影响其他 | 一个线程崩了,整个进程完蛋 |
1.2 并发与并行的区别
这两个词,我面试过的人十个有八个搞混。其实很简单:
- 并发(Concurrency):多个任务在同一个时间段内交替执行。单核CPU也能做到,靠时间片轮转。
- 并行(Parallelism):多个任务在同一个时刻同时执行。必须多核CPU才能做到。
你想想看,一个厨师同时做三道菜,他一会儿切菜,一会儿炒菜,一会儿装盘——这是并发。三个厨师各做一道菜——这是并行。
我的经验:写多线程程序时,别以为开了线程就一定能并行。如果机器只有单核,你的多线程其实是在并发,性能提升有限。我曾经在一个嵌入式项目里踩过这个坑,开了4个线程处理数据,结果发现CPU只有一个核,反而因为上下文切换变慢了。
1.3 线程的生命周期
一个线程从创建到销毁,会经历几个状态。嗯,这里要注意,不同操作系统对线程状态的叫法略有不同,但核心逻辑是一样的:
- 新建(New):线程对象刚创建,还没开始跑。
- 就绪(Runnable):线程已经可以运行,等待CPU分配时间片。
- 运行(Running):CPU正在执行这个线程的代码。
- 阻塞(Blocked):线程在等某个资源(比如锁、I/O操作),暂时不参与CPU调度。
- 终止(Terminated):线程执行完毕,或者被强制结束。
为什么会阻塞?最常见的情况是:线程A想拿一个锁,但锁被线程B占着,A就只能等着。我调试过一个死锁问题,两个线程互相等对方释放锁,结果双双卡死在阻塞状态——这个后面讲锁的时候会重点说。
注意:线程终止后,它的资源不会自动释放。你必须调用pthread_join()来回收,否则会造成资源泄漏。我曾经见过一个线上服务,跑了三天后突然OOM,查了半天发现是线程没有join,堆了一堆僵尸线程。
1.4 C语言中的线程模型
C语言本身没有内置多线程支持,我们用的是POSIX线程库——也就是pthread。它定义了一套标准的API,Linux、macOS、各种Unix系统都支持。Windows上也有pthread的移植版。
我个人习惯把pthread的用法分成三类:
- 线程管理:创建、等待、分离、退出
- 同步机制:互斥锁、条件变量、读写锁、信号量
- 线程局部存储:每个线程独有的数据
来看一个最简单的例子:
#include <pthread.h>
#include <stdio.h>
void* worker(void* arg) {
int id = *(int*)arg;
printf("线程 %d 开始干活\n", id);
return NULL;
}
int main() {
pthread_t t1, t2;
int id1 = 1, id2 = 2;
pthread_create(&t1, NULL, worker, &id1);
pthread_create(&t2, NULL, worker, &id2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("两个线程都干完了\n");
return 0;
}
这段代码里,pthread_create()创建线程,pthread_join()等待线程结束。注意worker函数的签名——它必须返回void*,参数也必须是void*。这是pthread的约定,别搞错了。
避坑指南:传给线程的参数,一定要保证在线程使用它时仍然有效。我曾经把局部变量的地址传给线程,结果主线程先退出了,局部变量被销毁,子线程读到了一个野指针。正确的做法是用malloc分配堆内存,或者用全局变量。
最后,我用一张图来总结本章的知识结构:
好了,这一章的内容就是这些。记住:进程和线程的区别是基础中的基础,并发和并行的概念决定了你写代码时的思维方式,线程生命周期帮你理解程序运行时发生了什么,而pthread模型则是你动手实践的工具。下一章我们开始真正写代码,创建和管理线程。