asyncio中的锁定优化错误

时间:2017-01-17 23:03:23

标签: python python-3.x concurrency python-asyncio python-3.6

更新:编辑标题以关注主要问题。请参阅我的回答以获取完整更新。

在以下代码中,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

似乎第二个yield实际上并没有屈服,而是立即重新获取锁定并继续。

这对我来说似乎是个错误。我对吗?还是有其他解释?

以下代码,使用额外的初始修改" noop" yield,按预期正常工作。这让我相信锁定确实是公平的,也许是正确的。

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次计数。然而,这样做会导致第一段代码中预期的每两个计数交错。

在获取没有其他人等待的锁时,调度程序中只有一些优化(我认为是一个错误)并不是yield

如何解释第一个输出?

2 个答案:

答案 0 :(得分:3)

更新:根据我对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

然而,正如我的第一个例子所示,这可以防止其他协同程序甚至成为服务员。他们没有机会跑到他们试图获得锁定的地步。

inefficent 解决方法是在获取锁定之前立即

这是低效的,因为在通常情况下会有其他服务员并且获取锁定也会将控制权交还给调度程序。因此,在大多数情况下,您将控制回到调度程序两次,这很糟糕。

另请注意,锁文档含糊不清地说:"此方法会阻塞,直到锁解锁,然后将其设置为锁定并返回True。"当然会给人一种印象,即在获得锁定之前它会对调度程序产生控制权。

在我看来,正确的做法是让锁实现始终产生而不是太聪明。 或者,锁实现应该有一个方法,告诉你如果获得它是否会产生,以便你的代码可以手动屈服,如果锁获取赢了。 另一种选择是让yield from asyncio.sleep(0)调用返回一个值,告诉您它是否实际产生了。这不太可取,但仍比现状好。

有人可能认为更好的解决方法可能是在acquire()时手动收益。但是,如果你看一个在工作之后释放并重新获得的紧密循环,那么它就相同 - 在通常的情况下它仍会产生两次,一次是在发布时,再次是在获取时间,增加效率低下。 / p>

答案 1 :(得分:1)

目前还不清楚您要实现的目标,但似乎Lock不是您需要的工具。要交错python代码,您可以这样做:

def foo(tag, n):
    for i in range(n):
        print("inside", tag, i)
        yield (tag, i)


print('start')

for x in zip(foo('A', 10), foo('B', 10)):
    print(x)

print('done')

无需asynciothreading。无论如何,没有IO的asyncio没有多大意义。

threading Lock用于同步程序的关键部分,否则这些部分在独立线程中运行。当一个协同程序等待时,asyncio.Lock将允许其他协同程序继续使用IO:

import asyncio
import random

lock = asyncio.Lock()


async def foo(tag):
    print(tag, "Start")
    for i in range(10):
        print(tag, '>', i)
        await asyncio.sleep(random.uniform(0.1, 1))
        print(tag, '<', i)

    async with lock:
        # only one coroutine can execute the critical section at once.
        # other coroutines can still use IO.
        print(tag, "CRITICAL START")
        await asyncio.sleep(1)
        print(tag, "STILL IN CRITICAL")
        await asyncio.sleep(1)
        print(tag, "CRITICAL END")

    for i in range(10, 20):
        print(tag, '>', i)
        await asyncio.sleep(random.uniform(0.1, 1))
        print(tag, '<', i)

    print(tag, "Done")


print('start')

loop = asyncio.get_event_loop()
tasks = asyncio.gather(foo('A'), foo('B'), foo('C'))
loop.run_until_complete(tasks)
loop.close()

print('done')

请注意,关键字yield并不总是遵循 yield 的英文含义:-)。

你可以看到async with lock立即获得锁定而不等待其他协同程序做更多工作更有意义:到达关键部分的第一个couroutine应该开始运行它。 (即在await asyncio.sleep(0)之前添加async with lock:只是没有任何意义。)