前言threading.Thread是多线程编程的入口类但很多人只用了它的start()和join()对构造函数里那几个参数、run()与start()的分工、ident与native_id的区别、异常到底去了哪里都是模糊的。结果是遇到「线程静默退出」「异常看不见」「线程名对不上」这类问题时无从下手。本文把Thread这一个类拆开讲透构造签名、两种指定任务的方式、全部属性与方法、异常处理钩子以及threading模块级的几个辅助函数。所有签名均以 Python 官方文档为准示例以 CPython 3.8 及以上为基准涉及更高版本的改动会单独标注。一、构造签名与参数官方文档给出的签名是threading.Thread(groupNone, targetNone, nameNone, args(), kwargs{}, *, daemonNone, contextNone)逐个参数说清楚参数默认值说明groupNone保留参数必须为NonetargetNone线程要执行的可调用对象None表示什么也不做nameNone线程名省略时自动生成args()传给target的位置参数元组kwargs{}传给target的关键字参数字典daemonNone是否守护线程None表示继承当前线程contextNone上下文对象Python 3.14 新增不必手动传文档提醒这个构造函数应当始终用关键字参数调用也就是写Thread(targetf, args(1,))不要图省事按位置去传group。关于name省略时默认按Thread-N形式生成从 3.10 起如果指定了target还会把 target 的函数名并进去形如Thread-N (target)。另外要记住名字只是个标识允许重复没有任何语义。关于daemon初值继承自创建它的线程主线程不是守护线程所以在主线程里创建出来的线程默认都是daemonFalse。二、run()与start()的分工这是全篇最该记牢的一组关系start()只能调用一次负责真的开一个操作系统线程并让新线程去执行run()。重复调用抛RuntimeError。run()方法体才是任务本身。默认实现会去调用构造时传入的target。于是有两种指定任务的方式# 适用于 Python 3.8import threadingimport timedef work(n):time.sleep(0.1)print(f[target 方式] 处理 {n})# 方式一传 targett1 threading.Thread(targetwork, args(1,), namevia-target)t1.start()# 方式二继承并重写 runclass Worker(threading.Thread):def __init__(self, n):super().__init__(namevia-subclass)self.n ndef run(self):time.sleep(0.1)print(f[subclass 方式] 处理 {self.n})t2 Worker(2)t2.start()t1.join()t2.join()官方文档明确子类里只应重写__init__()和run()其他方法都不要覆盖重写__init__()时第一件事必须是调用父类构造。还有一个高频错误是直接调t.run()——那只是在当前线程里把一个方法跑一遍压根没有新线程。run()里抛出的未捕获异常不会让主线程崩溃它会交给threading.excepthook()处理。三、属性与方法清单成员类型说明start()方法启动线程每个对象至多一次run()方法线程体可重写join(timeoutNone)方法阻塞等待结束恒返回Noneis_alive()方法是否存活run()开始前到结束后这段时间为Truename属性线程名仅作标识daemon属性布尔值必须在start()前设置ident属性线程标识符未启动时为Nonenative_id属性OS 分配的线程 ID3.8 新增isDaemon()/setDaemon()方法自 3.10 起废弃改用daemongetName()/setName()方法自 3.10 起废弃改用nameident和native_id的区别值得单独说。ident是 Python 层的线程标识是非零整数线程退出后这个值仍可读取且可能被回收给后来创建的线程。native_id是操作系统内核分配的线程 ID非负整数线程没启动时是None在系统范围内唯一直到线程结束。两者都不能当作全局唯一键长期保存。join()有三个容易记错的点永远返回None别指望它把结果带回来。想知道是正常结束还是超时得在join(timeout)之后再查is_alive()。对当前线程自己调join()会抛RuntimeError那是自锁死。对还没start()的线程调join()也抛RuntimeError。四、异常去了哪里threading.excepthookrun()里未捕获的异常不会像主线程那样打印完整的调用方栈并终止程序它会走一个专门的钩子。默认实现会忽略SystemExit并把其他异常打印到标准错误。可以自己接管这个钩子做统一日志# 适用于 Python 3.8import threadingimport tracebackdef my_hook(args):# args 有 exc_type / exc_value / exc_traceback / thread 四个属性print(f线程 {args.thread.name} 出错了, flushTrue)traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback)threading.excepthook my_hookdef boom():raise ValueError(线程里的意外错误)t threading.Thread(targetboom, nameboom-thread)t.start()t.join()print(主线程继续执行没有被异常带走)args的四个属性exc_type、exc_value、exc_traceback、thread来自官方文档定义的ExceptHookArgs。注意异常被这个钩子吞掉了调用方不会收到所以用了线程池时更要通过Future.result()把异常取回来。五、模块级辅助函数与local除了Thread类threading模块还提供函数作用threading.current_thread()返回当前线程对象threading.main_thread()返回主线程对象threading.enumerate()返回所有存活线程的列表含守护线程threading.active_count()存活线程数量等于enumerate()的长度threading.get_ident()当前线程的identthreading.get_native_id()当前线程的native_idthreading.local()线程本地存储threading.local()解决的是「每个线程各有一份同名变量」的问题。它的属性在不同线程里互不干扰# 适用于 Python 3.8import threadingimport timedata threading.local()def process(user):data.user user # 只写进本线程自己的那份time.sleep(0.1)print(f线程 {threading.current_thread().name} 看到 user {data.user})ts [threading.Thread(targetprocess, args(fu{i},), nameft{i}) for i in range(3)]for t in ts:t.start()for t in ts:t.join()如果data换成普通全局变量三个线程会互相覆盖用threading.local()之后每个线程读到的都是自己写进去的那份。数据库连接、请求上下文这类「每个线程一份」的对象正是它的典型用途。六、一个综合示例# 适用于 Python 3.8import threadingimport timedef worker(lock, results, idx):with lock:results.append((idx, threading.current_thread().name,threading.get_ident()))time.sleep(0.05)lock threading.Lock()results []threads [threading.Thread(targetworker, args(lock, results, i),namefworker-{i}) for i in range(4)]for t in threads:t.start()for t in threads:t.join()for item in sorted(results):print(item)print(活跃线程数不含已结束的, threading.active_count())print(主线程, threading.main_thread().name)这里用lock保护results.append因为列表的追加虽然通常安全但读改写在并发下仍应显式同步。active_count()在join()之后只会剩下主线程自己。常见坑点1. 直接调用run()而不调用start()❌t.run()—— 在当前线程里同步执行完全没有并发。 ✅t.start()。2. 对同一个线程对象重复start()❌t.start(); t.join(); t.start()抛RuntimeError。 ✅ 每个对象只能启动一次要重跑请新建对象。3.join()的返回值当成任务结果❌res t.join()然后print(res)永远得到None。 ✅ 结果通过共享容器、queue.Queue或Future.result()取回。4. 忘了join之后还要查is_alive❌t.join(1)之后默认线程一定已经结束。 ✅join返回None超时与否只能靠is_alive()判断。5. 在线程自己身上调join()❌ 线程里写threading.current_thread().join()——RuntimeError。 ✅join只能由别的线程调用。6. 还在用setDaemon()/getName()❌t.setDaemon(True)、t.getName()3.10 起会给DeprecationWarning。 ✅ 用t.daemon True必须在start()前和t.name。7. 把ident当全局唯一 ID 长期保存❌ 用t.ident做字典键缓存数据以为永不重复。 ✅ident会在线程结束后被回收复用需要稳定标识请自己生成uuid。8. 以为子线程异常会终止主程序❌ 指望子线程里抛异常能中断整个程序。 ✅ 未捕获异常交给threading.excepthook主程序照常需要感知异常就用线程池的Future.result()。总结成员关键结论Thread(...)用关键字参数args是元组、kwargs是字典start()只调用一次负责开线程run()线程体可重写不要直接调用join()恒返回None只等待不终止is_alive()判断是否存活配join(timeout)用ident/native_idPython 标识 / OS 标识都会被回收daemon必须在start()前设置excepthook接管子线程未捕获异常local()每个线程一份的本地存储把Thread的这些成员分清「谁负责启动、谁负责等待、谁负责传递结果、异常去了哪」多线程代码就不再是一团黑箱。下一步再学Lock、Event、Queue串起来就是一套完整可用的并发工具箱。