加锁,在并发编程里,那可太重要了!不然,数据乱套了,那可就麻烦大了。Python 里,加锁其实挺方便的,但门道也不少,一不小心,死锁了,那真是欲哭无泪。
最常用的,肯定是 threading.Lock
了。创建一个 Lock
对象,然后用 acquire()
获取锁,用 release()
释放锁。就这么简单吗?当然没这么简单!
“`python
import threading
lock = threading.Lock()
def my_function():
lock.acquire()
try:
# 这里是需要保护的代码
print(“线程获取了锁”)
finally:
lock.release()
创建并启动线程
thread1 = threading.Thread(target=my_function)
thread2 = threading.Thread(target=my_function)
thread1.start()
thread2.start()
“`
这段代码,两个线程会竞争同一个锁,保证了 my_function
里面的代码同一时间只有一个线程在执行。但是,你看那个 finally
没?一定要有!不然,万一 try
里面的代码出错了,锁没释放,那就死锁了!血的教训啊!
with
语句,强烈推荐使用!它能自动帮你 acquire()
和 release()
锁,代码更简洁,也更安全。
“`python
import threading
lock = threading.Lock()
def my_function():
with lock:
# 这里是需要保护的代码
print(“线程获取了锁(使用 with 语句)”)
创建并启动线程
thread1 = threading.Thread(target=my_function)
thread2 = threading.Thread(target=my_function)
thread1.start()
thread2.start()
“`
用了 with
,就再也不用担心忘记释放锁了,代码也看着更舒服。简直是神器!
除了 Lock
,Python 还有 RLock
,也就是可重入锁。啥叫可重入?就是同一个线程可以多次 acquire()
同一个 RLock
,但是也要 release()
相应次数才能真正释放锁。
“`python
import threading
lock = threading.RLock()
def recursive_function(n):
with lock:
if n > 0:
print(f”线程递归调用,n = {n}”)
recursive_function(n – 1)
创建并启动线程
thread = threading.Thread(target=recursive_function, args=(3,))
thread.start()
“`
如果不用 RLock
,用普通的 Lock
,那这段代码直接就死锁了,因为同一个线程试图再次 acquire()
已经持有的锁。所以,递归调用,或者在一个线程里多次需要获取同一个锁,那必须用 RLock
。
再说说 Condition
,这个就更高级了,它基于 Lock
,提供了 wait()
和 notify()
方法,可以实现线程之间的等待和通知。简单来说,就是线程可以等待某个条件成立,然后被其他线程唤醒。
“`python
import threading
import time
condition = threading.Condition()
items = []
def producer():
with condition:
for i in range(5):
time.sleep(1) # 模拟生产过程
items.append(i)
print(f”生产者添加了物品 {i}”)
condition.notify() # 通知消费者
def consumer():
with condition:
while True:
if not items:
print(“消费者等待物品…”)
condition.wait() # 等待生产者通知
item = items.pop(0)
print(f”消费者消费了物品 {item}”)
创建并启动线程
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
“`
这段代码,生产者生产物品,消费者消费物品。如果物品为空,消费者就等待,直到生产者生产了物品并通知它。这在多线程编程里非常常见,比如消息队列、生产者消费者模型等等。
还有 Semaphore
,信号量。它维护一个计数器,acquire()
会让计数器减 1,release()
会让计数器加 1。计数器为 0 时,acquire()
会阻塞。这可以用来限制同时访问某个资源的线程数量。
“`python
import threading
import time
semaphore = threading.Semaphore(2) # 允许最多两个线程同时访问
def my_function():
with semaphore:
print(f”线程 {threading.current_thread().name} 获取了信号量”)
time.sleep(2) # 模拟访问资源
print(f”线程 {threading.current_thread().name} 释放了信号量”)
创建并启动线程
threads = []
for i in range(5):
thread = threading.Thread(target=my_function, name=f”Thread-{i}”)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
“`
这个例子里,最多只有两个线程能同时执行 my_function
里面的代码。其他线程必须等待,直到有线程释放了信号量。
说到死锁,那是并发编程里最头疼的问题之一。简单来说,就是两个或多个线程互相等待对方释放锁,导致所有线程都无法继续执行。避免死锁的方法有很多,比如:
- 避免循环等待: 线程按照固定的顺序获取锁。
- 设置超时时间:
acquire()
可以设置超时时间,如果超时了还没获取到锁,就放弃。 - 使用死锁检测工具: 一些工具可以自动检测死锁。
其实,最关键的还是设计好你的代码,避免不必要的锁竞争。锁用得越多,死锁的风险就越高。能不用锁解决问题,尽量不用锁。
Python 的 multiprocessing
模块也提供了锁,但是它用于进程间的同步,而不是线程间的同步。进程间的锁,其实是操作系统级别的锁,比线程间的锁开销更大,但是更安全,因为进程之间是独立的,一个进程崩溃了,不会影响其他进程。
总而言之,Python 加锁是个很有意思的话题,但是一定要小心谨慎。理解各种锁的特性,避免死锁,才能写出高效、稳定的并发程序。多写代码,多踩坑,才能真正掌握加锁的精髓。毕竟,纸上得来终觉浅,绝知此事要躬行嘛!而且,并发编程这玩意儿,不亲手试试,永远体会不到那种“玄学”的滋味儿!
评论(0)