前言
标题里的「四种方式」不是官方术语,而是实践中归纳出来的四种启动线程的写法。它们最终都在调用threading.Thread.start(),区别只在于「谁来组织任务」和「谁来管理生命周期」:有的是直接传一个函数,有的是继承类,有的是交给线程池,有的还带定时。
需要先纠正一个常见说法:网上有些资料把「用_thread.start_new_thread()」也算作一种「启动方式」,还说它比threading更快。_thread是低层模块,threading正是建立在它之上的封装;日常编程直接使用_thread会丢掉线程名、join()、异常钩子等一大堆便利,官方也不推荐。本文把它作为补充说明列出,但不作为推荐的四种之一。
四种推荐写法是:函数式、继承式、线程池式、定时器式。下面逐个给出可运行的例子,示例以 CPython 3.8 及以上为基准。
一、方式一:直接传函数(函数式)
最常用、代码最短的一种。把现成的函数交给target。
# 适用于 Python 3.8+
import threading
import time
def download(name, seconds):
print(f"开始下载 {name}")
time.sleep(seconds)
print(f"下载完成 {name}")
t = threading.Thread(target=download, args=("文件A", 1.0), name="dl-A")
t.start()
t.join()注意args是元组,单参数必须写成args=("文件A", 1.0)这种形式,元素之间用逗号分隔;如果只有一个参数,末尾逗号不能省。
优点是简单、无需定义类;缺点是任务与参数混在一起,需要共享状态时要么用闭包、要么用全局变量,久了不容易维护。
二、方式二:继承Thread并重写run()
把任务封装成类,状态用实例属性保存,可读性更好。
# 适用于 Python 3.8+
import threading
import time
class Downloader(threading.Thread):
def __init__(self, name, seconds):
super().__init__(name=f"dl-{name}") # 先调用父类构造
self.file_name = name
self.seconds = seconds
def run(self):
print(f"开始下载 {self.file_name}")
time.sleep(self.seconds)
print(f"下载完成 {self.file_name}")
d = Downloader("文件B", 1.0)
d.start() # 由 start() 去调用 run()
d.join()三条硬性规则,来自官方文档:
- 子类只应重写
__init__()和run(),其他方法不要碰。 - 重写
__init__()时,必须先调用super().__init__(),再干别的。 - 启动用
start(),不要直接调run()——直接调只是当前线程里的普通方法调用,不会开新线程。
三、方式三:线程池ThreadPoolExecutor
前两种方式都要自己数线程、自己join()。任务一多,更推荐交给线程池:线程复用、结果可收集、异常可传播、退出自动收尾。
# 适用于 Python 3.8+
import concurrent.futures
import time
def fetch(url):
time.sleep(0.5) # 模拟一次网络请求
return f"{url} 返回 200"
def main():
urls = [f"https://example.com/page/{i}" for i in range(5)]
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool:
# 方式 A:submit + as_completed,按完成先后取结果
future_map = {pool.submit(fetch, u): u for u in urls}
for fut in concurrent.futures.as_completed(future_map):
print(fut.result())
if __name__ == "__main__":
main()也可以用map()一次提交一批,结果按提交顺序产出:
# 适用于 Python 3.8+
import concurrent.futures
import time
def square(x):
time.sleep(0.1)
return x * x
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
for value in pool.map(square, range(5)):
print(value)| 需求 | 推荐 API |
|---|
| 提交单个任务、逐个拿结果 | executor.submit(fn, *args) |
| 一次提交一批、要最终结果 | executor.map(fn, iterable) |
| 按完成先后处理 | submit+as_completed |
| 控制并发上限 | 构造参数max_workers |
max_workers的默认值有过变化:3.8 起为min(32, os.cpu_count() + 4),3.13 起改为min(32, (os.process_cpu_count() or 1) + 4)。手动指定一个值更可控。
四、方式四:定时器Timer
threading.Timer是Thread的子类,用来「过一段时间再执行」。它同样用start()启动。
# 适用于 Python 3.8+
import threading
def remind(msg):
print(f"提醒:{msg}")
reminder = threading.Timer(2.0, remind, args=("该保存文件了",))
reminder.start() # 2 秒后执行
# 如果中途反悔,可以在它执行前取消:
# reminder.cancel()官方文档给出的构造签名是Timer(interval, function, args=None, kwargs=None)。注意这里args的默认值是None而不是():传None时内部按空列表处理。实际使用时照常传元组即可。
Timer也可以join()和cancel()。cancel()只能取消尚未开始执行的定时器;一旦动作已经跑起来,就取消不掉了。
五、四种方式的对比
| 方式 | 最小代码量 | 适合场景 | 主要缺点 |
|---|
函数式Thread(target=...) | 最少 | 一两个临时任务 | 任务多了要手动管理 |
继承Thread | 较多 | 任务有状态、需要复用 | 类写得啰嗦 |
ThreadPoolExecutor | 中等 | 批量任务、生产环境 | 需要理解Future |
Timer | 少 | 延迟执行、超时提醒 | 只适合一次性延时 |
顺带说一句低层的_thread:
# 适用于 Python 3.8+
import _thread
import time
def worker(name):
time.sleep(0.3)
print(f"worker {name} 结束")
# 返回一个线程标识,但没有 Thread 对象可用
_thread.start_new_thread(worker, ("X",))
time.sleep(0.5) # 必须自己等,否则主线程退出会带走它它没有join(),没有线程名,异常处理也更粗糙——正因如此,threading才是日常选择。Python 2 里的thread模块在 Python 3 中已改名为_thread,thread这个名字在 3 里已经不存在了。
常见坑点
1.args忘了逗号,参数被拆散
❌Thread(target=f, args=("abc")),"abc"被按字符拆成三个参数。 ✅args=("abc",)。
2. 继承了Thread却直接调run()
❌downloader.run()—— 在当前线程同步执行,没开新线程。 ✅downloader.start()。
3. 子类__init__忘了调父类构造
❌ 子类里只写self.name = ...不调super().__init__(),字段可能缺失、报错。 ✅ 第一行就super().__init__(**kwargs)。
4. 用Timer却指望cancel()万能
❌ 动作已经开始执行了还调cancel(),发现没用。 ✅cancel()只对尚未触发的定时器有效。
5. 手工开一堆线程却不join()
❌ 循环Thread(target=f).start()后直接退出主流程。 ✅ 收集线程对象后统一join(),或改用ThreadPoolExecutor的with。
6. 忘了with之外还要shutdown
❌ 用ThreadPoolExecutor()不用with,也没有shutdown(),进程迟迟不退出。 ✅ 用with块,或显式executor.shutdown(wait=True)。
7._thread.start_new_thread之后主线程先跑完
❌ 用_thread启动后直接结束主线程,任务被中断。 ✅ 改用threading并join();实在要用就自己sleep等待。
8. 在 CPU 密集任务上选线程池
❌ 用ThreadPoolExecutor跑大量纯计算,结果没变快。 ✅ 受 GIL 限制,计算密集请换ProcessPoolExecutor。
总结
| 启动方式 | 启动语句 | 何时用 |
|---|
| 函数式 | Thread(target=f, args=(...)).start() | 临时任务 |
| 继承式 | class T(Thread): ...; T().start() | 任务带状态 |
| 线程池 | with ThreadPoolExecutor() as p: p.submit(f) | 批量任务 |
| 定时器 | Timer(2.0, f).start() | 延迟执行 |
四种写法本质上是同一个start()的不同包装。选型的标准不是「哪个高级」,而是任务的规模与生命周期:单个任务用函数式,有状态用继承式,批量用线程池,需要延时用Timer。无论选哪种,都别忘了等待——线程不会因为你start()了它,就自动在主程序退出前干完活。