异步锁优化错误
作者:互联网
更新:编辑标题以关注主要问题.请参阅我的答案以获取完整更新.
在以下代码中,a()和b()相同.它们每个从0到9同时计数,同时每2个计数获取并产生一个锁.
import asyncio
lock = asyncio.Lock()
def a ():
yield from lock.acquire()
for i in range(10):
print('a: ' + str(i))
if i % 2 == 0:
lock.release()
yield from lock.acquire()
lock.release()
def b ():
yield from lock.acquire()
for i in range(10):
print('b: ' + str(i))
if i % 2 == 0:
lock.release()
yield from lock.acquire()
lock.release()
asyncio.get_event_loop().run_until_complete(asyncio.gather(a(), b()))
print('done')
我期望交错输出,但是我得到:
b: 0
b: 1
b: 2
b: 3
b: 4
b: 5
b: 6
b: 7
b: 8
b: 9
a: 0
a: 1
a: 2
a: 3
a: 4
a: 5
a: 6
a: 7
a: 8
a: 9
done
似乎第二个收益实际上并没有收益,而是立即重新获得了锁并继续.
对我来说,这似乎是个错误.我对吗?还是有另一种解释?
下面的代码经过修改,并带有额外的初始“空转”产量,可以按预期工作.这使我相信锁定确实是公平的并且可能是正确的.
import asyncio
lock = asyncio.Lock()
def a ():
yield from lock.acquire()
yield from asyncio.sleep(0)
for i in range(10):
print('a: ' + str(i))
if i % 2 == 0:
lock.release()
yield from lock.acquire()
lock.release()
def b ():
yield from lock.acquire()
yield from asyncio.sleep(0)
for i in range(10):
print('b: ' + str(i))
if i % 2 == 0:
lock.release()
yield from lock.acquire()
lock.release()
asyncio.get_event_loop().run_until_complete(asyncio.gather(a(), b()))
print('done')
输出:
a: 0
b: 0
a: 1
a: 2
b: 1
b: 2
a: 3
a: 4
b: 3
b: 4
a: 5
a: 6
b: 5
b: 6
a: 7
a: 8
b: 7
b: 8
a: 9
b: 9
done
请注意,我在一开始只执行一次无操作产量,而不是每2次计数一次.但是这样做会导致第一段代码中每2个计数发生一次交错.
调度程序中只有一些优化(我认为是一个错误),在获得其他人没有等待的锁时并没有真正产生.
其他如何解释第一个输出?
解决方法:
更新:鉴于我对github问题的评论(link),以下内容已过时.该注释指出,您可以使用Lock.locked()来预测Lock.acquire()是否将屈服.它还观察到在快速情况下无法产生其他许多协程,因此甚至考虑修复所有协程是一个迷失的原因.最后,它涉及到如何解决其他问题,并建议可以更好地解决它.这就是对asyncio.nop()方法的请求,该方法只会让调度程序屈服,而不会做其他任何事情.他们没有添加该方法,而是决定重载asyncio.sleep(0)并对其进行“解优化”(在lock.acquire()讨论的上下文中),以在参数为0时产生给调度程序.
以下是原始答案,但已被上一段取代:
根本原因是asyncio.lock的实现在其first three lines中尝试变得过于聪明,并且如果没有服务员,则不会将控制权交还给调度程序:
if not self._locked and all(w.cancelled() for w in self._waiters):
self._locked = True
return True
但是,如我的第一个示例所示,这阻止了其他协程甚至成为服务员.他们只是没有机会跑到试图获取锁的地步.
低效的解决方法是始终在获取锁之前立即从asyncio.sleep(0)屈服.
这是低效的,因为在通常情况下,还会有其他服务员,并且获取锁也将控制权交还给调度程序.因此,在大多数情况下,您将两次将控制权交还给调度程序,这很糟糕.
另请注意,锁文档含糊不清地指出:“此方法将阻塞,直到锁被解锁,然后将其设置为已锁并返回True.”当然给人的印象是,它将在获得锁之前将控制权交给调度程序.
我认为,正确的做法是使锁实现始终屈服并且不要太聪明.另外,锁实现应具有一个方法,该方法告诉您是否在获取锁时将其屈服,以便您的代码在锁获取不会时可手动屈服.另一种选择是让acquire()调用返回一个值,该值告诉您是否实际产生了该值.这不太可取,但仍比现状好.
有人认为更好的解决方法可能是在release()时手动屈服.但是,如果您看到一个紧紧的循环,在工作完成后释放并重新获取,那么就等于是同一件事-在通常情况下,它仍然会产生两倍的收益,一次是在发布时,一次是在获取时,这会增加效率.
标签:python-3-6,python-3-x,concurrency,python-asyncio,python 来源: https://codeday.me/bug/20191026/1935755.html