创建你的第一个线程:Hello来自另一个厨师

开篇除恐

“写一个多线程程序”听起来像是要先读完一本操作系统教程,才敢碰 threading.Thread 或类似的 API。其实,创建线程就像雇佣一个临时帮工:你只要告诉它“去干什么”(即提供一个函数),然后把它“放进厨房”(即启动它),其余的细节(比如怎么分配栈、怎么保存寄存器)都由语言库和操作系统帮你处理。只要记住这两步——定义任务函数 + 启动线程,你就能写出第一个多线程程序,之後再慢慢学习怎么等它结束、怎么共享数据等等。

白话化

  • 任务函数(Thread Function):线程启动后会从这里开始执行,就像给新厨师下达的第一道指令:“去切这些菜。”这个函数可以没有参数、可以有参数、可以返回值(在某些实现中返回值被忽略),重点是它里面写的是线程要干的活。
  • 线程句柄(Thread Handle):创建线程时返回的对象(如 threading.Thread 实例),用来之后对线程进行操作:等它结束(join)、分离它(daemon 线程的概念类似 detach,但 Python 没有真正的 detach,而是可以设置守护线程或让线程自然结束),查询它的 ID 等。把它想象成你手上的临时工牌子——有了它才能找到并管理这个帮工。

在这两个概念之上,其他的 API(比如互斥锁、条件变量)都是围绕“怎样让多个线程安全地合作”而来;但最基本的“创建并运行”只要这两步就够了

直觉先行

想象你有一个菜谱,里面有一步是“把洋葱切成丁”。你可以自己动手,也可以喊一个临时帮工:“你去把洋葱切成丁,切完喊我。”如果你自己动手,那就得切完才能做下一步;如果你喊帮工,你就在帮工切洋葱的时候可以去做其他事情(比如预热锅子),等帮工喊你时再继续后面的步骤。这就像主线程把任务交给子线程后,可以继续自己做别的事情,或者选择等子线程完成后再继续

在编码里,这对应于: - 不等待(守护线程或不 join):让线程自行运行,主线程继续往后执行,不关心线程何时结束(只要确保线程不会访问已经被销毁的资源)。 - 等待(join):主线程在这里暂停,直到子线程执行完任务函数并返回,然后才继续向下执行。

例子贴身

例子 1(☼ 热身):用 threading.Thread 打印来自另一个线程的问候

任务:主线程打印 “Hello from main”,子线程打印 “Hello from thread”,然后主线程等子线程结束后再退出。

怎么想到的:我们定义一个无参数的函数 thread_hello,里面只输出一句问候。在 main 中创建 threading.Thread 对象并把该函数作为 target 传进去,这就是启动线程。随后调用 join() 让主线程等待子线程完成。

示例代码(Python)

import threading

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

def main():
    print("Hello from main")
    t = threading.Thread(target=thread_hello)  # 启动线程,立即开始执行 thread_hello
    t.start()                                # 必须调用 start() 才开始运行
    t.join()                                 # 等线程结束,防止主线程提前退出导致子线程被杀
    # 注意:在 Python 中,必须调用 start(),构造仅创建对象

if __name__ == "__main__":
    main()

运行结果(可能的顺序)

Hello from main
Hello from thread

Hello from thread
Hello from main

要点
- threading.Thread(target=thread_hello) 创建线程对象;t.start() 完成了“创建并启动”。start() 返回后,线程已经在后台运行。
- t.join() 是主线程说:“我在这儿等你把活儿干完,才继续走。”如果不加 join,主线程可能在子线程还没来得及打印就结束进程,导致子线程被强制终止(输出可能丢失)。

例子 2(☼☼ 正经):带参数的线程函数(模拟厨师报告切菜进度)

任务:让子线程接收一个整数参数 n,模拟切 n 次菜,每次切完后打印一声 “切了一刀”,切完所有后返回;主线程等待结束并打印 “所有菜都切好了”。

怎么想到的:我们把要切的次数作为参数传入线程函数。在 Python 中可以直接给函数加参数并用 threading.Thread 的构造函数传递 args(需要注意参数的生命周期)。这里我们用值传递(参数拷贝)最直观。

示例代码(Python)

import threading
import time

def cut_vegetables(times):
    for i in range(1, times + 1):
        # 模拟切菜需要一点时间
        time.sleep(0.05)  # 50 ms
        print(f"切了一刀 ({i}/{times})")

def main():
    num_cuts = 5
    print(f"开始切菜,准备切 {num_cuts} 次")
    t = threading.Thread(target=cut_vegetables, args=(num_cuts,))  # 把函数和参数一起传给线程构造函数
    t.start()                                            # 开始线程
    t.join()                                             # 等切完所有菜

    print("所有菜都切好了")

if __name__ == "__main__":
    main()

运行结果片段(每 50 ms 一行)

开始切菜,准备切 5 次
切了一刀 (1/5)
切了一刀 (2/5)
切了一刀 (3/5)
切了一刀 (4/5)
切了一刀 (5/5)
所有菜都切好了

要点
- 线程函数可以接受参数;在 threading.Thread 构造时,参数会被拷贝(或引用)传入线程的内部存储,因而即使主线程以后改动原变量也不会影响子线程看到的值(除非你显式传引用或可变对象)。
- 这里我们用 time.sleep 仅是为了让输出有时间间隔,方便观察;实际生产代码里往往不需要这种人工等待,而是靠真实工作来消耗时间。

收尾

这一章要带走的东西
- 创建线程的两个基本要素:任务函数(线程要执行的代码)和 线程句柄(用来以后管理线程的对象)。
- 在 Python 中使用 threading.Thread:构造时把函数(及其参数)传进去,调用 start() 启动线程;调用 join() 等待线程结束;若不再需要等待,可设置 daemon=True 让线程在主线程退出时自动结束(但需确保线程不会访问已经释放的资源)。
- 主线程可以在子线程运行时继续做其他工作,或选择在此处等待子线程完成。
- 掌握了这一点,你就能写出最基本的多线程程序;接下来的章节将围绕“线程之间怎样安全地共享数据”展开。

就这样。 下一章我们学习如何让主线程“耐心等待”子线程完成,而不是猜测它什么时候结束——即 join 的细节和超时等待的技巧。