python 并发编程系列(三)之线程同步
一、线程同步介绍
在Python中,线程同步指的是协调不同线程对共享资源的访问,以防止数据竞争和不一致的状态。通俗的讲就是当一个线程访问某些数据时,让其他线程不能访问这些数据,直到该线程完成对数据的操作。常用的同步机制包括锁(确保同一时刻只能有一个线程访问共享变量)、信号量(限制并发线程的数量)、事件、condition(让线程等待某个条件成立,然后才继续执行,同时还能保证对共享资源的互斥访问)和Barrier。例如考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。
那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。
锁有两种状态——锁定和未锁定。每当一个线程比如"set"要访问共享数据时,必须先获得锁定;如果已经有别的线程比如"print"获得锁定了,那么就让线程"set"暂停,也就是同步阻塞;等到线程"print"访问完毕,释放锁以后,再让线程"set"继续。
经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。
二、Lock
Python的threading模块提供了锁(Lock)作为最基本的线程同步机制,Lock是互斥锁。锁有两种状态,"locked"和"unlocked"。当多个线程要访问共享数据时,它们必须先获取锁,访问数据后再释放锁。只有一个线程可以获取锁,其他线程必须等待,直到锁被释放。2.1、Lock类API
acquire([blocking[, timeout]]):
尝试获取锁。
如果 blocking 为 True(默认),则会阻塞当前线程直到锁可用。
如果 blocking 为 False,则立即检查锁是否可用;如果不可用,则返回 False。
如果提供了 timeout 参数,则最多等待 timeout 秒,然后返回 False 如果锁仍未获得。
release():
释放锁。
如果锁尚未被当前线程获取,则抛出 RuntimeError。
locked():
判断锁是否被获取,如果锁已被获取,则返回 True;否则返回 False。
# 使用上下文管理器
为了防止在异常情况下忘记释放锁,推荐使用上下文管理器(即 with 语句),这可以确保即使在发生异常的情况下锁也能被正确释放:
with lock:
# 执行需要保护的代码
print("临界区内的代码")
例子:
# @Author: JIWEI.SUN
# @Date: 2024/9/3 13:45
import time
from threading import Thread, Lock, RLock, Event, Barrier
lock = Lock()
total = 0
class AddThread(Thread):
def __init__(self, name):
super().__init__(name=name)
def run(self) -> None:
global total
time_add_start = time.time()
for _ in range(10000000):
# lock.acquire()
# total += 1
# lock.release()
with lock:
total += 1
time_add_end = time.time()
print(f'add执行时间为;{time_add_end - time_add_start}')
class SubThread(Thread):
def __init__(self, name):
super().__init__(name=name)
def run(self) -> None:
global total
time_sub_start = time.time()
for _ in range(10000000):
with lock:
total -= 1
time_sub_end = time.time()
print(f'sub执行时间为:{time_sub_end - time_sub_start}')
sub_thread = SubThread('sub_thread')
add_thread = AddThread('add_thread')
time_main_start = time.time()
sub_thread.start()
add_thread.start()
sub_thread.join()
add_thread.join()
time_main_end = time.time()
print(f'总共执行时间为:{time_main_end - time_main_start}')
# 注意:加锁会增加程序时间,经测试,计算10000000,两个线程不加锁执行1秒,加锁执行5秒
三、RLock
在 Python 的 threading 模块中,RLock 类(递归锁)是一种特殊的锁类型,它允许一个线程多次获取同一个锁。这意味着,如果一个线程已经持有了一个 RLock,它还可以再次获取这个锁,而不会导致死锁。递归锁是通过在每次获取锁时增加一个计数器,每次释放锁时减少一个计数器来实现的。只有当计数器的值为零时,锁才会真正的被释放,这样其他线程才有可能获取到这个锁。
递归锁可以解决一些复杂的锁需求,例如一个函数在递归调用时需要获取锁,或者一个线程需要在不同的函数中获取同一个锁。但请注意,虽然递归锁可以使得代码更加灵活,但是它也使得代码更难理解,更难保证线程同步的正确性,因此应尽量避免使用递归锁,除非确实有需要。
3.1、API
acquire([blocking[, timeout]]):
尝试获取锁。
如果 blocking 为 True(默认),则会阻塞当前线程直到锁可用。
如果 blocking 为 False,则立即检查锁是否可用;如果不可用,则返回 False。
如果提供了 timeout 参数,则最多等待 timeout 秒,然后返回 False 如果锁仍未获得。
release():
释放锁。
如果锁尚未被当前线程获取,则抛出 RuntimeError。
如果锁的计数大于 1,释放一次锁,计数减一;如果计数为 1,则完全释放锁。
locked():
如果锁已被获取,则返回 True;否则返回 False
例子:
import threading
lock = threading.RLock()
count = 0
def recursive_function(n):
global count
with lock:
print(f"Thread {threading.current_thread().name} entering recursive_function({n})")
if n > 0:
recursive_function(n - 1)
count += 1
print(f"Thread {threading.current_thread().name} incremented count to {count}")
# 创建线程
thread1 = threading.Thread(target=recursive_function, args=(5,))
thread2 = threading.Thread(target=recursive_function, args=(5,))
# 启动线程
thread1.start()
thread2.start()
# 等待线程结束
thread1.join()
thread2.join()
print(f"最终 count 的值是: {count}")
四、信号量Semaphore
信号量(Semaphore)是一个更高级的线程同步机制,它维护了一个内部计数器,该计数器被acquire()调用减一,被release()调用加一。当计数器大于零时,acquire()不会阻塞。当线程调用acquire()并导致计数器为零时,线程将阻塞,直到其他线程调用release()。与Lock不同,信号量不是用来控制某一时刻只有一个线程访问共享资源的,它非常适合用于限制同时访问某个资源的线程数量,比如限制并发的网络请求、文件操作等。如果想控制某个线程同时并发不能超过多少个,这个很有用。
4.1、Semaphore类API
acquire([timeout]):
尝试获取一个许可。
如果 timeout 参数为正数,则最多等待 timeout 秒。如果超时后仍无法获取许可,则返回 False。
如果 timeout 参数为 None 或负数,则会一直等待直到获取到许可。
release():
释放一个之前获取的许可。如果当前信号量的计数为零,则表明有线程正在等待获取许可,此时释放许可会使一个等待的线程继续执行。
enter() 和 exit():
Semaphore 类实现了上下文管理协议,可以使用 with 语句自动管理许可的获取和释放。
# 信号量也可以使用with语句
例子:
# @Author: JIWEI.SUN
# @Date: 2024/9/3 16:44
import time
import random
from threading import Semaphore, BoundedSemaphore, Thread
# 创建一个信号量,允许最多5个线程同时执行
semaphore = Semaphore(value=5)
def worker(num):
# 请求获取一个许可
semaphore.acquire()
try:
print(f"线程 {num} 开始执行...")
time.sleep(random.randint(1, 3)) # 模拟一些耗时操作
print(f"线程 {num} 执行完毕!")
finally:
# 释放许可
semaphore.release()
# 创建多个线程
threads = []
for i in range(10):
t = Thread(target=worker, args=(i,))
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
print("所有线程执行完毕!")
4.2、BoundedSemaphore 类
BoundedSemaphore 类是 Semaphore 的一个子类,它增加了对信号量计数的边界检查。当使用 release() 方法时会+1,并且会检查信号量的上限情况,如果信号量的计数超过了初始值,BoundedSemaphore 会发出警告。这有助于检测错误的许可释放操作。五、Condition 条件变量
在 Python 的 threading 模块中,Condition 类是一种更高级的同步原语,它结合了锁和条件变量的功能。Condition 类允许一个或多个线程等待某个条件成立,同时还能保证对共享资源的互斥访问。Condition 类通常用于实现更复杂的同步逻辑,如生产者-消费者模式、多生产者-多消费者队列等。5.1、Condition类API
acquire() 和 release():
获取和释放底层的锁对象。通常不需要直接调用这些方法,而是通过 with 语句来自动管理锁的获取和释放。
wait([timeout]):
释放锁并等待直到被 notify() 或 notify_all() 唤醒,或者超时。
如果提供了 timeout 参数,则最多等待 timeout 秒。如果超时后仍没有被唤醒,则返回。
notify(n=1):
唤醒一个正在等待的线程(如果有的话)。如果有多个线程在等待,最多唤醒 n 个线程。默认值为 1。
notify_all():
唤醒所有正在等待的线程
# condition 类初始化:
Condition 类可以使用一个锁对象来初始化,如果没有提供锁对象,默认使用 RLock(递归锁)
# 默认使用RLock初始化
condition = threading.Condition()
# 或者使用自定义锁
lock = threading.Lock()
condition = threading.Condition(lock)
例子:
# @Author: JIWEI.SUN
# @Date: 2024/9/3 17:32
import threading
import time
import random
# 创建一个 Condition 对象
condition = threading.Condition()
queue = []
def producer():
for i in range(5):
with condition:
print(f"生产者生产了产品 {i},等待消费者...")
queue.append(i)
condition.notify() # 通知消费者
time.sleep(random.randint(1, 3)) # 模拟生产时间
def consumer():
for _ in range(5):
with condition:
while not queue: # 如果队列为空,等待
print("消费者发现队列为空,等待...")
condition.wait()
product = queue.pop(0)
print(f"消费者消费了产品 {product}!")
time.sleep(random.randint(1, 3)) # 模拟消费时间
# 创建生产者和消费者线程
producer_thread = threading.Thread(target=producer, name="Producer")
consumer_thread = threading.Thread(target=consumer, name="Consumer")
# 启动线程
producer_thread.start()
consumer_thread.start()
# 等待所有线程完成
producer_thread.join()
consumer_thread.join()
print("所有线程执行完毕!")
六、Barrier类
在 Python 的 threading 模块中,Barrier 类用于协调多个线程到达一个公共点(称为屏障),只有当所有参与的线程都到达这一点时,所有线程才能继续执行。Barrier 特别适用于需要同步多个线程到达某个特定点的情况,比如在并行计算或分布式系统中。
常用API
wait(timeout=None):
等待其他线程到达屏障。如果提供了 timeout 参数,则最多等待 timeout 秒。如果超时后仍有线程未到达屏障,则返回 -1。
abort():
中断屏障,使所有等待的线程抛出 BrokenBarrierError 异常。
reset():
重置屏障,清除所有到达屏障的记录,并允许线程重新进入屏障。
n_waiting:
返回当前正在等待的线程数量。
parties:
返回参与屏障的线程数量
例子:
# @Author: JIWEI.SUN
# @Date: 2024/9/3 17:41
import threading
import time
import random
# 创建一个 Barrier 对象,允许最多5个线程同时到达屏障
barrier = threading.Barrier(5)
def worker(id):
print(f"线程 {id} 开始执行...")
time.sleep(random.randint(1, 3)) # 模拟一些耗时操作
print(f"线程 {id} 到达屏障...")
try:
barrier.wait() # 等待其他线程到达屏障
print(f"线程 {id} 继续执行...")
except threading.BrokenBarrierError:
print(f"线程 {id} 捕获到 BrokenBarrierError")
# 创建多个线程
threads = []
for i in range(5):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
print("所有线程执行完毕!")
# 使用 Barrier 类的注意事项:
线程数量:
创建 Barrier 对象时指定的线程数量必须与实际参与的线程数量相匹配,否则可能会导致死锁。
异常处理:
如果某个线程在 wait() 方法中抛出了异常(例如 BrokenBarrierError),则其他线程也会受到影响。因此,在使用 Barrier 时需要妥善处理异常。
重置屏障:
如果需要重新开始同步过程,可以使用 reset() 方法来重置屏障。
动作函数:
可以在创建 Barrier 对象时指定一个动作函数,当所有线程到达屏障时,这个函数会被一个线程执行。这可以用于执行一些共同的操作。

浙公网安备 33010602011771号