asyncio uses event loops to orchestrate the callbacks and asynchronous tasks. Event loops live in the context of event loop policies.

Coroutines can be thought of as functions you can “pause” at stages explicitly marked with some sort of syntactical element. The coroutine’s state is tracked via a task object, instantiated by the respective event loop. The event loop keeps track of which task is currently running and delegates CPU time from idling coroutines to a pending one.
### Locating the Currently Running Loop
Как узнать выполняется ли event loop в данный момент и какой именно?
```
Используйте asyncio.get_event_loop и asyncio.get_running_loop.
```
Что делает asyncio.get_event_loop под капотом?
1. Проверяет выполняется ли цикл в момент вызова функции.
2. Возвращает запущенный цикл, pid которого соответствует pid текущего процесса, если такой имеется.
3. Если нет, получает thread-global LoopPolicy, который хранится в глобальной переменной в модуле asyncio.
4. Если он не установлен, создает его с помощью Default Loop Policy, используя блокировку.
5. Обратите внимание, что DefaultLoopPolicy зависит от операционной системы и имеет подклассы BaseDefaultEventLoopPolicy, которые предоставляют реализацию loop. get_event_loop по умолчанию, которая вызывается.
6. Вот в чем загвоздка: метод loop_policy.get_event_loop создает экземпляр цикла только в том случае, если вы находитесь в основном потоке, и присваивает его локальной переменной потока.
Если вы не находитесь в основном потоке и ни один запущенный цикл не создается другими способами, это вызовет ошибку RuntimeError.
asyncio.get_running_loop работает по-другому. Он всегда будет возвращать запущенный в данный момент экземпляр цикла, если он уже запущен. Если такового нет, это вызовет ошибку RuntimeError.
### Attaching a Loop to the Thread Problem
Creating one loop per thread that’s bond to the thread and which’s finishing can be also awaited can be a challenging task. Later we learn about the executor API, which allows us to execute blocking coroutine calls as non-blocking calls by executing the respective calls on a thread pool.
Solution
Using the threading. Thread and the side-effect-free (besides event loop policy creation) asyncio.new_event_loop APIs, we can create thread instances that have unique event loop instances.
```
import asyncio
import threading
def create_event_loop_thread(worker, *args, **kwargs):
def _worker(*args, **kwargs):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(worker(*args, **kwargs))
finally:
loop.close()
return threading.Thread(target=_worker, args=args, kwargs=kwargs)
async def print_coro(*args, **kwargs):
print(f"Inside the print coro on {threading.get_ident()}:", (args, kwargs))
def start_threads(*threads):
[t.start() for t in threads if isinstance(t, threading.Thread)]
def join_threads(*threads):
[t.join() for t in threads if isinstance(t, threading.Thread)]
workers = [create_event_loop_thread(print_coro) for i in range(10)]
start_threads(*workers)
join_threads(*workers)
```
Loops live in the context of a loop policy. The DefaultLoopPolicy scopes the loop per thread and does not allow creation of a loop outside a main thread via asyncio.get_event_loop. Hence, we must create a thread local event loop via asyncio.set_event_loop(asyncio.new_event_loop()).
We then await the asyncio.run_until_complete completion inside our internal worker function called _worker by waiting for the thread to be joined via join_threads.