2. threading模块入门:创建线程的两种方式、线程的启动与等待、线程的daemon属性

好,咱们正式开始玩线程了。Python里搞多线程,最核心的就是threading模块。说实话,我当年刚接触它的时候,觉得挺简单的——不就是开个线程跑任务嘛。但用着用着才发现,里面有不少门道。今天咱们就把最基础的东西捋清楚。

2.1 创建线程的两种方式

Python里创建线程,说白了就两条路:直接传函数,或者继承Thread类。两种我都用过,各有各的适用场景。

方式一:直接传入目标函数

这是最常用的方式。你定义一个普通函数,然后把它丢给Thread对象就行。我个人习惯用这种方式,因为代码简洁,逻辑清晰。

import threading
import time

def worker(name):
    print(f"线程 {name} 开始工作")
    time.sleep(2)
    print(f"线程 {name} 工作结束")

# 创建线程
t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=worker, args=("B",))

# 启动线程
t1.start()
t2.start()

你看,代码就这么几行。我在项目中处理大量IO任务时,经常用这种模式。比如批量下载文件,每个文件开一个线程,传不同的URL进去,跑起来特别顺畅。

小提示:args参数必须是一个元组。如果只有一个参数,记得加个逗号,比如args=("A",)。我见过不少新手在这栽跟头。

方式二:继承Thread类

这种方式适合你需要在线程里维护一些状态,或者想封装更复杂的逻辑。说白了,就是把线程本身当做一个对象来用。

import threading
import time

class MyWorker(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name
        self.result = None
    
    def run(self):
        print(f"线程 {self.name} 开始工作")
        time.sleep(2)
        self.result = f"来自 {self.name} 的结果"
        print(f"线程 {self.name} 工作结束")

# 创建并启动
t = MyWorker("C")
t.start()
t.join()
print(t.result)  # 获取线程执行结果

嗯,这里要注意:必须重写run方法,而不是start方法。start是父类的方法,它会自动调用run。我曾经见过有人直接重写了start,结果线程根本没跑起来——因为start里本该做的初始化工作全被覆盖了。

核心区别:函数方式适合简单任务,类方式适合需要封装状态和行为的复杂场景。我个人建议:80%的情况用函数方式就够了。

2.2 线程的启动与等待

创建完线程,下一步就是让它跑起来。这里有两个关键操作:start()和join()。

start():让线程真正跑起来

你想想看,创建Thread对象只是准备好了任务,但线程还没开始执行。只有调用了start(),操作系统才会真正分配资源,让线程跑起来。

t = threading.Thread(target=worker, args=("D",))
# 此时线程还没开始
t.start()  # 现在才开始

这里有个坑:一个线程只能start一次。如果你调了两次,Python会直接抛RuntimeError。我在早期项目里踩过这个坑,当时写了个循环,不小心重复启动了同一个线程,排查了半天才发现。

join():等待线程结束

有时候,主线程需要等子线程干完活,才能继续往下走。这时候就用join()。说白了,就是让主线程「等一下」。

import threading
import time

def slow_worker():
    time.sleep(3)
    print("子线程完成")

t = threading.Thread(target=slow_worker)
t.start()
print("主线程等待中...")
t.join()  # 主线程停在这里,等t结束
print("主线程继续执行")

运行结果会是这样:

主线程等待中...
子线程完成
主线程继续执行

你注意到没有?「主线程等待中」先打印出来,然后等了3秒,子线程打印完,主线程才继续。这就是join的效果。

注意:如果不调join(),主线程可能直接跑完退出,子线程还在后台跑。这在某些场景下会导致程序行为不可预测。我曾经在写爬虫时,主线程退出了,子线程的请求还没发完,结果数据丢了。

join()还可以传超时参数:

t.join(timeout=2)  # 最多等2秒

如果2秒内线程没结束,主线程就不等了,继续往下走。这个在需要控制总执行时间时特别有用。

2.3 线程的daemon属性

这个属性,说实话,很多人一开始会忽略。但它其实挺重要的。

daemon线程,也叫守护线程。它的特点是:当主线程结束时,所有daemon线程会被强制终止。而非daemon线程(默认情况)会继续运行,直到自己结束。

为什么会这样?你想想看,有些线程是「辅助性」的,比如后台日志收集、心跳检测。主线程都退出了,这些线程留着也没意义。这时候就应该设为daemon。

import threading
import time

def daemon_worker():
    while True:
        print("守护线程工作中...")
        time.sleep(1)

t = threading.Thread(target=daemon_worker)
t.daemon = True  # 设为守护线程
t.start()

time.sleep(3)
print("主线程结束")
# 主线程一结束,daemon线程立刻被终止

运行结果:

守护线程工作中...
守护线程工作中...
守护线程工作中...
主线程结束

看到没?主线程睡了3秒,daemon线程打印了3次。主线程一结束,daemon线程就停了,不会继续打印第4次。

重要规则:daemon属性必须在start()之前设置。一旦线程启动了,就不能再改了。否则Python会抛RuntimeError。

那什么时候用daemon?我总结了几种场景:

  • 后台监控任务:比如定期检查某个资源是否可用
  • 日志收集:主程序退出时,日志线程没必要继续
  • 心跳检测:网络连接的心跳包发送
  • 缓存刷新:定期刷新本地缓存

反过来,什么时候不要用daemon?

  • 你需要线程执行完某些关键操作,比如写文件、提交数据库事务
  • 线程里有资源需要手动释放
  • 你希望线程无论如何都要跑完

我曾经在做一个实时数据处理系统时,把数据写入线程设成了daemon。结果主程序异常退出时,写入线程被强制终止,最后一批数据没来得及落盘。嗯,从那以后,凡是涉及数据持久化的线程,我都不敢设daemon了。

2.4 本章知识体系

为了让你更直观地理解本章内容,我画了一张图:

threading模块入门知识体系 创建线程 方式一:传入目标函数 方式二:继承Thread类 启动(start)与等待(join) daemon守护线程属性 简单任务首选 封装复杂逻辑 重写run方法 start前设置 主线程结束自动终止 适合后台辅助任务 核心:start启动 → join等待 → daemon控制生命周期
我的建议:刚开始学的时候,先掌握函数方式创建线程,配合start和join使用。daemon属性可以等遇到具体场景再深入理解。别想一口吃成胖子。

好了,这一章的内容就到这。记住三个核心操作:start()让线程跑起来,join()让主线程等一等,daemon属性控制线程是不是「随主而终」。把这些基础打牢,后面学锁、学队列、学线程池,都会轻松很多。

可可精品资料库