Is there any way to guarantee asyncio.Task will be

2019-08-13 03:03发布

问题:

import asyncio


l = asyncio.Lock()

async def test():
    print('locked' if l.locked() else 'unlocked')

    await l.acquire()
    # await asyncio.ensure_future(l.acquire())

    await asyncio.sleep(1)
    l.release()

async def main():
    await asyncio.gather(test(), test())

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

We start two test() coroutines, first of them immediately locks Lock by l.acquire(), second prints locked state. Output:

unlocked
locked

Everything changes if you will comment await l.acquire() line and uncomment next one. Output will be:

unlocked
unlocked

It happens because l.acquire() wrapped in Task starts after second test() was started.

Is there any way to make l.acquire() Task to be started as soon as possible, before second test() (and to get same output as in the original code)?

回答1:

Looks like I found solution. We need global lock that would suspend second task locked checking while first task starts:

import asyncio


l = asyncio.Lock()
check_lock = asyncio.Lock()

async def test():
    async with check_lock:
        print('locked' if l.locked() else 'unlocked')
        await asyncio.ensure_future(l.acquire())
        print('now', 'locked' if l.locked() else 'unlocked')

    await asyncio.sleep(1)
    l.release()

async def main():
    await asyncio.gather(test(), test())

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Output:

unlocked
now locked
locked
# 1 second delay here
now locked

But here we get another problem: second task would be suspended while first task completely done, that's why we get 1 second delay before second locking. It was hard to see in original example (because test() wait for single lock), that's why I added second printing. But it can be important for multiple locking of different resources to start coroutine immediately: we should lock only creating task, but not it's awaiting. Creating task itself wouldn't start that task (and l.acquire()) immediately, we should return control to event loop. It can be done by await asyncio.sleep(0). Here's final solution for original code:

import asyncio


l = asyncio.Lock()
check_lock = asyncio.Lock()

async def test():
    async with check_lock:
        print('locked' if l.locked() else 'unlocked')
        task = asyncio.ensure_future(l.acquire())  # only create task
        await asyncio.sleep(0)  # return control to event loop, it allows lock to be locked before task completed
        print('now', 'locked' if l.locked() else 'unlocked')  # would be printed immediately

    await task  # and now we can await for task done
    await asyncio.sleep(1)
    l.release()

async def main():
    await asyncio.gather(test(), test())

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Output:

unlocked
now locked
locked
# no delay here
now locked