Exactly-Once Initialization in Asynchronous Python
nullprogram.com
nullprogram.com
I've seen DI frameworks in python but not really used them. At a glance they don't strike me as pythonic. Rolling your own kind of inversion of control can result in unruly "config" or "context" objects that bring difficulties as well.
So i assume he means initilize it once in program entry and Pass in the pointer to the object that needs it via including it the constructor or a function call.
Do you think There’s a pythonic way of doing these things?
I'm not a big python user, but I do find it kind of surprising there isn't an awaitable and thread safe mutex in the stdlib.
I'd say single-threading is the right call for 99% of Python's use-cases and users. C extensions are available and widely used for core functionality that needs to be fast and/or parallel, e.g. large parts of numpy are in the c extension.
As soon as you have an `await` anywhere in the code, you've got to assume that your code will be re-entered. Lots of asyncio.Locks all over the place for me.
Glad people are bringing this up. I had to learn this on my own.
At least the re-entry points are explicitly marked with `await`. IMO that's the main benefit of async-await (stackless coroutines) over stackful coroutines or threads, which allow your code to be suspended and re-entered almost anywhere.
Of course the drawback of async-await is the "function color" issue [0], in which it's difficult for functions that don't suspend to call those which do.
[0] http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y...
So when your function is not reentrant, params = await command.get() runs in a loop inside a task ( command.put_nowait(params) is called elsewhere)
you can also use this to distribute tasks to different class methods
I work on several async projects, but I never had to use multiple event loops. What are use cases for using multiple event loops?
For example, when working w/ immutable GPU dataframes to represent our user's datasets, we often get into variants where loading a dataset may take a bit and thus get multiple services requesting it before ETL is done. So, we want to only trigger the parser once per file and have any subsequent calls wait on the first one:
datasets = {}
async def load_once(name):
if not (name in datasets): # sync, many
fut = asyncio.create_future() # sync, once
datasets[name] = fut # sync, once
fut.set_result(await load(name)) # async, once
return await datasets[name] # async, many
And then throw in an async lru.. :)The issue is with exception safety - first, this does not handle exceptions in load() properly, but that is a trivial fix.
The more insidious problem is due to the fact that Python future are cancellable - and exceptions cancel futures.
What this means is that if two callers call load_once() in parallel, and the first caller encounters an exception (eg. from calling something else in parallel), the load() future will be cancelled for _all_ callers (eg. the second one), and will remain in a permanently wedged state.
Fixing that is, well, quite a bit more code...
So load() needs a try/except, and except is either kill process / retry / clear / cancel, and the other loader should also expect an exn depending on that choice. All of that is app/use-case dependent.
Usefully, Futures and async/await natively represent most of these. The case we find missing is around back pressure, retry, etc., but I haven't seen a good lang construct for that. We used to do a lot of Rx for that, and now a lot of HTTP headers/libs when a remote call, but it all feels messy. This is somewhat orthogonal to futures for load once tho.
I tried skimming that article, but it comes off as a long and hard to read rant, which suggests the author needs to understand their idea better or pick an explanation/analogy that is more direct. Maybe something like the ocap reasoning for promises vs futures, or say the issues of coloring, may help...
Interesting - do you think there's a better solution than async-await for smaller codebases?
I found async testing in python to be annoying, although i found a couple of libraries to make it nicer (pytest-async and i forget the name of the other).
It's very boilerplate-y (Mopidy uses Pykka) though and takes some time getting used to coming from other frameworks.
initialized = D.setdefault('initialized', True)
... D = {}
async def maybe_initialize():
global D
this_setup = asyncio.ensure_future(one_time_setup())
actual_setup = D.setdefault('initializer', this_setup)
if this_setup is not actual_setup:
this_setup.cancel()
await actual_setup
return
complete example: https://gist.github.com/mgraczyk/e251443bccfe54505e75b652655...If you read the full blog post, the author solves the problem without setdefault, and without having to cancel any futures.
It's way easier with a single thread. You don't need a lock or atomic operations.
initializer = None
async def maybe_initialize():
global initializer
if initializer is None:
initializer = asyncio.ensure_future(one_time_setup())
await initializer
return- Chris forgot to declare the variable as global;
- Chris used asyncio.create_task() instead of asyncio.ensure_future().
The problem is that there are three logical states that need three different actions
1. if nothing done yet: start async computation
2. if async computation in progress: wait for it
3. return result as computaton done
Atomicity in the sense that no other Python code can run during a setdefault, is not relevant.
Waiting for something that is running in another loop actually isn't easy either; I'm not sure that Python really allows it - won't you get an error:
The future belongs to a different loop than the one specified as the loop argumentHaving multiple loops waiting for each other is pretty complex to debug.
import asyncio
import threading
future = None
def p(msg):
print(f"{msg}: {threading.get_ident()}, event loop: {id(asyncio.get_running_loop())}")
async def one_time_setup():
p('one_time_setup start')
await asyncio.sleep(1)
p('one_time_setup done')
async def maybe_initialize():
global future
if not future:
future = asyncio.create_task(one_time_setup())
p('waiting start')
await future
p('waiting done')
def worker():
new_loop = asyncio.new_event_loop()
asyncio.set_event_loop(new_loop)
new_loop.run_until_complete(maybe_initialize())
number_of_threads = 2
for _ in range(number_of_threads):
threading.Thread(target=worker).start()
this prints RuntimeError: Task <Task pending coro=<maybe_initialize() running at test.py:20> cb=[_run_until_complete_cb() at /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/asyncio/base_events.py:158]> got Future <Task pending coro=<one_time_setup() running at test.py:11> cb=[<TaskWakeupMethWrapper object at 0x10cafdeb8>()]> attached to a different loopIf that doesnt work, then set it to an 'asyncio.event`, and run the one_time_setup "in the background" (create_task), and when its done it marks the event as complete.
[0] https://developer.apple.com/documentation/dispatch/1447169-d...
the good thing with go's sync.Once is that it's implemented as a library instead of something in the language itself, so it's easy for curious user to see how it's actually implemented. they even have comments there pointing out wrong implementations, which I have seen people make the exact same mistake during code reviews (in other language).
> Kind of surprised python doesn’t as this sort of pattern is common in applications dealing with concurrency
Well yeah, python was not designed for that.
./once.py:29: RuntimeWarning: coroutine 'once.
<locals>.once_wrapper' was never awaited
one_time_setup()https://stackoverflow.com/questions/57399157/runtimewarning-...
Edit: Also tangentially relevant is the article linked by pansa2 in another thread, which was also discussed on HN a few months ago: http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y...
Please write classes, people!
"global" is a fine way to do that when you need it. Simple and says what it means.