An event loop runs in a thread (typically the main thread) and executes
all callbacks and Tasks in its thread. While a Task is running in the
event loop, no other Tasks can run in the same thread. When a Task
executes an await expression, the running Task gets suspended, and
the event loop executes the next Task.

Almost all asyncio objects are not thread safe, which is typically
not a problem unless there is code that works with them from outside
of a Task or a callback. If there’s a need for such code to call a
low-level asyncio API, the loop.call_soon_threadsafe() method
should be used, e.g.:

Blocking (CPU-bound) code should not be called directly. For example,
if a function performs a CPU-intensive calculation for 1 second,
all concurrent asyncio Tasks and IO operations would be delayed
by 1 second.

An executor can be used to run a task in a different thread or even in
a different process to avoid blocking block the OS thread with the
event loop. See the loop.run_in_executor() method for more
details.

If a Future.set_exception() is called but the Future object is
never awaited on, the exception would never be propagated to the
user code. In this case, asyncio would emit a log message when the
Future object is garbage collected.