第2章:threading模块基础——创建线程的两种方式、线程的启动与等待、线程的属性和方法

好,咱们正式开始动手写多线程代码了。

这一章,我带你从最基础的 threading 模块入手。说实话,Python 的多线程入门门槛并不高,但坑也不少。我个人习惯先把基础打牢,再谈那些花里胡哨的并发模型。

你想想看,如果连线程怎么创建、怎么启动、怎么等它结束都搞不清楚,后面谈线程安全、谈锁,那都是空中楼阁。

2.1 创建线程的两种方式

Python 的 threading 模块给了我们两条路走。我在项目中两种都用过,各有各的适用场景。

方式一:直接使用 Thread 类(函数式)

这是最直观的方式。你把一个普通函数丢给 Thread 对象,它就帮你跑起来了。

import threading
import time

def download_file(file_id):
    print(f"开始下载文件 {file_id}")
    time.sleep(2)  # 模拟下载耗时
    print(f"文件 {file_id} 下载完成")

# 创建线程
t1 = threading.Thread(target=download_file, args=(1,))
t2 = threading.Thread(target=download_file, args=(2,))

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

print("主线程继续做其他事情...")

嗯,这里要注意:args 参数必须是一个元组。哪怕只有一个参数,也要写成 (1,) 这种形式,逗号不能省。我曾经在这上面栽过跟头,少写个逗号,结果传参传了个寂寞。

方式二:继承 Thread 类(面向对象式)

当你的线程逻辑比较复杂,或者需要维护一些内部状态时,我建议用这种方式。说白了,就是把线程封装成一个类。

import threading
import time

class DownloadThread(threading.Thread):
    def __init__(self, file_id):
        super().__init__()
        self.file_id = file_id
        self.progress = 0

    def run(self):
        print(f"开始下载文件 {self.file_id}")
        for i in range(5):
            time.sleep(0.5)
            self.progress = (i + 1) * 20
            print(f"文件 {self.file_id} 进度: {self.progress}%")
        print(f"文件 {self.file_id} 下载完成")

# 创建并启动
t = DownloadThread(1)
t.start()

关键点在于:你必须重写 run() 方法。线程启动后,会自动调用这个方法。别问我为什么不是 __init__,这是 threading 模块的设计约定。

两种方式怎么选?

  • 任务简单、逻辑单一 → 用函数式,代码更简洁
  • 需要维护状态、有复杂逻辑 → 用继承式,结构更清晰

2.2 线程的启动与等待

创建完线程,只是第一步。你得知道怎么让它跑起来,怎么等它跑完。

启动线程:start()

调用 start() 方法,线程才会真正开始执行。注意,不是 run()

千万别直接调 run()!

直接调 run() 不会启动新线程,它只是在当前线程里执行函数。我见过不少新手犯这个错,结果发现程序还是串行执行的,百思不得其解。

等待线程:join()

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

import threading
import time

def worker():
    print("子线程开始工作")
    time.sleep(3)
    print("子线程工作结束")

t = threading.Thread(target=worker)
t.start()

print("主线程等待子线程...")
t.join()  # 主线程阻塞在这里,直到 t 结束
print("主线程继续执行")

运行结果会告诉你:主线程确实在 join() 那里停住了,等子线程跑完才继续。

你还可以给 join() 传一个超时参数,比如 t.join(1),表示最多等 1 秒。超时后主线程就不等了,继续往下走。这个特性我在做监控系统时用过,防止某个线程卡死导致整个程序挂掉。

2.3 线程的属性和方法

一个线程对象,身上挂着不少有用的信息。我挑几个常用的说说。

属性/方法 说明 使用场景
name 线程名称 调试时区分不同线程
ident 线程唯一标识符 底层系统识别
is_alive() 线程是否还在运行 检查线程状态
daemon 是否为守护线程 后台任务管理
current_thread() 获取当前线程对象 在任意位置获取线程信息

name 和 ident

每个线程都有名字。你不指定的话,Python 会帮你自动生成,比如 Thread-1Thread-2。我个人习惯在创建线程时给个有意义的名字,方便调试。

t = threading.Thread(target=worker, name="下载线程-1")
print(t.name)   # 输出:下载线程-1
print(t.ident)  # 输出:一个整数,比如 12345

is_alive() 方法

这个方法返回布尔值,告诉你线程是否还在跑。我在做进度监控时经常用它。

t = threading.Thread(target=long_task)
t.start()

while t.is_alive():
    print("线程还在跑...")
    time.sleep(0.5)

print("线程已结束")

daemon 属性——守护线程

这个属性有点意思。守护线程的特点是:当主线程结束时,守护线程会被强制终止,不管它有没有跑完。

什么时候用守护线程?

比如后台日志收集、心跳检测这类任务。主程序退出时,这些后台任务也没必要继续了。

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

t = threading.Thread(target=background_task, daemon=True)
t.start()

time.sleep(3)
print("主线程结束,守护线程也会被强制终止")

运行这段代码,你会发现主线程结束后,程序就退出了,后台任务不再打印。如果不设置 daemon=True,程序会一直跑下去,因为那个 while True 永远不会停。

注意: 守护线程里不要做重要的资源清理工作。因为它随时可能被强制终止,你无法保证它执行完毕。

2.4 避坑指南

最后,分享几个我踩过的坑。

  • 线程不能重复 start():一个线程对象只能 start() 一次。想再跑一次,得新建一个线程对象。
  • join() 会阻塞主线程:如果你在 GUI 程序的主线程里调 join(),界面会卡死。记得把 join() 放在工作线程里,或者用超时参数。
  • daemon 线程要小心:我曾经写过一个后台数据同步任务,忘了设 daemon=True,结果程序关不掉,每次都得杀进程。

好了,这一章的内容就这些。创建线程、启动线程、等待线程、查看线程属性,这些都是基本功。下一章,我们会聊线程安全的问题——这才是多线程编程真正的硬骨头。