线程基础: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 线程用起来方便,但也要承担相应的风险。
知识体系总览
下面这张图把本章的核心知识点串起来了,你可以对照着回顾一下:
这张图把本章的知识点串成了完整的流程。从创建线程开始,到启动、等待,再到 daemon 属性的选择,每一步都有它的意义。我在实际项目中,就是按照这个流程来设计线程的。
小技巧:写多线程代码时,先在纸上画出线程的生命周期图。哪个线程负责什么任务,什么时候创建,什么时候 join,一目了然。这个习惯帮我避免了很多并发问题。
好了,线程基础就聊到这里。记住:创建线程只是第一步,怎么管理好线程的生命周期才是关键。下一章我们会深入线程同步的话题,到时候再聊。
想要更多资料,联系可可