Python多线程基础——04、threadinglocal类使用

发布时间:2026/10/23 4:55:00
Python多线程基础——04、threadinglocal类使用
摘要本文通过五个典型场景系统讲解 Python 中 threading.local 类的线程隔离机制。文章首先用全局对象在多线程下的累加实验说明其不安全性随后依次演示 threading.local 实例属性的定义、多线程同名属性的隔离、主线程与工作线程的属性隔离、跨线程访问属性报错以及实例属性在所属线程工作域内的全局有效性帮助读者理解并正确使用 threading.local 实现线程安全。目录一、全局对象的线程不安全性二、使用方法场景一定义threading.local的实例和实例属性场景二多线程操作同名threading.local对象属性是线程隔离的场景三主线程和工作线程操作同名属性是线程隔离的场景四从其他线程获取threading.local对象属性是错误的场景五threading.local的实例属性在定义它的线程工作域内是全局有效的一、全局对象的线程不安全性这里的不安全性体现在多个工作线程使用同一个全局对象各线程对这个全局对象的操作都是有效的这就是多线程的不安全性。class A: def __init__(self): self.x 0 g_data A() def worker(): start time.time() for i in range(100): time.sleep(0.0001) g_data.x 1 end time.time() logging.info(x{},{},{},{}.format(g_data.x, start, end, end - start)) for i in range(10): threading.Thread(targetworker, namew{}.format(i)).start() print( * 30) 2026-10-15 14:57:36,417 w3 3212 x988,1792047454.872863,1792047456.417175,1.5443120002746582 2026-10-15 14:57:36,417 w4 12996 x989,1792047454.8738782,1792047456.417546,1.5436677932739258 2026-10-15 14:57:36,417 w0 21096 x990,1792047454.871863,1792047456.417546,1.5456831455230713 2026-10-15 14:57:36,417 w6 6404 x992,1792047454.8738782,1792047456.417546,1.5436677932739258 2026-10-15 14:57:36,417 w5 6916 x993,1792047454.8738782,1792047456.417546,1.5436677932739258 2026-10-15 14:57:36,418 w2 21172 x995,1792047454.872863,1792047456.4181864,1.545323371887207 2026-10-15 14:57:36,418 w1 12832 x997,1792047454.872863,1792047456.4181864,1.545323371887207 2026-10-15 14:57:36,433 w8 15296 x998,1792047454.8748682,1792047456.4332154,1.558347225189209 2026-10-15 14:57:36,433 w7 22404 x999,1792047454.8748682,1792047456.4332154,1.558347225189209 2026-10-15 14:57:36,433 w9 5432 x1000,1792047454.8748682,1792047456.4332154,1.558347225189209上方案例中启动了10个线程分别对同一个全局对象g_data的属性x做100次累加每次累加之间都有一次I/O阻塞延迟此时操作系统切换线程执行最终我们类似于10个线程轮流累加1次这个过程循环100次此时我们就会看到主线程结束时每个线程在结束时刻获取属性x的值都接近10 * 1000。这是由python的线程模型决定的。线程模型原理查看往期文章Python并发编程基础——03、daemon线程和join()方法如果不添加I/O阻塞延迟操作呢class A: def __init__(self): self.x 0 g_data A() def worker(): start time.time() for i in range(10000): # time.sleep(0.0001) 我们将它注释掉 g_data.x 1 end time.time() logging.info(x{},{},{},{}.format(g_data.x, start, end, end - start)) for i in range(10): threading.Thread(targetworker, namew{}.format(i)).start() print( * 30)2026-10-15 15:07:16,133 w0 14460 x10000,1792048036.13145,1792048036.1334496,0.0019996166229248047 2026-10-15 15:07:16,134 w1 9552 x20000,1792048036.1334496,1792048036.1344578,0.0010082721710205078 2026-10-15 15:07:16,136 w2 5888 x30000,1792048036.1354597,1792048036.136463,0.001003265380859375 2026-10-15 15:07:16,138 w3 2448 x40000,1792048036.136463,1792048036.1384594,0.0019965171813964844 2026-10-15 15:07:16,139 w4 11336 x50000,1792048036.1384594,1792048036.1394591,0.0009996891021728516 2026-10-15 15:07:16,141 w5 24156 x60000,1792048036.1404645,1792048036.1414604,0.0009958744049072266 2026-10-15 15:07:16,143 w6 13504 x70000,1792048036.1414604,1792048036.1434605,0.002000093460083008 2026-10-15 15:07:16,145 w7 20688 x80000,1792048036.1434605,1792048036.1454604,0.0019998550415039062 2026-10-15 15:07:16,147 w8 20776 x90000,1792048036.1454604,1792048036.1474588,0.001998424530029297 2026-10-15 15:07:16,150 w9 5776 x100000,1792048036.148463,1792048036.1494603,0.000997304916381836 此时工作线程中没有I/O密集型操作了CPU密集型操作会占用CPU资源操作系统无法调度其他工作线程四舍五入相当于串行执行从打印结果的start和end时间戳可以看出它们的顺序确实是首尾相接的各线程结束时可读取到的属性x的值也仅限于本线程的累加结果。可见多线程对全局对象的操作行为是有效的和不可预期的。如果在工作线程任务中严格使用局部对象确实可以隔绝不同线程对对象的操作但是会涉及到复杂的局部变量传参。此时threading.local类的用途显现出来可以将threading.local对象的属性作为属性被定义时所处的线程的全局对象即属性仅在定义它的线程的作用域中是全局的在其父线程和子线程中都无效。二、使用方法场景一定义threading.local的实例和实例属性g_data threading.local() 创建threading.local对象 def worker(): g_data.y 0 创建对象属性 for i in range(300): g_data.y 1 logging.info(x{}.format(g_data.y)) threading.Thread(targetworker, nameworker).start()2026-10-15 15:24:58,937 worker 16460 x300你可能会觉得这不就工作线程相当于操作全局类对象的属性吗并没有体现出threading.local对象属性的作用域特殊性和在线程安全性中起到的特殊作用啊。请看场景二。场景二多线程操作同名threading.local对象属性是线程隔离的g_data threading.local() def worker(): g_data.y 0 for i in range(300): g_data.y 1 logging.info(x{}.format(g_data.y)) for i in range(10): threading.Thread(targetworker, namew{}.format(i)).start() print( * 30)2026-10-15 15:39:56,317 w0 19196 x300 2026-10-15 15:39:56,318 w1 27260 x300 2026-10-15 15:39:56,318 w2 8888 x300 2026-10-15 15:39:56,319 w3 16436 x300 2026-10-15 15:39:56,319 w4 16968 x300 2026-10-15 15:39:56,319 w5 23424 x300 2026-10-15 15:39:56,320 w6 26036 x300 2026-10-15 15:39:56,320 w7 25796 x300 2026-10-15 15:39:56,320 w8 14876 x300 2026-10-15 15:39:56,321 w9 15180 x300 此时如果你依旧认为多个工作线程操作了同一个全局类对象的属性就大错特错了如果真是那样每个线程中操作的g_data.y就是相等的最终打印结果应该是在前一个线程的累加结果上继续累加300600900... ...但是世界情况来看并没有。这也说明了每个线程的工作域下的同名g_data.y并不是相同的对象。为了更具说服力我们来看情况三。场景三主线程和工作线程操作同名属性是线程隔离的g_data threading.local() g_data.y 主线程中的y 主线程中定义类实例属性y def worker(): g_data.y 0 工作线程中定义或赋值同名类实例属性y for i in range(300): g_data.y 1 logging.info(x{}.format(g_data.y)) for i in range(10): threading.Thread(targetworker, namew{}.format(i)).start() print( * 30) time.sleep(5) 确保全部工作线程都结束 print(f此时运行中的线程有几个{threading.activeCount()}分别是{threading.enumerate()}) print(g_data.y)2026-10-15 15:51:30,593 w0 25664 x300 2026-10-15 15:51:30,593 w1 7128 x300 2026-10-15 15:51:30,594 w2 3008 x300 2026-10-15 15:51:30,594 w3 18552 x300 2026-10-15 15:51:30,595 w4 23448 x300 2026-10-15 15:51:30,595 w5 23848 x300 2026-10-15 15:51:30,595 w6 8260 x300 2026-10-15 15:51:30,596 w7 16596 x300 2026-10-15 15:51:30,596 w8 20472 x300 2026-10-15 15:51:30,596 w9 16244 x300 此时运行中的线程有几个1分别是[_MainThread(MainThread, started 17240)] 主线程中的y与场景二相比我们在主线程中定义了同名的类实例属性y并在全部子线程结束后打印该属性值。可见不同线程工作域下的同名threading.local对象属性确实是不相等的且尽在本线程工作域内有效。场景四从其他线程获取threading.local对象属性是错误的主线程试图获取还未结束的工作线程中的threading.loacal对象属性。g_data threading.local() def worker(): g_data.y 100 time.sleep(5) logging.info(5秒后工作线程结束x{}.format(g_data.y)) print( * 30) time.sleep(2) print(f此时运行中的线程有几个{threading.activeCount()}分别是{threading.enumerate()}) print(g_data.y) Traceback (most recent call last): File D:\11_学习\06_python_multi_tasks\05_threadinglocal类使用.py, line 76, in module print(g_data.y) AttributeError: _thread._local object has no attribute y 此时运行中的线程有几个2分别是[_MainThread(MainThread, started 20788), Thread(worker, started 17320)] 2026-10-15 15:58:16,434 worker 17320 5秒后工作线程结束x100执行结果中报错了AttributeError: _thread._local object has no attribute y。说明在主线程的工作域内threading.loacal的类实例g_data并没有属性g_data.y。场景五threading.local的实例属性在定义它的线程工作域内是全局有效的工作线程调用子函数子函数操作threading.local的实例属性。g_data threading.local() def child_worker(): g_data.y 1 def worker(): g_data.y 0 for i in range(300): g_data.y 1 child_worker() logging.info(x{}.format(g_data.y)) start time.time() for i in range(10): threading.Thread(targetworker, namew{}.format(i)).start() print( * 30)2026-10-15 16:02:13,234 w0 24744 x301 2026-10-15 16:02:13,235 w1 14996 x301 2026-10-15 16:02:13,235 w2 18852 x301 2026-10-15 16:02:13,236 w3 17916 x301 2026-10-15 16:02:13,236 w4 1444 x301 2026-10-15 16:02:13,236 w5 17256 x301 2026-10-15 16:02:13,237 w6 13104 x301 2026-10-15 16:02:13,237 w7 8616 x301 2026-10-15 16:02:13,237 w8 18092 x301 2026-10-15 16:02:13,237 w9 22876 x301 可见这种方式是线程隔离的不需要传参的说明threading.local的实例属性在所属工作线程中具有全局性。