12

The two classes represent excellent abstractions for concurrent programming, so it's a bit disconcerting that they don't support the same API.

Specifically, according to the docs:

asyncio.Future is almost compatible with concurrent.futures.Future.

Differences:

  • result() and exception() do not take a timeout argument and raise an exception when the future isn’t done yet.
  • Callbacks registered with add_done_callback() are always called via the event loop's call_soon_threadsafe().
  • This class is not compatible with the wait() and as_completed() functions in the concurrent.futures package.

The above list is actually incomplete, there are a couple more differences:

  • running() method is absent
  • result() and exception() may raise InvalidStateError if called too early

Are any of these due to the inherent nature of an event loop that makes these operations either useless or too troublesome to implement?

And what is the meaning of the difference related to add_done_callback()? Either way, the callback is guaranteed to happen at some unspecified time after the futures is done, so isn't it perfectly consistent between the two classes?

Community
  • 1
  • 1
max
  • 49,282
  • 56
  • 208
  • 355

2 Answers2

8

The core reason for the difference is in how threads (and processes) handle blocks vs how coroutines handle events that block. In threading, the current thread is suspended until whatever condition resolves and the thread can go forward. For example in the case of the futures, if you request the result of a future, it's fine to suspend the current thread until that result is available.

However the concurrency model of an event loop is that rather than suspending code, you return to the event loop and get called again when ready. So it is an error to request the result of an asyncio future that doesn't have a result ready.

You might think that the asyncio future could just wait and while that would be inefficient, would it really be all that bad for your coroutine to block? It turns out though that having the coroutine block is very likely to mean that the future never completes. It is very likely that the future's result will be set by some code associated with the event loop running the code that requests the result. If the thread running that event loop blocks, no code associated with the event loop would run. So blocking on the result would deadlock and prevent the result from being produced.

So, yes, the differences in interface are due to this inherent difference. As an example, you wouldn't want to use an asyncio future with the concurrent.futures waiter abstraction because again that would block the event loop thread.

The add_done_callbacks difference guarantees that callbacks will be run in the event loop. That's desirable because they will get the event loop's thread local data. Also, a lot of coroutine code assumes that it will never be run at the same time as other code from the same event loop. That is, coroutines are only thread safe under the assumption that two coroutines from the same event loop do not run at the same time. Running the callbacks in the event loop avoids a lot of thread safety issues and makes it easier to write correct code.

Andrew Svetlov
  • 16,730
  • 8
  • 66
  • 69
Sam Hartman
  • 6,210
  • 3
  • 23
  • 40
7

concurrent.futures.Future provides a way to share results between different threads and processes usually when you use Executor.

asyncio.Future solves same task but for coroutines, that are actually some special sort of functions running usually in one process/thread asynchronously. "Asynchronously" in current context means that event loop manages code executing flow of this coroutines: it may suspend execution inside one coroutine, start executing another coroutine and later return to executing first one - everything usually in one thread/process.

These objects (and many other threading/asyncio objects like Lock, Event, Semaphore etc.) look similar because the idea of concurrency in your code with threads/processes and coroutines is similar.

I think the main reason objects are different is historical: asyncio was created much later then threading and concurrent.futures. It's probably impossible to change concurrent.futures.Future to work with asyncio without breaking class API.

Should both classes be one in "ideal world"? This is probably debatable issue, but I see many disadvantages of that: while asyncio and threading look similar at first glance, they're very different in many ways, including internal implementation or way of writing asyncio/non-asyncio code (see async/await keywords).

I think it's probably for the best that classes are different: we clearly split different by nature ways of concurrency (even if their similarity looks strange at first).

Quentin Pradet
  • 4,691
  • 2
  • 29
  • 41
Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159