Task.gather does not seem to work

Continuing my endeavour to convert my callbacks spaghetti code into something more readable, I’m stumbling against Panda3D implementation of gather()

In asyncio, gather() takes a list of coroutines or futures and returns a future that typically is awaited on.

AsyncFuture has a static method gather() that seems to do this job, but I can not make the whole thing work.

from direct.task.Task import gather

async def one():
    print("ONE")

async def two():
    print("TWO")

async def test():
    await gather(one(), two())
    print("THREE")

def add_task():
    print("CREATE")
    taskMgr.add(test(), 'test')
    print("DONE")

import direct.directbase.DirectStart

add_task()
base.run()

The output does not show any code from the coro one or two :

Using deprecated DirectStart interface.
Known pipe types:
CocoaGraphicsPipe
(all display modules loaded.)
CREATE
DONE

You should create a task for them, scheduling them with the task manager, and then treat the task like a future:

await gather(taskMgr.add(one()), taskMgr.add(two()))

I have never really given much thought about the behaviour for passing a coroutine to gather() directly. At some point I added await coro() directly as a useful shorthand for await taskMgr.add(coro()), but I suppose that gather() should apply the same logic.

Ah, in hindsight it´s obvious :slight_smile:

Asyncio.gather() automatically creates tasks when receiving coro or futures, and I took that for granted when using panda version of it.

It’s easy enough to fix. Do you think the fix for this should go in 1.10.9?

Thinking about it, I would say yes, tit would be useful to be in 1.10.9. If people like me simply apply what they have learned using asyncio, the change of behaviour would be unexpected.
Otherwise the documentation should perhaps describe the discrepencies between asyncio and panda3d ?

Hmm, one sticky point is that there’s no obvious way for Panda to know which task manager and task chain to schedule the task on. We could just pick “the default one” for both, which is apparently what asyncio does, and may be sufficient for most use cases, though I find it mildly inelegant.

Alternatively, we could deviate from asyncio here, and instead of scheduling the task in gather, do it in await; it would loop over the futures in a gathering future and make sure all of them are scheduled with whatever task manager is managing the task that you’re awaiting it from.

Do you have any thoughts? I’m almost wondering whether perhaps gather() should be a method on the task manager to make this less ambiguous.

The implementation of gather() in asyncio schedules the tasks either on the current loop, or, if there is no loop, on the default loop. If I translate this into Panda parlance, Task.gather() should schedule the task using the current taskChain, or the default taskChain if gather() is called from outside. Is my understanding correct ?

If gather can not find or use the current task chain, using the default one feels a bit dangerous. In my app, I have a couple of taskChain running, some of them in asynchronous threads, so I don’t want gather to implicitly trigger a thread switch.

Perhaps your suggestion of doing the scheduling in await instead of gather is the most fool proof solution.

But again, I’m only a vanilla user of asyncio (and now “async-panda”) so I’m maybe missing what are the practical impacts of each solution?

That’s not a bad idea, I can check what the currently running task on the thread is, though this does assume that gather() is being called from within a task. Hmm.

I checked in a fix:

It now schedules the coroutine with the currently running task’s chain and manager. If not called within a task, prints a warning—I figure it’s easy enough for a user to explicitly convert it into a task using taskMgr.add if they need to use it from outside a task.

Thanks for the suggestion!

Just tested and it works perfectly !

1 Like