线程不是鬼,是厨房里的小帮手

开篇除恐

当你第一次听到“线程”这个词,脑子里可能冒出一堆符号:pthread, std::thread, synchronized……好像只有写过底层服务器的大牛才能触碰的禁区。其实,线程并不是一些深奥的咒语,它只是操作系统里最基本的“干活的小帮手”。只要把它想象成厨房里那个随时可以切菜、炒菜、洗碗的小厨师,你就会发现,线程没什么可怕的——它只是帮你把任务分开来做,而不是一次只能做一件事。

白话化

  • 进程(Process):就像一家完整的餐厅,有自己的门面、厨房、库房和员工名单。每个进程都有独立的内存空间,彼此互不干扰。
  • 线程(Thread):则是餐厅里的厨师。他们都在同一间厨房里工作,可以共用同一套锅碗瓢盆(即共享内存),但每人有自己的任务清单和做事步骤。

换句话说,线程是进程内部可以并行执行的最小单元。它们像厨师一样,可以同时切菜、炒菜、洗碗,只要厨房(内存)够大、调度(操作系统)够公平。

直觉先行

想象你一个人在家做晚饭:先切菜,切完才能炒菜;炒完才能装盘;最后才能洗碗。这个过程是串行的——一步接一步。

现在假设你有两个熟练的帮手(线程): - 帮手 A 负责切菜和洗碗; - 帮手 B 负责炒菜和装盘。

只要厨房里有足够的案板和炉灶,他们就可以交叉进行:A 切菜时 B 可以先准备调料;A 洗碗时 B 可以炒菜。整顿饭的时间就被拉伸了,但总体完成得更快——这就是线程带来的并发优势。

在操作系统里,CPU 在极短的时间片里轮流让每个线程执行一会儿(时间片轮转),从宏观上看就像它们“同时”在工作。

例子贴身

例子 1(☼ 热身):打印两行问候

任务:让主线程打印 “Hello from main”,另一个线程打印 “Hello from thread”。

怎么想到的:我们只需要创建一个新线程,在它的入口函数里写一条打印语句,然后让主线程也打印一条。这样两段代码就能独立运行。

示例代码(Python)

import threading

def thread_func():
    print("Hello from thread")

def main():
    print("Hello from main")
    t = threading.Thread(target=thread_func)
    t.start()   # 创建线程并开始执行 thread_func
    t.join()    # 等待线程结束,否则主线程可能先退出

if __name__ == "__main__":
    main()

要点
- threading.Thread(target=thread_func) 这一行其实是“雇佣”了一个新厨师(线程),并让他从 thread_func 开始工作。
- t.join() 是主线程说:“我先等你把活儿干完,我才去收摊。”如果不加这一句,主线程可能提前结束,导致新线程还没来得及打印就被强制停下。

例子 2(☼☼ 正经):累加计数器(演示共享变量)

任务:让 10 条线程各自对同一个全局变量 counter 加 1,共加 100 次,最后打印结果。

怎么想到的:我们首先定义一个全局整型变量,每条线程在循环里对它执行 counter += 1 操作。这样所有线程都在修改同一块内存——这正是线程“共享厨房”的体现。随后我们用 join 把所有线程拉回主线程,最后读取计数器的值。

示例代码(Python)

import threading

counter = 0                     # 共享的厨房案板

def worker():
    global counter
    for _ in range(10):         # 每人加 10 次
        counter += 1            # 对共享变量操作

def main():
    threads = []
    for _ in range(10):         # 雇佣 10 个帮手
        t = threading.Thread(target=worker)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()                # 等大家都忙完
    print(f"Final counter = {counter}")

if __name__ == "__main__":
    main()

注意:运行多次你可能会发现结果不总是恰好等于 100——这就是后面要讲的竞态条件(race condition)的初步表现。这里我们先把重点放在“线程怎么创建、怎么共享数据”上,后面章节会专门解释为什么会出现不准确的结果以及怎样避免。

收尾

这一章要带走的东西
- 线程是进程内可以并行执行的最小执行单元,类比厨房里的厨师。
- 创建线程后,它会独立执行自己的入口函数;主线程可以用 join 等它结束。
- 同一进程的所有线程共享同一份内存(厨房里的锅碗瓢盆),因而可以通过全局变量或指针进行通信。
- 虽然看起来是“同时”工作,实际上是操作系统用很短的时间片让它们交替执行。

就这样。 下一章我们把“进程”和“线程”的关系说得更清楚:为什么说进程是餐厅,线程是厨师?