Making concurrent.futures.Futures awaitable

There's an open issue for adding support for awaiting for concurrent.futures.Futures here: http://bugs.python.org/issue24383 This is about writing code like this: async def handler(self): result = await some_blocking_api.do_something_cpu_heavy() await self.write(result) As it stands, without this feature, some boilerplate is required: from asyncio import wrap_future async def handler(self): result = await wrap_future(some_blocking_api.do_something_cpu_heavy()) await self.write(result) I wrote a patch (with tests by Yury Selivanov) that adds __await__() to concurrent.futures.Future and augments the asyncio Task class to handle concurrent Futures. My arguments on why we should add this: * it eliminates the boilerplate code, reducing complexity * it also makes concurrent Futures work with "yield from" style non-native coroutines * it does not interfere with any existing functionality * standard library components should work with each other

FWIW, I am against this (as Alex already knows), for the same reasons I didn't like Nick's proposal. Fuzzing the difference between threads and asyncio tasks is IMO asking for problems -- people will stop understanding what they are doing and then be bitten when they least need it. The example code should be written using loop.run_in_executor(). (This requires that do_something_cpu_heavy() be refactored into a function that does the work and a wrapper that creates the concurrent.futures.Future.) On Fri, Aug 7, 2015 at 6:51 PM, Alex Grönholm <alex.gronholm@nextday.fi> wrote:
-- --Guido van Rossum (python.org/~guido)

On 8 August 2015 at 03:08, Guido van Rossum <guido@python.org> wrote:
I'm against concurrent.futures offering native asyncio support as well - that dependency already goes the other way, from asyncio down to concurrent.futures by way of the loop's pool executor. The only aspect of my previous suggestions I'm still interested in is a name and signature change from "loop.run_in_executor(executor, callable)" to "loop.call_in_background(callable, *, executor=None)". Currently, the recommended way to implement a blocking call like Alex's example is this: from asyncio import get_event_loop async def handler(self): loop = asyncio.get_event_loop() result = await loop.run_in_executor(None, some_blocking_api.some_blocking_call) await self.write(result) I now see four concrete problems with this specific method name and signature: * we don't run functions, we call them * we do run event loops, but this call doesn't start an event loop running * "executor" only suggests "background call" to folks that already know how concurrent.futures works * we require the explicit "None" boilerplate to say "use the default executor", rather than using the more idiomatic approach of accepting an alternate executor as an optional keyword only argument With the suggested change to the method name and signature, the same example would instead look like: async def handler(self): loop = asyncio.get_event_loop() result = await loop.call_in_background(some_blocking_api.some_blocking_call) await self.write(result) That should make sense to anyone reading the handler, even if they know nothing about concurrent.futures - the precise mechanics of how the event loop goes about handing off the call to a background thread or process is something they can explore later, they don't need to know about it in order to locally reason about this specific handler. It also means that event loops would be free to implement their *default* background call functionality using something other than concurrent.futures, and only switch to the latter if an executor was specified explicitly. There are still some open questions about whether it makes sense to allow callables to indicate whether or not they expect to be IO bound or CPU bound, and hence allow event loop implementations to opt to dispatch the latter to a process pool by default (I saw someone suggest that recently, and I find the idea intriguing), but I think that's a separate question from dispatching a given call for parallel execution, with the result being awaited via a particular event loop. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

08.08.2015, 11:12, Nick Coghlan kirjoitti: > On 8 August 2015 at 03:08, Guido van Rossum <guido@python.org> wrote: >> FWIW, I am against this (as Alex already knows), for the same reasons I >> didn't like Nick's proposal. Fuzzing the difference between threads and >> asyncio tasks is IMO asking for problems -- people will stop understanding >> what they are doing and then be bitten when they least need it. > I'm against concurrent.futures offering native asyncio support as well > - that dependency already goes the other way, from asyncio down to > concurrent.futures by way of the loop's pool executor. Nobody is suggesting that. The __await__ support suggested for concurrent Futures is generic and has no ties whatsoever to asyncio. > The only aspect of my previous suggestions I'm still interested in is > a name and signature change from "loop.run_in_executor(executor, > callable)" to "loop.call_in_background(callable, *, executor=None)". That name would and argument placement would be better, but are you suggesting that the ability to pass along extra arguments should be removed? The original method was bad enough in that it only supported positional and not keyword arguments, forcing users to pass partial() objects as callables. > Currently, the recommended way to implement a blocking call like > Alex's example is this: > > from asyncio import get_event_loop > > async def handler(self): > loop = asyncio.get_event_loop() > result = await loop.run_in_executor(None, > some_blocking_api.some_blocking_call) > await self.write(result) > > I now see four concrete problems with this specific method name and signature: > > * we don't run functions, we call them > * we do run event loops, but this call doesn't start an event loop running > * "executor" only suggests "background call" to folks that already > know how concurrent.futures works > * we require the explicit "None" boilerplate to say "use the > default executor", rather than using the more idiomatic approach of > accepting an alternate executor as an optional keyword only argument > > With the suggested change to the method name and signature, the same > example would instead look like: > > async def handler(self): > loop = asyncio.get_event_loop() > result = await > loop.call_in_background(some_blocking_api.some_blocking_call) > await self.write(result) Am I the only one who's bothered by the fact that you have to get a reference to the event loop first? Wouldn't this be better: async def handler(self): result = await asyncio.call_in_background(some_blocking_api.some_blocking_call) await self.write(result) The call_in_background() function would return an awaitable object that is recognized by the asyncio Task class, which would then submit the function to the default executor of the event loop. > That should make sense to anyone reading the handler, even if they > know nothing about concurrent.futures - the precise mechanics of how > the event loop goes about handing off the call to a background thread > or process is something they can explore later, they don't need to > know about it in order to locally reason about this specific handler. > > It also means that event loops would be free to implement their > *default* background call functionality using something other than > concurrent.futures, and only switch to the latter if an executor was > specified explicitly. Do you mean background calls that don't return objects compatible with concurrent.futures.Futures? Can you think of a use case for this? > > There are still some open questions about whether it makes sense to > allow callables to indicate whether or not they expect to be IO bound > or CPU bound, What do you mean by this? > and hence allow event loop implementations to opt to > dispatch the latter to a process pool by default Bad idea! The semantics are too different and process pools have too many limitations. > (I saw someone > suggest that recently, and I find the idea intriguing), but I think > that's a separate question from dispatching a given call for parallel > execution, with the result being awaited via a particular event loop. > > Cheers, > Nick. >

On 8 Aug 2015 22:48, "Alex Grönholm" <alex.gronholm@nextday.fi> wrote:
That name would and argument placement would be better, but are you
suggesting that the ability to pass along extra arguments should be removed? The original method was bad enough in that it only supported positional and not keyword arguments, forcing users to pass partial() objects as callables. That's a deliberate design decision in many of asyncio's APIs to improve the introspection capabilities and to clearly separate concerns between "interacting with the event loop" and "the operation being dispatched for execution".
That was my original suggestion a few weeks ago, but after playing with it for a while, I came to agree with Guido that hiding the event loop in this case likely wasn't helpful to the conceptual learning process. Outside higher level frameworks that place more constraints on your code, you really can't get very far with asyncio without becoming comfortable with interacting with the event loop directly. I gave a demo using the current spelling as a lightning talk at PyCon Australia last weekend: https://www.youtube.com/watch?v=_pfJZfdwkgI The only part of that demo I really wasn't happy with was the "run_in_executor" call - the rest all felt good for the level asyncio operates at, while still allowing higher level third party APIs that hide more of the underlying machinery (like the event loop itself, as well as the use of partial function application).
The call_in_background() function would return an awaitable object that
is recognized by the asyncio Task class, which would then submit the function to the default executor of the event loop.
concurrent.futures.Futures? A background call already returns an asyncio awaitable, not a concurrent.futures.Future object.
Can you think of a use case for this?
Yes, third party event loops like Twisted may have their own background call mechanism that they'd prefer to use by default, rather than the concurrent.futures model.
There was a thread on the idea recently, but I don't have a link handy. Indicating CPU vs IO bound directly wouldn't work (that's context dependent), but allowing callables to explicitly indicate "recommended", "supported", "incompatible" for process pools could be interesting. limitations. Yes, that's why I find it an intriguing notion to allow callables to explicitly indicate whether or not they're compatible with them. Cheers, Nick

09.08.2015, 03:22, Nick Coghlan kirjoitti:
While I won't pretend to understand what this means, I recognize that you've given it considerably more thought than I have.
As long as I can still write a high level framework where boilerplate is minimized in user code, I can "yield" on this issue.
What I don't get is why you say that this name and signature change would somehow enable event loops to implement an alternative mechanism for background calls. By event loops do you mean something like Twisted's reactors or just customized versions of asyncio event loops? To me, the former makes no sense at all and with the latter, I don't see how this name and signature change changes anything. Could they not already use whatever mechanism they please as long as it returns an awaitable (or iterable in the case of 3.4 or earlier) object, by having their custom implementation of run_in_executor()?
Yeah -- it'll be interesting to see where that goes.

On 09.08.2015 02:22, Nick Coghlan wrote:
Thread start (more or less): https://mail.python.org/pipermail/python-ideas/2015-August/034917.html Current post of that thread: https://mail.python.org/pipermail/python-ideas/2015-August/035211.html And I would like to hear you insights on the try: block idea as well. :)

FWIW, I am against this (as Alex already knows), for the same reasons I didn't like Nick's proposal. Fuzzing the difference between threads and asyncio tasks is IMO asking for problems -- people will stop understanding what they are doing and then be bitten when they least need it. The example code should be written using loop.run_in_executor(). (This requires that do_something_cpu_heavy() be refactored into a function that does the work and a wrapper that creates the concurrent.futures.Future.) On Fri, Aug 7, 2015 at 6:51 PM, Alex Grönholm <alex.gronholm@nextday.fi> wrote:
-- --Guido van Rossum (python.org/~guido)

On 8 August 2015 at 03:08, Guido van Rossum <guido@python.org> wrote:
I'm against concurrent.futures offering native asyncio support as well - that dependency already goes the other way, from asyncio down to concurrent.futures by way of the loop's pool executor. The only aspect of my previous suggestions I'm still interested in is a name and signature change from "loop.run_in_executor(executor, callable)" to "loop.call_in_background(callable, *, executor=None)". Currently, the recommended way to implement a blocking call like Alex's example is this: from asyncio import get_event_loop async def handler(self): loop = asyncio.get_event_loop() result = await loop.run_in_executor(None, some_blocking_api.some_blocking_call) await self.write(result) I now see four concrete problems with this specific method name and signature: * we don't run functions, we call them * we do run event loops, but this call doesn't start an event loop running * "executor" only suggests "background call" to folks that already know how concurrent.futures works * we require the explicit "None" boilerplate to say "use the default executor", rather than using the more idiomatic approach of accepting an alternate executor as an optional keyword only argument With the suggested change to the method name and signature, the same example would instead look like: async def handler(self): loop = asyncio.get_event_loop() result = await loop.call_in_background(some_blocking_api.some_blocking_call) await self.write(result) That should make sense to anyone reading the handler, even if they know nothing about concurrent.futures - the precise mechanics of how the event loop goes about handing off the call to a background thread or process is something they can explore later, they don't need to know about it in order to locally reason about this specific handler. It also means that event loops would be free to implement their *default* background call functionality using something other than concurrent.futures, and only switch to the latter if an executor was specified explicitly. There are still some open questions about whether it makes sense to allow callables to indicate whether or not they expect to be IO bound or CPU bound, and hence allow event loop implementations to opt to dispatch the latter to a process pool by default (I saw someone suggest that recently, and I find the idea intriguing), but I think that's a separate question from dispatching a given call for parallel execution, with the result being awaited via a particular event loop. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

08.08.2015, 11:12, Nick Coghlan kirjoitti: > On 8 August 2015 at 03:08, Guido van Rossum <guido@python.org> wrote: >> FWIW, I am against this (as Alex already knows), for the same reasons I >> didn't like Nick's proposal. Fuzzing the difference between threads and >> asyncio tasks is IMO asking for problems -- people will stop understanding >> what they are doing and then be bitten when they least need it. > I'm against concurrent.futures offering native asyncio support as well > - that dependency already goes the other way, from asyncio down to > concurrent.futures by way of the loop's pool executor. Nobody is suggesting that. The __await__ support suggested for concurrent Futures is generic and has no ties whatsoever to asyncio. > The only aspect of my previous suggestions I'm still interested in is > a name and signature change from "loop.run_in_executor(executor, > callable)" to "loop.call_in_background(callable, *, executor=None)". That name would and argument placement would be better, but are you suggesting that the ability to pass along extra arguments should be removed? The original method was bad enough in that it only supported positional and not keyword arguments, forcing users to pass partial() objects as callables. > Currently, the recommended way to implement a blocking call like > Alex's example is this: > > from asyncio import get_event_loop > > async def handler(self): > loop = asyncio.get_event_loop() > result = await loop.run_in_executor(None, > some_blocking_api.some_blocking_call) > await self.write(result) > > I now see four concrete problems with this specific method name and signature: > > * we don't run functions, we call them > * we do run event loops, but this call doesn't start an event loop running > * "executor" only suggests "background call" to folks that already > know how concurrent.futures works > * we require the explicit "None" boilerplate to say "use the > default executor", rather than using the more idiomatic approach of > accepting an alternate executor as an optional keyword only argument > > With the suggested change to the method name and signature, the same > example would instead look like: > > async def handler(self): > loop = asyncio.get_event_loop() > result = await > loop.call_in_background(some_blocking_api.some_blocking_call) > await self.write(result) Am I the only one who's bothered by the fact that you have to get a reference to the event loop first? Wouldn't this be better: async def handler(self): result = await asyncio.call_in_background(some_blocking_api.some_blocking_call) await self.write(result) The call_in_background() function would return an awaitable object that is recognized by the asyncio Task class, which would then submit the function to the default executor of the event loop. > That should make sense to anyone reading the handler, even if they > know nothing about concurrent.futures - the precise mechanics of how > the event loop goes about handing off the call to a background thread > or process is something they can explore later, they don't need to > know about it in order to locally reason about this specific handler. > > It also means that event loops would be free to implement their > *default* background call functionality using something other than > concurrent.futures, and only switch to the latter if an executor was > specified explicitly. Do you mean background calls that don't return objects compatible with concurrent.futures.Futures? Can you think of a use case for this? > > There are still some open questions about whether it makes sense to > allow callables to indicate whether or not they expect to be IO bound > or CPU bound, What do you mean by this? > and hence allow event loop implementations to opt to > dispatch the latter to a process pool by default Bad idea! The semantics are too different and process pools have too many limitations. > (I saw someone > suggest that recently, and I find the idea intriguing), but I think > that's a separate question from dispatching a given call for parallel > execution, with the result being awaited via a particular event loop. > > Cheers, > Nick. >

On 8 Aug 2015 22:48, "Alex Grönholm" <alex.gronholm@nextday.fi> wrote:
That name would and argument placement would be better, but are you
suggesting that the ability to pass along extra arguments should be removed? The original method was bad enough in that it only supported positional and not keyword arguments, forcing users to pass partial() objects as callables. That's a deliberate design decision in many of asyncio's APIs to improve the introspection capabilities and to clearly separate concerns between "interacting with the event loop" and "the operation being dispatched for execution".
That was my original suggestion a few weeks ago, but after playing with it for a while, I came to agree with Guido that hiding the event loop in this case likely wasn't helpful to the conceptual learning process. Outside higher level frameworks that place more constraints on your code, you really can't get very far with asyncio without becoming comfortable with interacting with the event loop directly. I gave a demo using the current spelling as a lightning talk at PyCon Australia last weekend: https://www.youtube.com/watch?v=_pfJZfdwkgI The only part of that demo I really wasn't happy with was the "run_in_executor" call - the rest all felt good for the level asyncio operates at, while still allowing higher level third party APIs that hide more of the underlying machinery (like the event loop itself, as well as the use of partial function application).
The call_in_background() function would return an awaitable object that
is recognized by the asyncio Task class, which would then submit the function to the default executor of the event loop.
concurrent.futures.Futures? A background call already returns an asyncio awaitable, not a concurrent.futures.Future object.
Can you think of a use case for this?
Yes, third party event loops like Twisted may have their own background call mechanism that they'd prefer to use by default, rather than the concurrent.futures model.
There was a thread on the idea recently, but I don't have a link handy. Indicating CPU vs IO bound directly wouldn't work (that's context dependent), but allowing callables to explicitly indicate "recommended", "supported", "incompatible" for process pools could be interesting. limitations. Yes, that's why I find it an intriguing notion to allow callables to explicitly indicate whether or not they're compatible with them. Cheers, Nick

09.08.2015, 03:22, Nick Coghlan kirjoitti:
While I won't pretend to understand what this means, I recognize that you've given it considerably more thought than I have.
As long as I can still write a high level framework where boilerplate is minimized in user code, I can "yield" on this issue.
What I don't get is why you say that this name and signature change would somehow enable event loops to implement an alternative mechanism for background calls. By event loops do you mean something like Twisted's reactors or just customized versions of asyncio event loops? To me, the former makes no sense at all and with the latter, I don't see how this name and signature change changes anything. Could they not already use whatever mechanism they please as long as it returns an awaitable (or iterable in the case of 3.4 or earlier) object, by having their custom implementation of run_in_executor()?
Yeah -- it'll be interesting to see where that goes.

On 09.08.2015 02:22, Nick Coghlan wrote:
Thread start (more or less): https://mail.python.org/pipermail/python-ideas/2015-August/034917.html Current post of that thread: https://mail.python.org/pipermail/python-ideas/2015-August/035211.html And I would like to hear you insights on the try: block idea as well. :)
participants (4)
-
Alex Grönholm
-
Guido van Rossum
-
Nick Coghlan
-
Sven R. Kunze