☰
详解如何使用python创建和结束线程
2026/10/30 8:39:42 网站建设 项目流程

前言


「创建线程」很好讲,Thread(...).start()一行就够;「结束线程」才是真正的难点,也是误解最集中的地方。


最常见的错误期待是:调用某个stop()方法,线程立刻停下。Python 3 的threading.Thread没有stop()方法,也没有任何受支持的、从外部强制终止线程的手段。原因不复杂:线程共享进程内存,如果它正好持有一把锁、或者正在改一个共享结构,这时候把它从中间掐断,锁永远不会被释放,别的线程就会永久卡死。强制终止线程在几乎所有语言里都是被刻意去掉的能力。


所以正确的心智模型是:线程只能自己结束。外部能做的是「发一个请求让它停」,这叫作协作式取消(cooperative cancellation)。本文给出创建线程的标准写法,再重点讲清结束线程的四种情形和协作停止的正确实现。示例以 CPython 3.8 及以上为基准。


一、创建线程


创建线程就两条路:传target函数,或继承Thread重写run()。


# 适用于 Python 3.8+
import threading
import time


def task(n):
time.sleep(0.2)
print(f"任务 {n} 完成")


# 路径一:函数式
t1 = threading.Thread(target=task, args=(1,), name="worker-1")
t1.start()

# 路径二:继承式
class Task(threading.Thread):
def __init__(self, n):
super().__init__(name=f"worker-{n}")
self.n = n

def run(self):
time.sleep(0.2)
print(f"任务 {self.n} 完成")


t2 = Task(2)
t2.start()

t1.join()
t2.join()
print("主线程结束")

要点回顾:args是元组(单元素要带逗号);start()只能调用一次,第二次抛RuntimeError;继承式只重写__init__和run,且__init__里必须先调super().__init__()。


二、线程什么时候「自己结束」


一个线程结束,只有下面几种情形:




结束方式触发条件是否优雅



run()正常返回任务执行完毕是

run()抛出未捕获异常出错退出是(资源释放仍需finally)

线程内调用sys.exit()等价于_thread.exit(),抛SystemExit是,只结束当前线程

解释器关闭、守护线程被掐断程序退出时否,资源可能没释放



关键点:sys.exit()在多线程里只结束当前线程,不会结束整个进程。这是很多人误以为「能用来杀程序」的地方。只有在主线程里,SystemExit才会一路向上导致程序退出。


# 适用于 Python 3.8+
import threading
import sys
import time


def quick_exit():
time.sleep(0.1)
sys.exit() # 只让这个线程结束,主进程照常
print("这行永远不会执行")


t = threading.Thread(target=quick_exit)
t.start()
t.join()
print("主进程还活着")

如果想结束整个程序,正确的做法是在主线程里做,或者用_thread.interrupt_main()向主线程发一个信号(它默认发的是SIGINT,等价于按下 Ctrl+C)。


三、正确做法:用事件做协作式停止


既然不能强杀,那就设计一条「停下来的路」。标准做法是用threading.Event当开关:工作线程循环里不断检查开关,主线程负责把它打开。


# 适用于 Python 3.8+
import threading
import time

stop_event = threading.Event()


def worker():
count = 0
# 用 wait 代替 sleep:既能限速,又能立刻被唤醒
while not stop_event.wait(timeout=0.1):
count += 1
print(f"处理第 {count} 批")
print(f"收到停止信号,共处理 {count} 批,准备收尾")


t = threading.Thread(target=worker, name="worker")
t.start()

time.sleep(0.35)
stop_event.set() # 通知它停
t.join(timeout=2) # 给它 2 秒收尾
print("是否还活着:", t.is_alive())

为什么用stop_event.wait(timeout=0.1)而不是time.sleep(0.1)?因为Event.wait在set()的瞬间就会返回True,线程能立即响应停止信号;而sleep必须睡满才醒,停止会有延迟。这是一个很实用的小技巧。


对比一下锁和事件:Lock是「互斥」,Event是「广播一个布尔状态」,语义完全不同。Event有set()、clear()、is_set()、wait(timeout=None)四个主要方法,wait的返回值可以在超时和置位之间区分——超时返回False,被set()唤醒返回True。


四、阻塞在 I/O 上怎么办


协作式停止有个前提:线程要能「醒过来」检查开关。如果它正阻塞在一个没有超时的网络读取上,那就永远等不到检查时机。


补救办法是给所有阻塞操作都加上超时:


# 适用于 Python 3.8+
import socket
import threading

stop_event = threading.Event()


def receive_loop(host, port):
with socket.create_connection((host, port), timeout=5) as sock:
sock.settimeout(1.0) # 关键:让 recv 定期返回
while not stop_event.is_set():
try:
data = sock.recv(4096)
except socket.timeout:
continue # 超时了就回去检查停止信号
if not data:
break
print("收到", len(data), "字节")

socket的settimeout()让recv到点就抛socket.timeout(从 Python 3.10 起它就是内置的TimeoutError的别名,在更早的版本里它只是OSError的子类,所以捕获socket.timeout才是跨版本都成立的写法),循环于是有机会检查stop_event。这就是把「不可中断的等待」改造成「可中断的等待」的通用思路,文件读写、数据库查询也同理——用带超时的接口,或在循环里分段处理。


五、实在需要强制终止:改用进程


如果任务真的无法配合(比如调用了某个不提供超时选项的第三方 C 库),那就不该用线程,而该用进程。子进程有独立的地址空间,可以安全地强制终止:


# 适用于 Python 3.8+
import multiprocessing
import time


def stubborn_job():
while True:
time.sleep(0.1) # 一个不检查任何标志的死循环


if __name__ == "__main__":
p = multiprocessing.Process(target=stubborn_job)
p.start()
time.sleep(0.5)
p.terminate() # 强制终止子进程
p.join()
print("已终止子进程,exitcode =", p.exitcode)

terminate()之所以在进程上可行、在线程上不可行,正是因为进程之间不共享内存,粗暴结束不会连累别人。线程不能强杀,进程可以——这是两者的一个本质区别。


六、守护线程:不是「结束」而是「被掐断」


daemon=True的线程,主程序退出时会被突然停止。官方文档的原话是:守护线程在关闭时会被 abruptly stopped,它持有的资源(打开的文件、数据库事务等)可能无法正确释放。所以守护线程适合「丢了也无所谓」的辅助任务,不适合关键写操作。


还有一个细节:daemon属性必须在start()之前设定,start()之后再改会抛RuntimeError。旧的setDaemon()/isDaemon()写法自 Python 3.10 起已废弃。


常见坑点


1. 到处找不存在的thread.stop()


❌t.stop()/t.kill()——AttributeError,Python 3 里根本没有。 ✅ 用Event做协作式停止,或把任务放到multiprocessing.Process里用terminate()。


2. 在子线程里调sys.exit()想结束整个程序


❌ 以为子线程里的sys.exit()能终止主程序。 ✅ 它只结束当前线程;要退出整个程序请在主线程处理,或用_thread.interrupt_main()。


3. 用time.sleep做循环间隔,导致停止延迟


❌while not stop: time.sleep(1),最多要等 1 秒才响应。 ✅while not stop_event.wait(timeout=1):,set()时立刻返回。


4. 守护线程里写关键数据


❌Thread(target=save_all, daemon=True),程序一退出数据就丢。 ✅ 关键任务设非守护并join(),或用Event通知其主动收尾。


5. 阻塞的recv/read上等不到停止信号


❌ 用默认无超时的连接,线程卡死无法响应。 ✅ 给 socket、文件、数据库操作都设超时,让循环有检查的机会。


6. 用join()当成「结束线程」


❌ 以为t.join()能让线程停下。 ✅join()只是等待,不做任何终止;不设timeout还可能永久阻塞。


7.join()之后不看is_alive()


❌t.join(1)后直接认为线程已停。 ✅join恒返回None,超时与否要看is_alive()。


8.start()之后才设daemon


❌t.start(); t.daemon = True——RuntimeError。 ✅ 构造时就写daemon=True。


总结




主题结论



创建线程Thread(target=...)或继承重写run(),用start()启动

正常结束run()返回或抛异常,线程自然结束

子线程sys.exit()只结束该线程,不影响其他线程和主进程

协作停止Event+ 循环检查,阻塞 I/O 要加超时

强制终止线程做不到;改用multiprocessing的terminate()

守护线程退出时被突然掐断,不做关键写操作



记住一句话就够了:线程的结束权在自己手上,外部只能请求、不能命令。把「停止」设计成一个能被检查的信号,再给所有阻塞操作留出超时,你的多线程程序才能干净地启动、也干净地退出。




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

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

立即咨询