欢迎访问 生活随笔!

生活随笔

当前位置: 首页 >

python线程同步锁_[python] 线程间同步之Lock RLock

发布时间:2025/3/11 38 豆豆
生活随笔 收集整理的这篇文章主要介绍了 python线程同步锁_[python] 线程间同步之Lock RLock 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

为什么需要同步

同样举之前的例子,两个线程分别对同一个全局变量进行加减,得不到预期结果,代码如下:

total = 0

def add():

global total

for i in range(1000000):

total += 1

def desc():

global total

for i in range(1000000):

total -= 1

import threading

thread1 = threading.Thread(target=add)

thread2 = threading.Thread(target=desc)

thread1.start()

thread2.start()

thread1.join()

thread2.join()

print(total)

原因就是因为 += 和 -=并不是原子操作。可以使用dis模块查看字节码:

import dis

def add(total):

total += 1

def desc(total):

total -= 1

total = 0

print(dis.dis(add))

print(dis.dis(desc))

# 运行结果如下:

# 3 0 LOAD_FAST 0 (total)

# 3 LOAD_CONST 1 (1)

# 6 INPLACE_ADD

# 7 STORE_FAST 0 (total)

# 10 LOAD_CONST 0 (None)

# 13 RETURN_VALUE

# None

# 5 0 LOAD_FAST 0 (total)

# 3 LOAD_CONST 1 (1)

# 6 INPLACE_SUBTRACT

# 7 STORE_FAST 0 (total)

# 10 LOAD_CONST 0 (None)

# 13 RETURN_VALUE

# None

可以看到 add()函数虽然其中只有一行代码,但是字节码主要分为四个步骤:

load 变量total

load 常量 1

执行加法操作

对total进行赋值

同理,desc()函数的步骤相同,只是第三步改为执行减法。

假设一种极端情况,开始total = 0,首先线程1 load 变量total,得到值为0,切换到线程2,同样的到total为0,再次切换线程1 load常量1,执行加法,给total赋值得到1;然后线程2也 laod常量1,执行减法,给total赋值为-1。最终total为-1,而不是预期的0。

期望中,必须在+=操作结束后,才能执行-=,所以线程同步的需求就出来了。

互斥锁Lock

threading模块中提供了threading.Lock类(互斥锁),基本用法如下:

import threading

lock = threading.Lock()

lock.acquire() # 获取锁

# dosomething…… # 临界区的代码只能被同时只能被一个线程运行

lock.release() # 释放锁

将上面的代码修改,即可得到正确结果:

import threading

total = 0

lock = threading.Lock()

def add(lock):

global total

for i in range(1000000):

lock.acquire()

total += 1

lock.release()

def desc(lock):

global total

for i in range(1000000):

lock.acquire()

total -= 1

lock.release()

thread1 = threading.Thread(target=add, args=(lock,))

thread2 = threading.Thread(target=desc, args=(lock,))

thread1.start()

thread2.start()

thread1.join()

thread2.join()

print(total)

# 执行结果 0

可以看到,添加互斥锁之后,程序执行结果是正确的,但是用了互斥锁之后,同样有一些缺陷:

添加锁之后,会影响程序性能。

可能引起"死锁"。

死锁主要在两种情况下发生:

迭代死锁

一个线程“迭代”请求同一个资源 ,会造成死锁。

lock = threading.Lock()

lock.acquire()

lock.acquire()

total += 1

lock.release()

lock.release()

上例种,第一次请求资源后还未 release ,再次acquire,最终无法释放,造成死锁 。(可通过可重入锁解决这个问题)。

互相调用死锁

两个线程中都会调用相同的资源,互相等待对方结束的情况 。假设A线程需要资源a,b,B线程也需要资源a,b,而线程A先获取资源a后,再获取资源b, B线程先获取资源b,再获取资源a。在A线程获取资源a,B线程获取资源b后,A线程在等待B线程释放资源b,而B线程在等待A线程释放资源a,从而死锁就发生了。

import threading

import time

lock_a = threading.Lock()

lock_b = threading.Lock()

def func1():

global lock_a

global lock_b

lock_a.acquire()

time.sleep(1)

lock_b.acquire()

time.sleep(1)

lock_b.release()

lock_a.release()

def func2():

global lock_a

global lock_b

lock_b.acquire()

time.sleep(1)

lock_a.acquire()

time.sleep(1)

lock_a.release()

lock_b.release()

thread1 = threading.Thread(target=func1)

thread2 = threading.Thread(target=func2)

thread1.start()

thread2.start()

thread1.join()

thread2.join()

print("program finished")

# 程序会陷入死循环

这个例子比较重要,开始理解错了,如果B线程获取了资源b,然后释放之后再获取资源a,这样是不会发生死锁的。只有在B线程获取了资源b,还没有释放的时候,获取了资源a,才会发生死锁。

可重入锁RLock

为解决同一线程种不能多次请求同一资源的问题,python提供了“可重入锁”:threading.RLock,RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源 。用法和threading.Lock类相同。

将上面迭代死锁的代码改写一下,就不会发生死锁,但注意,调用acquire和release的次数必须相等。

lock = threading.RLock()

lock.acquire()

lock.acquire()

total += 1

lock.release()

lock.release()

一般不会写这么无聊的代码,但是有一种情况是可能发生的,在加锁区域调用了某个函数,而这个函数内部又申请了同样的资源。

lock = threading.RLock()

def dosomething(lock):

lock.acquire()

# do something

lock.release()

lock.acquire()

dosomething(lock)

lock.release()

总结

线程间访问同一变量需要同步。

线程间加锁会导致性能损失。

加锁可能产生死锁,迭代死锁和互相调用死锁。

可重入锁可以避免迭代死锁。

参考

总结

以上是生活随笔为你收集整理的python线程同步锁_[python] 线程间同步之Lock RLock的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。