Yes, many of the dunder methods do work ‘out of the box’, for example async def __lshift__(self) can be used await (asyncthing << value). However, the mechanics of await get in the way quite quickly. For example, the sync
ostream << ‘My weight is ’ << me.weight << ‘ kg\n’
in async becomes
await (await (await (ostream << 'My weight is ') << me.weight) << ' kg\n'
If you need to await __getattr__() too,
await (await (await (ostream << 'My weight is ') << await me.weight) << ' kg\n'
What was a slick way to output to a stream, becomes somewhat unwieldy, masking the author’s intent with the mechanics.
Some dunder methods do not work. For example async def __setattr__(..). The return value from the call to __setattr__() is discarded. As that’s the coroutine which needs to be awaited, not a great deal happens except for the warning about the un-awaited coroutine.
Moving onto sync_to_async etc. Here I checked using asgiref’s implementations of these. sync_to_async executes the sync code on a separate thread through a thread pool. This brings limitations from the OS’s number of threads, and multi-threaded interlock hazards (TBH, not usually a problem), but that’s not the real gotcha. I noticed this implementation had a pool of one thread, executing the code pieces off a queue. It was easy to build a, not entirely contrived, case which deadlocked due to the thread pool bottleneck. Two tasks, A and B, both of which went async→sync→async and in the inner sync waited for a future before setting the result of a different future. A third task waited a bit, set the result of A’s waited-for future, waited for A’s other future, then set B’s waited-for future. If A was created before B everything was OK, if B was created first, its sync section blocked the task pool, so A’s sync section couldn’t execute and release the end of the sequence. IIRC I encountered this deadlock in my website - just one of the problems I encountered trying to adopt asyncio on my website.
So why does ‘await anywhere’ help? The park-this-Task-while-it-waits mechanism is completely different, so is allowed anywhere…
You don’t have to sync_to_async or async_to_sync, which avoids deadlocking through the thread pool.
You can monkey-patch sync libraries to be asynchronous aware. As a worked example, I thought requests was a good example as there’s a Stack Overflow asking for exactly this. After a couple of unsuccessful approaches using asyncio’s connection system, I went to the bottom layer and monkey-patched socket. The socket class can only have its methods replaced due to ssl’s implementation. Here’s an example monkey-patched function:
def recv_inner(self, do_recv, flags):
if AsyncSocket.docalldirect(self, flags):
return do_recv(flags)
AsyncSocket.setsubblocking(self, False)
fd = self.fileno()
loop = asyncio.get_event_loop()
fut = loop.create_future()
handle = loop.add_reader(fd, AsyncSocket.send_and_recv_cb, self, fut)
try:
while True:
try:
return do_recv(flags)
except (BlockingIOError, InterruptedError):
pass
except BaseException:
raise
await fut
finally:
if handle is None or not handle.cancelled():
loop.remove_reader(fd)
def recv(self, bufsize, flags=0):
return AsyncSocket.recv_inner(self, partial(rawsock.recv, self, bufsize), flags)
A little explanation: docalldirect() - is the caller expecting a non-blocking interaction?; setsubblocking - there’s a separation between what the socket user has set for non-blocking, and what the underlying socket is set to, this sets the underlying socket’s blocking mode; send_and_recv_cb() after the select has triggered this translates the socket’s state into the future’s result. This is written assuming selector_events (the default), and would need adapting to proactor events. If you dig into it further, you’ll realise that ssl does most of the communication directly, not via socket. However, once an optional call-back was added to use python code to do its selects, this was also very easy to make async capable.
The overall result: requests on my proof-of-concept python with the monkey patch is async ready without needing to adjust or add anything to requests. Moreover, any library which uses socket or ssl is now asynchronous ready. Any sync library can be used async as only the outermost entry point of a Task, and the innermost thing-which-waits need to know that async is happening.
As it worked so well, I’ll now integrate the monkey patch into the proof-of-concept and make improvements to its timeout handling (which, in the monkey-patch demo, is incomplete) and loop-kind awareness (selector versus proactor loops).