Python中的asyncio.Lock()有什么作用?

64
是因为协程将来可能会被抢占吗?或者它允许在关键段使用yield from(我认为不应该鼓励这样做)?

3
这是一个锁,不确定它与优先级抢占有何关系。当您尝试获取锁时,您会明确地放弃控制权,希望在持有锁的情况下将其退还给您。这是为了确保一次只有一个协程可以进入关键部分。所有信息都在文档中。 - Benjamin Gruenbaum
2
我认为你要么没有仔细阅读官方文档,要么没有理解问题。 - user3761759
2个回答

115

你使用它的原因与在线程化代码中使用锁的原因相同:用于保护关键部分。 asyncio 主要用于单线程代码中,但仍然会发生并发执行(每当遇到 yield fromawait 时),这意味着有时需要同步。

例如,考虑一个从Web服务器获取数据并缓存结果的函数:

async def get_stuff(url):
    if url in cache:
        return cache[url]
    stuff = await aiohttp.request('GET', url)
    cache[url] = stuff
    return stuff

现在假设您有多个并发运行的协程,这些协程可能需要使用get_stuff的返回值:

async def parse_stuff():
    stuff = await get_stuff("www.example.com/data")
    # do some parsing

async def use_stuff():
    stuff = await get_stuff("www.example.com/data")
    # use stuff to do something interesting

async def do_work():
     out = await aiohttp.request("www.awebsite.com")
     # do some work with out
   

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(
    parse_stuff(),
    use_stuff(),
    do_work(),
))

现在,假设从 url 获取数据非常缓慢。如果同时运行 parse_stuffuse_stuff,每个方法都将承担通过网络获取 stuff 的全部成本。如果你使用锁来保护该方法,就可以避免这种情况:

stuff_lock = asyncio.Lock()

async def get_stuff(url):
    async with stuff_lock:
        if url in cache:
            return cache[url]
        stuff = await aiohttp.request('GET', url)
        cache[url] = stuff
        return stuff

需要注意的是,当一个协程在执行get_stuff函数并发起aiohttp调用时,而另一个协程在等待stuff_lock的情况下,第三个协程完全不需要调用get_stuff也可以运行,而不会被在Lock上阻塞的协程所影响。

显然,这个例子有些虚构,但希望它能让您了解为什么asyncio.Lock很有用;它允许您保护关键部分,而不会阻塞其他不需要访问该关键部分的协程运行。


9
感谢详细的解释。让我总结一下(为那些不理解问题的人)。1)协程无法被抢占 - 它们会一直运行,直到通过“yield from”将控制权返回给循环。2)asyncio.Lock() 用于保护调用“yield from”的关键部分 - 否则不需要使用该锁。讨论基于单线程 asyncio 使用模式的假设。 - user3761759
3
在这个特定的例子中(请记住这确实是一个人为制造的例子),如果您只是移除 yield from 并进行阻塞式 HTTP 请求,那么您不也会获得相同的性能吗?因为按照现在的锁定方式,只有一个 get_stuff() 实例会一直在进行中(也就是说,当 A 实例完全完成时,B 实例才能开始)。 - Nick Chammas
10
如果整个应用程序中唯一运行的协程都在等待 get_stuff,那么性能将是相同的。但如果有其他运行其他不需要锁的方法的协程,那么阻塞的 HTTP 请求会阻止所有这些其他协程,完全停止应用程序直到请求完成。使用 asyncio.Lock 可以让所有这些其他协程继续运行。 - dano
1
@SamanthaAtkins 我的答案解释了为什么有时这仍然是必要的。基本上,因为您可以同时运行多个协程,所以您仍然可能需要同步来防止它们同时进入关键部分。只需使用 yield fromawait 调用的关键部分就需要在 asyncio 中需要锁定。 - dano
1
@SamanthaAtkins 它们不能并行运行,但它们可以同时运行。再次说明我的答案给出了一种并发执行而不需要锁定的用例,这会导致意外行为。 - dano
显示剩余13条评论

4

一个例子是当你只想让某些代码运行一次,但是需要被多个人请求(例如在Web应用程序中)

async def request_by_many():
    key = False
    lock = asyncio.Lock()
    async with lock:
        if key is False:
            await only_run_once()

async def only_run_once():
    while True:
        if random()>0.5:
            key = True
            break
        await asyncio.sleep(1)

2
我认为要使这个工作起效,你需要实例化asyncio.Lock对象一次,并将其存储在每个调用者都可以访问的地方,例如作为某个共享对象的属性。目前的情况是,每次调用都会实例化一个新的锁,并且继续执行,就好像它不存在一样。 - Seb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接