一、多线程基础概念

各位同学,今天我们正式进入多线程的世界。说实话,多线程编程是C语言系统编程里最考验功底的一块。我见过太多项目,功能都写好了,一上多线程就崩得稀里哗啦。所以这一章,咱们先把地基打牢。

1.1 进程与线程的区别

先问一个问题:你写了一个程序,双击运行,操作系统里发生了什么?

它会创建一个进程。进程是资源分配的最小单位,它有自己的地址空间、文件描述符、信号处理器等等。说白了,进程就像一个独立的工厂,有自己的厂房和设备。

线程呢?它是CPU调度的最小单位。一个进程里可以跑多个线程,它们共享进程的资源——内存、文件、信号。还是那个工厂的比喻:线程就是工厂里的工人,共用厂房和设备,但每个人干自己的活。

核心区别一句话:进程是资源容器,线程是执行单元。

我在项目中遇到过一个问题:一个服务进程开了10个线程处理请求,结果某个线程不小心把全局指针给free了,其他线程全崩了。这就是共享内存带来的风险——后面讲线程安全时我们再细聊。

对比项 进程 线程
地址空间 独立 共享
资源开销 大(创建、切换都重) 小(轻量级)
通信方式 IPC(管道、消息队列等) 直接读写共享内存
健壮性 一个进程挂了不影响其他 一个线程崩了,整个进程完蛋

1.2 并发与并行的区别

这两个词,我面试过的人十个有八个搞混。其实很简单:

  • 并发(Concurrency):多个任务在同一个时间段内交替执行。单核CPU也能做到,靠时间片轮转。
  • 并行(Parallelism):多个任务在同一个时刻同时执行。必须多核CPU才能做到。

你想想看,一个厨师同时做三道菜,他一会儿切菜,一会儿炒菜,一会儿装盘——这是并发。三个厨师各做一道菜——这是并行。

我的经验:写多线程程序时,别以为开了线程就一定能并行。如果机器只有单核,你的多线程其实是在并发,性能提升有限。我曾经在一个嵌入式项目里踩过这个坑,开了4个线程处理数据,结果发现CPU只有一个核,反而因为上下文切换变慢了。

1.3 线程的生命周期

一个线程从创建到销毁,会经历几个状态。嗯,这里要注意,不同操作系统对线程状态的叫法略有不同,但核心逻辑是一样的:

  1. 新建(New):线程对象刚创建,还没开始跑。
  2. 就绪(Runnable):线程已经可以运行,等待CPU分配时间片。
  3. 运行(Running):CPU正在执行这个线程的代码。
  4. 阻塞(Blocked):线程在等某个资源(比如锁、I/O操作),暂时不参与CPU调度。
  5. 终止(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分配堆内存,或者用全局变量。

最后,我用一张图来总结本章的知识结构:

多线程基础概念知识体系 进程 vs 线程 • 进程:资源分配最小单位,独立地址空间 • 线程:CPU调度最小单位,共享进程资源 • 线程创建/切换开销远小于进程 • 线程间通信更简单,但风险更高 并发 vs 并行 • 并发:交替执行,单核也能实现 • 并行:同时执行,必须多核 • 多线程 ≠ 并行,看硬件 • 上下文切换有开销,注意权衡 线程生命周期 • 新建 → 就绪 → 运行 → 阻塞 → 终止 • 阻塞原因:等待锁、I/O、条件变量 • 终止后必须 join 回收资源 • 死锁会导致线程永久阻塞 C语言线程模型 • POSIX线程(pthread)是事实标准 • 三大类:管理、同步、局部存储 • 线程函数签名:void* (*)(void*) • 注意参数生命周期,避免野指针 核心:理解资源隔离与共享的边界,是写好并发程序的第一步

好了,这一章的内容就是这些。记住:进程和线程的区别是基础中的基础,并发和并行的概念决定了你写代码时的思维方式,线程生命周期帮你理解程序运行时发生了什么,而pthread模型则是你动手实践的工具。下一章我们开始真正写代码,创建和管理线程。


公众号:蓝海资料掘金营,微信deep3321