2. threading模块入门:创建线程的两种方式
说实话,Python的多线程入门并不难。但很多新手一上来就被各种概念搞晕了。我刚开始学的时候也是这样,总觉得线程是个很玄乎的东西。后来做项目多了才发现,其实核心就那么几个点。
今天咱们就聊聊threading模块最基础的东西——创建线程的两种方式。你想想看,不管多复杂的并发程序,归根结底都是从这两招开始的。
2.1 函数式创建:最直接的方式
我个人习惯先用函数式来演示,因为它够简单、够直观。说白了,就是把你要并发执行的代码,封装到一个普通函数里,然后交给Thread去跑。
import threading
import time
def worker(name, delay):
"""模拟一个耗时任务"""
print(f"线程 {name} 开始工作")
time.sleep(delay)
print(f"线程 {name} 工作完成,耗时{delay}秒")
# 创建线程
t1 = threading.Thread(target=worker, args=("A", 2))
t2 = threading.Thread(target=worker, args=("B", 3))
# 启动线程
t1.start()
t2.start()
# 等待线程结束
t1.join()
t2.join()
print("所有线程执行完毕")
这段代码跑起来,你会发现两个线程几乎是同时启动的。我在项目中遇到过类似场景——批量下载文件时,用这种方式把每个下载任务丢到独立线程里,速度直接翻倍。
2.2 类继承方式:更灵活的结构
当你的线程逻辑比较复杂,或者需要维护一些内部状态时,函数式就不太够用了。这时候我推荐用类继承的方式——继承Thread类,重写run方法。
import threading
import time
class DownloadThread(threading.Thread):
def __init__(self, url, save_path):
super().__init__()
self.url = url
self.save_path = save_path
self.downloaded_size = 0 # 内部状态
def run(self):
"""线程启动后自动执行的方法"""
print(f"开始下载: {self.url}")
# 模拟下载过程
for i in range(5):
time.sleep(1)
self.downloaded_size += 20
print(f"[{self.name}] 已下载: {self.downloaded_size}%")
print(f"下载完成: {self.save_path}")
# 使用
t = DownloadThread("https://example.com/file.zip", "/tmp/file.zip")
t.start()
t.join()
你看,类继承的好处就出来了——可以保存每个线程自己的状态(比如下载进度),还能扩展更多方法。我记得有一次做爬虫项目,需要统计每个线程抓取了多少页面,用类继承就特别方便。
- 函数式:简单任务首选,代码量少
- 类继承:复杂逻辑、需要状态管理时用
2.3 线程的启动与等待
嗯,这里要注意两个关键方法:start() 和 join()。
start() 是告诉系统:「这个线程可以跑了」。但注意,它不会立即执行,而是进入就绪状态,等操作系统调度。我见过有人直接调用 run() 方法——那其实是在主线程里顺序执行,根本不是并发!
join() 是让主线程等一等。比如你下载文件,主线程必须等下载线程结束才能处理文件。不调用join的话,主线程可能先跑完了,程序就退出了。
import threading
import time
def slow_task(name, seconds):
time.sleep(seconds)
print(f"{name} 完成")
# 错误示范:不等待
t = threading.Thread(target=slow_task, args=("任务1", 3))
t.start()
print("主线程结束") # 这行会先打印,因为没等t
# 正确做法:等待
t = threading.Thread(target=slow_task, args=("任务2", 3))
t.start()
t.join() # 主线程停在这里,等t结束
print("主线程结束") # 这行会在任务2完成后才打印
2.4 线程的命名与标识
多线程调试时最头疼的是什么?分不清哪个线程在干活。所以给线程起个好名字特别重要。
| 方法 | 说明 | 示例 |
|---|---|---|
| name参数 | 创建时指定名字 | Thread(name="下载线程1") |
| ident属性 | 系统分配的唯一ID | thread.ident |
| native_id | 操作系统层面的线程ID | thread.native_id (Python 3.8+) |
| current_thread() | 获取当前线程对象 | threading.current_thread() |
import threading
import time
def worker():
current = threading.current_thread()
print(f"当前线程: {current.name}, ID: {current.ident}")
time.sleep(1)
# 创建时指定名字
threads = []
for i in range(3):
t = threading.Thread(target=worker, name=f"Worker-{i}")
threads.append(t)
t.start()
for t in threads:
t.join()
# 如果不指定名字,系统会默认叫 Thread-1, Thread-2 ...
t_default = threading.Thread(target=worker)
t_default.start()
t_default.join()
我个人习惯在创建线程时都加上有意义的名字,比如 "下载线程-图片"、"计算线程-数据分析"。这样日志一打出来,一眼就能看出问题出在哪个环节。
2.5 本章知识体系
说了这么多,咱们用一张图来总结一下。这张图把线程的创建、启动、等待、命名串起来了,你一看就明白整个流程。
这张图把整个流程串起来了。你从上面两种创建方式选一种,然后调用start()启动,线程就跑起来了。最后用join()等它结束,或者让它自己跑完。就这么简单。
好了,线程的创建入门就聊到这儿。记住两个核心点:函数式适合简单任务,类继承适合复杂逻辑。命名一定要有意义,调试时能省不少时间。下一节咱们聊聊线程同步的问题——那才是真正考验并发功底的地方。