线程的创建与启动:两种方式,一个目标
各位同学,今天我们来聊聊 Python 多线程编程的起点——threading 模块。说实话,我刚开始接触多线程时,总觉得这东西很神秘。后来踩过不少坑,才慢慢摸清门道。今天这章,咱们就从最基础的线程创建开始。
为什么需要多线程?
先问大家一个问题:你写过一个程序,运行起来后界面卡死过吗?
我遇到过。有一次写个文件下载工具,单线程下载大文件时,整个界面像死了一样。用户点取消都没反应。这就是典型的单线程阻塞问题。
多线程的核心价值,说白了就是「并发执行」。让程序同时干多件事,比如一边下载文件,一边响应用户操作。嗯,这就是我们学习 threading 的初衷。
创建线程的两种方式
Python 的 threading 模块提供了两种创建线程的方式。我个人习惯用第二种,但第一种在某些场景下也很方便。
方式一:直接使用 Thread 类
最直接的方式,就是把一个函数传给 Thread 对象。看代码:
import threading
import time
def download_file(file_name):
print(f"开始下载:{file_name}")
time.sleep(3) # 模拟下载耗时
print(f"下载完成:{file_name}")
# 创建线程
t = threading.Thread(target=download_file, args=("python_教程.pdf",))
# 启动线程
t.start()
print("主线程继续执行其他任务...")
运行这段代码,你会发现「主线程继续执行其他任务...」会先打印出来,然后才是下载完成的消息。这就是并发——主线程没等下载线程结束,就继续往下走了。
方式二:继承 Thread 类
这种方式更面向对象一些。当你的线程需要维护状态,或者逻辑比较复杂时,我推荐用这个。
class DownloadThread(threading.Thread):
def __init__(self, file_name):
super().__init__()
self.file_name = file_name
self.progress = 0
def run(self):
print(f"开始下载:{self.file_name}")
for i in range(10):
time.sleep(0.3)
self.progress = (i + 1) * 10
print(f"{self.file_name} 下载进度:{self.progress}%")
print(f"下载完成:{self.file_name}")
# 使用
t = DownloadThread("系统编程.pdf")
t.start()
注意看,关键是要重写 run() 方法。线程启动后,会自动调用 run() 里的代码。我在项目中经常用这种方式,因为可以方便地在线程对象上保存一些中间状态,比如这里的 progress。
线程的启动与等待
创建完线程,下一步就是启动它。启动很简单,调用 start() 方法就行。但有个细节要注意:不要直接调用 run()。
为什么?
直接调用 run() 相当于普通函数调用,不会创建新线程。代码会在当前线程里顺序执行。我曾经见过新手同事犯这个错,调试了半天才发现线程根本没跑起来。
那怎么等待线程结束呢?用 join() 方法:
t = threading.Thread(target=download_file, args=("data.zip",))
t.start()
print("等待下载线程结束...")
t.join() # 主线程会在这里阻塞,直到 t 线程执行完毕
print("下载线程已结束,主线程继续")
join() 的作用就是让主线程「等一等」。这在需要汇总多个线程结果时特别有用。比如你要同时下载 10 个文件,等全部下载完再合并处理,这时候 join() 就派上用场了。
daemon 属性:守护线程的秘密
这是很多初学者容易忽略的点,但我觉得它非常重要。
先看个例子:
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 线程的特性:当所有非守护线程都结束时,守护线程会被强制终止。
我曾经写过一个日志收集系统,后台有个线程专门收集日志并写入文件。这个线程就设成了 daemon。因为主程序退出后,日志收集也没必要继续了。但如果你的线程在做一些必须完成的操作(比如保存数据),千万别设成 daemon,否则数据可能丢失。
start() 之前设置。我曾经在 start() 之后才想起来设 daemon,结果抛了个 RuntimeError。记住:先设属性,再启动线程。
知识体系总览
下面这张图总结了本章的核心内容,帮你理清思路:
总结一下
这一章我们学了三个核心点:
- 创建线程:两种方式,简单场景用函数式,复杂场景用继承式
- 启动与等待:
start()启动新线程,join()等待线程结束。千万别直接调run() - daemon 属性:守护线程随主线程退出而终止,适合后台服务类任务
嗯,这些基础打牢了,后面讲线程同步、锁、条件变量时,你才不会觉得吃力。下一章我们聊聊线程之间怎么安全地共享数据——那才是多线程编程真正「烧脑」的地方。