线程创建与管理:使用threading模块创建线程、线程的启动与等待、守护线程与前台线程

多线程编程,说白了就是让程序同时干几件事。但很多人一上来就踩坑——线程怎么创建?启动后怎么等它结束?守护线程又是什么鬼?别急,咱们一个一个捋清楚。

核心要点:Python的threading模块是标准库,开箱即用。但用不好,轻则逻辑混乱,重则程序崩溃。我见过太多人把线程当函数调,结果程序跑飞了还不知道为什么。

1. 创建线程的两种姿势

我个人习惯用两种方式创建线程。第一种最直接,适合简单任务;第二种更灵活,适合复杂场景。

方式一:直接传入函数

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()

# 等待线程结束
t1.join()
t2.join()

print("所有线程执行完毕")

你看,就这么简单。target指定要执行的函数,args传参数。注意args必须是元组,哪怕只有一个参数也要加逗号——我刚开始写的时候就老忘这个逗号,结果报错半天找不出原因。

方式二:继承Thread类

class MyThread(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name
    
    def run(self):
        print(f"线程 {self.name} 开始工作")
        time.sleep(2)
        print(f"线程 {self.name} 结束")

t1 = MyThread("C")
t2 = MyThread("D")
t1.start()
t2.start()
t1.join()
t2.join()

这种方式的好处是,你可以把线程相关的数据和逻辑封装在一起。我在项目中处理网络请求时,就喜欢用这种方式——每个线程有自己的连接池、重试逻辑,代码清晰多了。

我的建议:简单任务用函数式,复杂任务用继承式。别为了炫技硬上继承,代码可读性才是第一位的。

2. 启动与等待:start()和join()的玄机

很多新手会问:为什么不用run()直接调用?嗯,这里要注意——run()只是普通方法调用,不会启动新线程。start()才是真正的线程启动入口。

为什么会这样?因为start()会调用操作系统的底层接口创建新线程,然后在新线程里执行run()方法。你直接调run(),那跟普通函数调用没区别。

警告:千万不要直接调用run()!我曾经在代码审查时看到有人这么写,结果程序跑起来跟单线程一样慢,排查了半天才发现是这个问题。

再说join()。它的作用是等待线程结束。如果不调join(),主线程可能先跑完,子线程还在后台跑。这在某些场景下没问题,但如果你需要子线程的结果,就必须等。

# 错误的做法
t = threading.Thread(target=worker, args=("E",))
t.start()
print("主线程结束")  # 可能先于子线程打印

# 正确的做法
t = threading.Thread(target=worker, args=("F",))
t.start()
t.join()  # 等子线程结束
print("主线程结束")  # 一定在子线程之后打印

3. 守护线程 vs 前台线程

这个概念很多人搞混。说白了,守护线程就是后台跑的小弟,前台线程是主角。

前台线程:主程序必须等所有前台线程结束才能退出。
守护线程:主程序退出时,守护线程会被强制终止。

设置守护线程很简单,在start()之前设置daemon=True:

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循环永远不会结束。

避坑指南:我曾经写过一个日志收集服务,用守护线程定期刷日志。结果有一次主程序异常退出,守护线程里的日志还没写完就被杀了,导致丢了一部分日志。后来我改成前台线程,加上优雅关闭的逻辑,才彻底解决。

4. 知识体系图

下面这张图帮你理清线程创建与管理的核心逻辑:

线程创建与管理核心流程 threading.Thread 方式一:target+函数 方式二:继承Thread类 start() 启动线程 start() 启动线程 join() 等待结束 daemon=True 守护线程 前台线程

5. 实战中的几个坑

最后分享几个我踩过的坑,你遇到了能少走弯路:

常见问题 原因 解决方案
线程启动后不执行 忘了调start(),或者调了run() 确认调用的是start(),不是run()
主线程提前退出 没调join()等待子线程 在需要等待的地方加join()
守护线程数据丢失 主线程退出时强制终止 改用前台线程+优雅关闭
线程间数据混乱 共享变量没加锁 使用Lock或Queue(后续章节会讲)

小技巧:调试多线程程序时,可以给每个线程起个有意义的名字。threading.current_thread().name 能帮你快速定位问题线程。我习惯用"worker-1"、"worker-2"这种命名方式,一看就知道是哪个。

嗯,线程创建与管理就这些内容。记住:start()启动,join()等待,daemon控制生命周期。这三个点搞明白了,多线程编程就迈出了第一步。

可可精品资料库