Python多线程同步机制详解:互斥锁、队列与ThreadLocal
本文深入解析Python多线程开发中的同步问题,通过实例演示线程不安全现象及互斥锁解决方案。详细阐述生产者与消费者模式如何利用Queue模块实现解耦,并介绍ThreadLocal在隔离线程局部数据中的应用,帮助开发者构建高并发下的稳定应用。
多线程同步基础与线程不安全现象
多线程开发场景下,开发者常需面对资源竞争带来的挑战。
设想两个线程t1和t2同时对初始值为0的变量num执行加1操作,各自循环执行1000000次。理论上,num的最终值应为2000000。然而,由于多线程并发访问,实际运行结果往往偏离预期,可能出现如下情况:
from threading import Thread
import time
num = 0
def test1():
global num
for i in range(1000000):
num += 1
print("--test1--num=%d" % num)
def test2():
global num
for i in range(1000000):
num += 1
print("--test2--num=%d" % num)
if __name__ == '__main__':
Thread(target=test1).start()
Thread(target=test2).start()
print("num = %d" % num)
"""
num = 134116
--test1--num=1032814
--test2--num=1166243
"""
尽管每次运行的结果可能不同,但极少恰好等于2000000。造成这一现象的根本原因在于缺乏对多线程访问同一共享资源的控制,导致数据被意外破坏,使得线程执行结果具有不确定性。这种现象在计算机科学中被称为“线程不安全”。
使用互斥锁实现线程同步
为解决上述问题,需引入线程同步机制,其中互斥锁是基础手段。
当多个线程共同修改同一数据时,若不加限制,结果将不可预测。为确保数据正确性,必须对线程访问进行同步控制。在Python中,Thread模块提供的Lock和Rlock对象可实现简单的线程同步。这两个对象均具备acquire(获取锁)和release(释放锁)方法。对于每次仅允许一个线程操作的共享数据,应将相关代码逻辑置于acquire和release方法调用之间。
基于互斥锁机制,对上述累加示例进行修正后的代码实现如下:
from threading import Thread, Lock
import time
num = 0
def test1():
global num
# 上锁
mutex.acquire()
for i in range(1000000):
num += 1
# 解锁
mutex.release()
print("--test1--num=%d" % num)
def test2():
global num
mutex.acquire()
for i in range(1000000):
num += 1
mutex.release()
print("--test2--num=%d" % num)
start_time = time.time() # 开始时间
# 创建一把互斥锁,默认没有上锁
mutex = Lock()
p1 = Thread(target=test1)
p1.start()
# time.sleep(3) # 取消屏蔽之后 再次运行程序,结果会不一样,,,为啥呢?
p2 = Thread(target=test2)
p2.start()
p1.join()
p2.join()
end_time = time.time() # 结束时间
print("num = %d" % num)
print("运行时间:%fs" % (end_time - start_time)) # 结束时间-开始时间
"""
输出结果:
--test1--num=1000000
--test2--num=2000000
num = 2000000
运行时间:0.287206s
"""
线程有序执行与生产者消费者模式
同步机制的另一典型应用场景是实现多个线程的有序执行。
from threading import Lock, Thread
from time import sleep
class Task1(Thread):
def run(self):
while True:
# 判断是否上锁成功,返回值为bool类型
if lock1.acquire():
print("--task1--")
sleep(0.5)
lock2.release()
class Task2(Thread):
def run(self):
while True:
if lock2.acquire():
print("--task2--")
sleep(0.5)
lock3.release()
class Task3(Thread):
def run(self):
while True:
if lock3.acquire():
print("--task3--")
sleep(0.5)
lock1.release()
if __name__ == '__main__':
# 创建一把锁
lock1 = Lock()
# 创建一把锁,并且锁上
lock2 = Lock()
lock2.acquire()
# 创建一把锁,并且锁上
lock3 = Lock()
lock3.acquire()
t1 = Task1()
t2 = Task2()
t3 = Task3()
t1.start()
t2.start()
t3.start()
"""
--task1--
--task2--
--task3--
--task1--
--task2--
--task3--
--task1--
--task2--
...
"""
在生产者和消费者模式中,同步机制发挥着关键作用。
探讨为何需要采用生产者和消费者模式。
在线程模型中,生产者指负责生成数据的线程,消费者指负责处理数据的线程。在多线程开发中,若生产者的处理速度远快于消费者,生产者将被迫等待消费者处理完毕才能继续生产;反之,若消费者速度快于生产者,消费者则需等待生产者生成数据。为平衡这种速度差异,引入了生产者和消费者模式。
深入理解生产者消费者模式的核心定义。
该模式通过引入一个共享容器来解决生产者和消费者之间的强耦合问题。双方不直接通信,而是通过阻塞队列进行交互。生产者生成数据后直接放入阻塞队列,无需等待消费者处理;消费者则直接从阻塞队列中获取数据,而非向生产者索取。阻塞队列在此充当缓冲区,有效平衡了生产者和消费者的处理能力差异。
Python标准库中的Queue模块提供了多种同步且线程安全的队列类,包括FIFO(先进先出)的Queue、LIFO(后进先出)的LifoQueue以及PriorityQueue(优先级队列)。这些队列内部实现了锁原语(即原子操作,确保操作要么不执行,要么完整执行),可直接在多线程环境中使用。利用队列可实现线程间的高效同步。
以下代码展示了如何使用FIFO队列实现前述的生产者与消费者问题:
import threading
import time
from queue import Queue
class Producer(threading.Thread):
def run(self):
global queue
count = 0
while True:
if queue.qsize() < 1000:
for i in range(100):
count += 1
msg = "生成产品" + str(count)
queue.put(msg)
print(msg)
time.sleep(0.5)
class Consumer(threading.Thread):
def run(self):
global queue
while True:
if queue.qsize() > 100:
for i in range(3):
msg = self.name + "消费了" + queue.get()
print(msg)
time.sleep(0.5)
ThreadLocal与线程局部数据管理
ThreadLocal是Python中用于处理线程局部数据的特殊对象。
在多线程环境中,每个线程拥有独立的数据空间。使用局部变量优于全局变量,因为局部变量仅对当前线程可见,不会影响其他线程,而修改全局变量则必须加锁。ThreadLocal解决了参数在单个线程内多个函数间传递的问题,同时避免了全局变量的竞争。
import threading
"""
⼀个ThreadLocal变量虽然是全局变量,但每个线程都只能读写⾃⼰线程的独
⽴副本,互不⼲扰。
"""
# 创建全局ThreadLocal对象:
local_school = threading.local()
def process_student():
# 获取当前线程关联的student:
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student:
local_school.student = name
process_student()
t1 = threading.Thread(target=process_thread, args=('dongGe',), name="Thread-A")
t2 = threading.Thread(target=process_thread, args=('⽼王',), name="Thread-B")
t1.start()
t2.start()
以上内容涵盖了Python多线程同步的主要技术点,希望能为读者的学习提供有益参考。欢迎持续关注本站以获取更多信息。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









