About asyncio loop handling in BlenderCloud addon #49275

Closed
opened 2016-09-06 18:21:22 +02:00 by Bastien Montagne · 7 comments

So, have been banging my head against the issue of running asyncio's loop as a sub-loop of Blender one, trying to understand how the kick_async_loop() can actually work - and failing so far.

As far as I understand, asyncio loop is run by the addon that way:

  • Create a dummy modal operator, that execute a function very (very!) often, every 0.01ms!
  • That operator calls a 'loop kicker' (kick_async_loop()) to “make advance” the asyncio loop.

My problem is with that loop kicker: outside of some 'break' checks, it basically calls loop.stop(), and immediately after loop.run_forever(). But loop.run_forever() is blocking, right? It will only return once loop.stop() has been called. In other words, we are blocking the main thread (i.e. the whole Blender itself!) waiting for asyncio loop to suspend itself.

Which leads me to think that this cannot (should not!) actually work, unless maybe it does currently because you limit number of simultaneous pillar connections to 3?

Anyway, @dr.sybren, can you confirm that, or explain me how you stop the loop once launched? ;) get back to the

For own work (asset engines in asset-engine branch), after a lot of hours banging my head against the issue, I think I found the (correct?) solution: you have to schedule the call to loop.stop() before calling loop.run_forever() (with either call_soon() or, if you want to leave some more processing time to your loop, call_later() with reasonably small delay (below 1ms for sure)). That way, you can safely run a step or a small bit of the loop, and quickly get back the hand over main thread. ;)

So, have been banging my head against the issue of running asyncio's loop as a sub-loop of Blender one, trying to understand how the `kick_async_loop()` can actually work - and failing so far. As far as I understand, asyncio loop is run by the addon that way: - Create a dummy modal operator, that execute a function very (very!) often, every 0.01ms! - That operator calls a 'loop kicker' (`kick_async_loop()`) to “make advance” the asyncio loop. My problem is with that loop kicker: outside of some 'break' checks, it basically calls `loop.stop()`, and immediately after `loop.run_forever()`. But `loop.run_forever()` is blocking, right? It will only return once `loop.stop()` has been called. In other words, we are blocking the main thread (i.e. the whole Blender itself!) waiting for asyncio loop to suspend itself. Which leads me to think that this cannot (should not!) actually work, unless maybe it does currently because you limit number of simultaneous pillar connections to 3? Anyway, @dr.sybren, can you confirm that, or explain me *how* you stop the loop once launched? ;) get back to the For own work (asset engines in asset-engine branch), after a lot of hours banging my head against the issue, I think I found the (correct?) solution: you have to schedule the call to `loop.stop()` before calling `loop.run_forever()` (with either `call_soon()` or, if you want to leave some more processing time to your loop, `call_later()` with reasonably small delay (below 1ms for sure)). That way, you can safely run a step or a small bit of the loop, and quickly get back the hand over main thread. ;)
Author
Owner

Changed status to: 'Open'

Changed status to: 'Open'
Author
Owner

Added subscribers: @dr.sybren, @mont29

Added subscribers: @dr.sybren, @mont29

The select() calls in asyncio are pretty smart, so it doesn't wait around for data to appear. This is why it can very quickly relinquish execution to Blender.

The call to loop.stop() is there because asyncio doesn't have a loop.run_one_iteration() method. However, from what I've seen, this simulates it quite well. The loop.stop() call sets a flag that's checked at the end of a loop iteration. Effectively it makes asyncio do whatever can be done now, then stops it again.

We could run the asyncio loop in a separate thread, or even using a ThreadPoolExecutor in multiple parallel threads. That would have the advantage that it can keep running, and doesn't need any "kicking". However, it also introduces all the difficulty associated with multithreaded code. As a result, it prevents any async code from calling Blender functions or modifying Blender state, which is IMO a very big downside. With the code in the current state, there is little difference between what you can do in an async function and a regular one. Async-running operators can simply use self.report() to notify the user of events. If this ability disappears, we should have a well thought-out way to communicate with the user that's thread-safe.

The `select()` calls in asyncio are pretty smart, so it doesn't wait around for data to appear. This is why it can very quickly relinquish execution to Blender. The call to `loop.stop()` is there because asyncio doesn't have a `loop.run_one_iteration() ` method. However, from what I've seen, this simulates it quite well. The `loop.stop()` call sets a flag that's checked at the end of a loop iteration. Effectively it makes asyncio do whatever can be done *now*, then stops it again. We could run the asyncio loop in a separate thread, or even using a `ThreadPoolExecutor` in multiple parallel threads. That would have the advantage that it can keep running, and doesn't need any "kicking". However, it also introduces all the difficulty associated with multithreaded code. As a result, it prevents any async code from calling Blender functions or modifying Blender state, which is IMO a very big downside. With the code in the current state, there is little difference between what you can do in an async function and a regular one. Async-running operators can simply use `self.report()` to notify the user of events. If this ability disappears, we should have a well thought-out way to communicate with the user that's thread-safe.
Author
Owner

Ah OK, I see know… so basically, calling (assuming loop is currently stopped of course):

lang=python
loop.stop()
loop.run_forever()

is essentially the same thing as

lang=python
loop.call_soon(loop.stop)
loop.run_forever()

…right? So now things make sense, thanks! :)

I still remain unconvinced about the non-threadibility of the thing though, imho it would allow much more flexibility, and enforce more sane code by precisely preventing ability to call Blender stuff from everywhere. ;) But I do see how it can make things more easy on the other hand, so… can live with it!

Ah OK, I see know… so basically, calling (assuming loop is currently stopped of course): ``` lang=python loop.stop() loop.run_forever() ``` is essentially the same thing as ``` lang=python loop.call_soon(loop.stop) loop.run_forever() ``` …right? So now things make sense, thanks! :) I still remain unconvinced about the non-threadibility of the thing though, imho it would allow much more flexibility, and enforce more sane code by precisely preventing ability to call Blender stuff from everywhere. ;) But I do see how it can make things more easy on the other hand, so… can live with it!

Be sure to check whether loop.call_soon(loop.stop) doesn't starve other loop.call_soon(...) calls. Those are the tricky undocumented behaviours we might want to check in a unit test or something.

Be sure to check whether `loop.call_soon(loop.stop)` doesn't starve other `loop.call_soon(...)` calls. Those are the tricky undocumented behaviours we might want to check in a unit test or something.
Author
Owner

Changed status from 'Open' to: 'Archived'

Changed status from 'Open' to: 'Archived'
Bastien Montagne self-assigned this 2016-10-07 14:14:54 +02:00
Author
Owner

Eeeh, think we can close that one for now :)

Eeeh, think we can close that one for now :)
Sign in to join this conversation.
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: blender/blender-addons#49275
No description provided.