☰
python线程启动的四种方式总结
2026/10/30 3:41:26 网站建设 项目流程

前言


标题里的「四种方式」不是官方术语,而是实践中归纳出来的四种启动线程的写法。它们最终都在调用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()

三条硬性规则,来自官方文档:



  1. 子类只应重写__init__()和run(),其他方法不要碰。

  2. 重写__init__()时,必须先调用super().__init__(),再干别的。

  3. 启动用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()了它,就自动在主程序退出前干完活。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询