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

说到并发编程,线程是绕不开的基础。Python 的 threading 模块,说白了就是我们操作线程的瑞士军刀。今天我就带大家把这块硬骨头啃下来。

threading 模块入门

先聊聊 threading 模块。它是 Python 标准库的一部分,开箱即用。我个人习惯在项目里这样导入:

import threading
import time

def worker():
    print(f"线程 {threading.current_thread().name} 开始干活")
    time.sleep(2)
    print(f"线程 {threading.current_thread().name} 干完了")

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

你看,就这么几行代码,一个线程就跑起来了。我在项目中遇到过不少新手直接写 t.run() 而不是 t.start(),结果线程根本没启动,代码顺序执行了。嗯,这里要注意:start() 才是启动线程的正确姿势。

核心要点:threading 模块提供了线程的创建、管理、同步等全套工具。它是对底层 _thread 模块的封装,用起来更安全、更顺手。

创建线程的两种方式

创建线程有两种主流方式。我刚开始学的时候也觉得没必要分两种,后来踩了坑才明白——各有各的适用场景。

方式一:直接使用 Thread 类

这种方式最直接,适合简单的任务:

import threading

def download_file(url):
    print(f"正在下载:{url}")
    # 模拟下载耗时
    time.sleep(3)
    print(f"下载完成:{url}")

urls = ["http://example.com/1", "http://example.com/2"]
threads = []

for url in urls:
    t = threading.Thread(target=download_file, args=(url,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

你想想看,如果每个下载任务都写一个函数,代码会变得很零散。但这种方式胜在简单,临时用一下很方便。

方式二:继承 Thread 类

当任务逻辑比较复杂,或者需要封装状态时,我建议用继承的方式:

class DownloadThread(threading.Thread):
    def __init__(self, url):
        super().__init__()
        self.url = url
        self.result = None

    def run(self):
        print(f"线程 {self.name} 开始下载 {self.url}")
        time.sleep(2)
        self.result = f"{self.url} 的内容"
        print(f"线程 {self.name} 下载完成")

# 使用
t = DownloadThread("http://example.com")
t.start()
t.join()
print(t.result)

我在项目中遇到过需要在线程间传递复杂状态的场景。用继承方式,你可以把状态封装在类属性里,代码结构清晰多了。说白了,这就是面向对象思维在并发编程里的体现。

我的建议:简单任务用函数式,复杂任务用继承式。没有绝对的好坏,看场景选。

线程的启动与等待

启动线程用 start(),等待线程结束用 join()。这两个方法看似简单,但用不好会出大问题。

start() 做了两件事:创建操作系统线程,然后调用 run() 方法。记住,一个线程只能 start() 一次,重复调用会抛出 RuntimeError。

join() 是阻塞调用。主线程调用 t.join() 后,会一直等到线程 t 结束才继续往下走。你可以传一个超时参数:

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

我曾经在项目里遇到过一个诡异的 bug:主线程退出了,但子线程还在跑。排查了半天,发现是忘了调 join()。主线程一结束,子线程也被强制终止了。嗯,这个坑我记忆犹新。

避坑指南:如果你需要等待所有子线程完成,一定要在启动所有线程后再统一 join。不要在循环里启动一个就 join 一个,那样就变成串行执行了。

线程的 daemon 属性

daemon 属性是个容易让人迷惑的概念。我刚开始学的时候,总觉得它和 Linux 的守护进程是一回事。其实不是。

daemon 线程的特点是:当主线程结束时,所有 daemon 线程会被强制终止。非 daemon 线程则不会,主线程会等它们执行完。

import threading
import time

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

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

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

你想想看,这个特性在什么场景下有用?我举个例子:日志收集、心跳检测、后台监控。这些任务不需要主线程等它们结束,主线程退出时它们跟着退出就行。

属性 daemon=True daemon=False(默认)
主线程退出时 强制终止 等待执行完毕
适用场景 后台任务、监控 需要确保完成的任务
注意事项 不能持有需要清理的资源 可能导致程序无法退出

重要提醒:daemon 线程不能持有需要手动清理的资源(比如打开的文件、数据库连接)。因为主线程退出时,daemon 线程会被强制终止,资源来不及释放。

我曾经在项目里用 daemon 线程做日志上传。主线程退出时,日志线程被强制终止,导致最后几条日志没上传成功。后来我改成了非 daemon 线程,加了一个优雅退出的机制。说白了,daemon 线程用起来方便,但也要承担相应的风险。

知识体系总览

下面这张图把本章的核心知识点串起来了,你可以对照着回顾一下:

threading 模块 创建线程 启动与等待 daemon 属性 Thread(target=...) 继承 Thread 类 start() join(timeout) daemon=True daemon=False 核心:创建 → 启动 → 等待 → 管理生命周期

这张图把本章的知识点串成了完整的流程。从创建线程开始,到启动、等待,再到 daemon 属性的选择,每一步都有它的意义。我在实际项目中,就是按照这个流程来设计线程的。

小技巧:写多线程代码时,先在纸上画出线程的生命周期图。哪个线程负责什么任务,什么时候创建,什么时候 join,一目了然。这个习惯帮我避免了很多并发问题。

好了,线程基础就聊到这里。记住:创建线程只是第一步,怎么管理好线程的生命周期才是关键。下一章我们会深入线程同步的话题,到时候再聊。

想要更多资料,联系可可