2. Python线程基础:threading模块入门

说实话,很多初学者一上来就问我:「Python多线程到底有没有用?」我的回答是——看场景。但不管有没有用,threading模块都是你绕不开的基础。今天我们就把它彻底搞明白。

2.1 threading模块:Python的线程工具箱

Python标准库里的threading模块,说白了就是一套帮你创建和管理线程的工具。我最早接触它是在做一个爬虫项目,那时候还傻乎乎地用_thread底层模块,结果被锁和同步搞得焦头烂额。后来换成threading,真香。

这个模块提供了什么?

  • Thread类:创建线程的核心类
  • Lock、RLock:互斥锁和可重入锁
  • Semaphore、Event、Condition:线程同步工具
  • Queue:线程安全的队列

嗯,今天我们先聚焦最基础的——Thread类。

2.2 创建线程的两种方式

我个人习惯把创建线程的方式分成两类:函数式和类式。两种都能用,但场景不同。

方式一:直接传入函数

这是最直观的方式。你定义一个普通函数,然后把它丢给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()

我在项目中遇到过一种情况:临时需要一个轻量级的后台任务,比如定时刷新缓存。这时候用函数式创建线程,代码量少,逻辑清晰。你想想看,如果每个小任务都要定义一个类,那得多累赘。

方式二:继承Thread类

当你的线程需要维护状态、或者有复杂的初始化逻辑时,继承Thread类更合适。

import threading
import time

class MyWorker(threading.Thread):
    def __init__(self, name, delay):
        super().__init__()
        self.name = name
        self.delay = delay

    def run(self):
        print(f"线程 {self.name} 启动")
        time.sleep(self.delay)
        print(f"线程 {self.name} 完成")

t = MyWorker("C", 3)
t.start()

注意,必须重写run方法。我曾经见过有人重写了start方法,结果线程根本没跑起来——因为start内部会调用run,你把它覆盖了,逻辑就断了。

核心区别:函数式适合简单任务,类式适合需要封装状态的复杂线程。

2.3 线程的启动与等待

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

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

很多人以为创建Thread对象线程就开始了——不对。创建对象只是「准备好了」,调用start()才会让操作系统真正分配资源并执行。

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

我踩过一个坑:直接调用run()而不是start()。结果线程代码确实执行了,但是在主线程里同步执行的,根本没有并发效果。嗯,这个错误我犯过一次就再也没忘过。

join():等待线程结束

有时候你需要等子线程干完活,再继续往下走。这时候就用join()。

import threading
import time

def slow_task():
    time.sleep(3)
    print("任务完成")

t = threading.Thread(target=slow_task)
t.start()
print("等待子线程...")
t.join()  # 主线程阻塞在这里,直到t结束
print("子线程已结束,主线程继续")

你可以给join()传一个超时参数,比如t.join(2)表示最多等2秒。超时后主线程不再等待,但子线程仍在后台运行。这个特性我在做超时控制时经常用。

小技巧:如果你有多个线程要等,可以循环调用join()。但注意顺序——先调start()再调join(),别搞反了。

2.4 线程的daemon属性

这个属性,说实话,很多人一开始都搞不明白。我当年也是。

daemon线程,也叫守护线程。它的特点是:当所有非daemon线程都结束时,Python程序会强制退出,不管daemon线程是否还在运行。

import threading
import time

def background_task():
    while True:
        print("后台任务运行中...")
        time.sleep(1)

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

time.sleep(3)
print("主线程结束")
# 程序退出,daemon线程被强制终止

运行这段代码,你会发现程序在3秒后退出,不会无限打印。因为主线程(非daemon)结束了,daemon线程就被「抛弃」了。

警告:daemon线程在退出时不会执行清理操作(比如关闭文件、释放锁)。所以,不要把重要的资源管理逻辑放在daemon线程里。我曾经在daemon线程里写数据库连接,结果程序退出时连接没关闭,导致连接泄漏——排查了好久才发现。

什么时候用daemon线程?

  • 后台监控任务(比如心跳检测)
  • 日志轮转
  • 自动保存草稿

这些任务「丢了就丢了」,不影响主业务。反过来,如果你的线程需要完成收尾工作,比如写文件、发送确认消息,那就别设成daemon。

知识体系总览

下面这张图,是我梳理的本章核心脉络。你看一眼,心里就有谱了。

Python线程基础 创建线程的两种方式 函数式 继承Thread类 启动与等待 start() join() daemon属性 守护线程 vs 用户线程 核心:线程是操作系统调度的最小单位 Python的GIL让多线程在CPU密集场景受限 💡 先掌握基础,再深入锁与同步

这张图把本章的三个核心知识点串起来了。你记住:创建 → 启动 → 控制生命周期,这就是线程编程的基本套路。

本章小结:

  • 创建线程有两种方式:函数式(简单任务)和类式(复杂状态)
  • start()让线程真正运行,join()等待线程结束
  • daemon线程随主线程退出而终止,适合后台任务
  • 别在daemon线程里做资源清理工作——这是血的教训

好了,线程基础就聊到这儿。下一节我们会深入锁机制,到时候你会明白为什么「线程安全」这四个字值万金。


可可精品资料库