1. 线程基础与概念

大家好,我是你们这门课的主讲。做了十几年并发编程,从C++到Java再到Python,踩过的坑能绕地球三圈。今天咱们先聊聊最基础的东西——线程到底是什么?它和进程有啥区别?

说实话,很多人学了多年多线程,连这几个基本概念都模模糊糊。我面试过不少候选人,一说线程生命周期就背八股文,一问到上下文切换开销就卡壳。嗯,咱们今天就把这些基础打扎实。

1.1 进程与线程的区别

先问个问题:你打开一个浏览器,再打开一个Word,这是两个什么?对,两个进程。那你在浏览器里开10个标签页呢?这10个标签页,很可能就是10个线程。

进程是资源分配的最小单位。说白了,每个进程有自己的独立内存空间、文件句柄、网络连接。进程之间互相隔离,一个崩了不影响另一个。我当年做服务器开发,最怕的就是进程间通信——又要考虑性能,又要保证数据一致性,烦得很。

线程是CPU调度的最小单位。同一进程里的线程,共享进程的资源。这意味着什么?意味着它们可以访问同一块内存,但也意味着——你改我改大家改,数据就乱了。

核心区别一句话总结:

  • 进程:资源隔离,切换慢,通信麻烦
  • 线程:资源共享,切换快,同步复杂

我习惯用一个比喻来理解:进程就像一栋楼里的不同房间,每个房间有独立的门锁和家具;线程就像房间里的人,共享房间里的空调、冰箱、电视。人多了,就得商量着用,不然容易打架。

对比维度 进程 线程
内存空间 独立 共享
创建开销 大 小
切换开销 大(涉及页表切换) 小(只需保存寄存器)
通信方式 IPC(管道、消息队列等) 直接读写共享内存
健壮性 一个进程崩了不影响其他 一个线程崩了可能导致整个进程挂

1.2 线程的生命周期

线程不是生下来就一直跑,它也有自己的「人生阶段」。Java里把线程生命周期分成了6种状态,但咱们抽象一下,核心就5个:新建、就绪、运行、阻塞、死亡。

你想想看,一个线程被创建出来,它只是有了个「身份」,还没开始干活——这是新建状态。调了start()之后,它进入就绪队列,等着CPU分配时间片。抢到时间片了,开始执行run()里的代码——这是运行状态。运行中如果遇到I/O操作、锁竞争、sleep(),就会进入阻塞状态。等条件满足了,又回到就绪队列。最后run()执行完或者异常退出——死亡。

我的一点经验:很多新手搞混「就绪」和「运行」。记住一句话:就绪是「想跑但没轮到你」,运行是「正在跑」。这两个状态之间的切换,就是上下文切换的核心。

为什么会阻塞?我遇到过最典型的场景:一个线程在等数据库查询结果,这时候CPU如果还死等它,那就是浪费。不如把它挂起,让其他线程先跑。等数据回来了,再把它唤醒。

1.3 线程的上下文切换开销

上下文切换,说白了就是CPU从一个线程切换到另一个线程时,要保存当前线程的状态,再加载下一个线程的状态。这个「状态」包括程序计数器、寄存器、栈指针等等。

你以为切换很快?其实不然。一次上下文切换大概需要几微秒到几十微秒。听起来不多,但如果你的系统每秒切换几万次呢?那CPU时间就全花在「切换」上了,真正干活的时间反而少了。

我曾经踩过的坑:有一次做高并发网关,发现CPU使用率才30%,但吞吐量上不去。查了半天,发现线程数开太多了——200个线程抢8个核,大部分时间都在切换。后来把线程数降到16个,吞吐量直接翻倍。这就是上下文切换的代价。

上下文切换的开销主要包括:

  • 保存和恢复寄存器(几十条指令)
  • TLB(页表缓存)失效,导致内存访问变慢
  • CPU缓存失效,数据要重新加载
  • 操作系统调度器的调度开销

所以,线程不是越多越好。我个人的习惯是:CPU密集型任务,线程数 = CPU核心数 + 1;I/O密集型任务,线程数可以多一些,但也要控制,一般用公式:线程数 = CPU核心数 * (1 + 平均等待时间 / 平均计算时间)。

1.4 Python/C++/Java中的线程模型对比

三种语言我都用过,它们的线程模型差别挺大。咱们一个一个说。

Java线程模型

Java的线程是直接映射到操作系统原生线程的。你new一个Thread,底层就创建一个OS线程。所以Java的线程是「真线程」,能利用多核。但代价是创建和销毁开销大,所以一般用线程池。

// Java创建线程
Thread t = new Thread(() -> {
    System.out.println("Hello from thread");
});
t.start();

C++线程模型

C++11之后有了std::thread,也是原生线程。但C++给了你更多控制权——你可以设置线程的栈大小、调度策略、CPU亲和性。我当年做实时系统,就靠设置线程优先级和CPU绑定来保证关键任务的响应时间。

// C++创建线程
#include <thread>
std::thread t([]() {
    std::cout << "Hello from thread" << std::endl;
});
t.join();

Python线程模型

Python的情况比较特殊。它有GIL(全局解释器锁),同一时刻只有一个线程能执行Python字节码。所以Python的多线程,说白了是「伪并行」——对CPU密集型任务没用,但对I/O密集型任务还是有价值的。

# Python创建线程
import threading
t = threading.Thread(target=lambda: print("Hello from thread"))
t.start()

三种模型对比:

特性 Java C++ Python
线程类型 原生线程 原生线程 原生线程(受GIL限制)
并行能力 真并行 真并行 伪并行(I/O可并行)
创建开销 较大 中等 较小
控制粒度 中等 精细 较粗
适用场景 企业级应用 系统级/实时应用 I/O密集型/脚本

我个人建议:如果你做Web服务、中间件,Java是首选;如果你做游戏引擎、高频交易,C++更合适;如果你做爬虫、Web后端(Django/Flask),Python的线程配合异步IO效果也不错。

嗯,这一章的内容就到这里。基础打牢了,后面讲同步机制、锁、条件变量的时候,你才能理解为什么要有这些东西。

线程基础知识体系 线程基础 与概念 进程 vs 线程 资源隔离 vs 资源共享 线程生命周期 新建→就绪→运行→阻塞→死亡 上下文切换开销 寄存器保存/恢复、缓存失效 语言线程模型对比 Java/C++/Python Java:原生线程 C++:精细控制 Python:GIL限制 核心要点 线程是CPU调度的最小单位,共享进程资源 上下文切换有成本,线程数不是越多越好