Почему использование Asyncio не сокращает общее время выполнения в Python и одновременного запуска функций?

#python #multithreading #async-await #multiprocessing #python-asyncio

#python #многопоточность #async-await #многопроцессорность #python-asyncio

Вопрос:

Я пытаюсь запустить фрагмент кода с использованием asyncio и сократить время выполнения всего кода. Ниже приведен мой код, для полного выполнения которого требуется около 6 секунд

Обычные вызовы функций — (подход 1)

 from time import time, sleep
import asyncio


def find_div(range_, divide_by):
    lis_ = []
    for i in range(range_):
        if i % divide_by == 0:
            lis_.append(i)
        
    print("found numbers for range {}, divided by {}".format(range_, divide_by))
    return lis_

if __name__ == "__main__":
    start = time()
    find_div(50800000, 341313)
    find_div(10005200, 32110)
    find_div(50000340, 31238)
    print(time()-start)
    
  

Результат приведенного выше кода — это просто общее время выполнения, которое составляет 6 секунд.

Многопоточный подход — (подход 2) При этом использовалась многопоточность, но, на удивление, время увеличилось

 from time import time, sleep
import asyncio
import threading


def find_div(range_, divide_by):
    lis_ = []
    for i in range(range_):
        if i % divide_by == 0:
            lis_.append(i)
        
    print("found numbers for range {}, divided by {}".format(range_, divide_by))
    return lis_

if __name__ == "__main__":
    start = time()
    t1 = threading.Thread(target=find_div, args=(50800000, 341313)) 
    t2 = threading.Thread(target=find_div, args=(10005200, 32110)) 
    t3 = threading.Thread(target=find_div, args=(50000340, 31238)) 
  
    t1.start() 
    t2.start() 
    t3.start()

    t1.join() 
    t2.join() 
    t3.join()
    print(time()-start)
  

Вывод приведенного выше кода составляет 12 секунд.

Подход с многопроцессорной обработкой — (подход 3)

 from time import time, sleep
import asyncio
from multiprocessing import Pool

def multi_run_wrapper(args):
   return find_div(*args)

def find_div(range_, divide_by):
    lis_ = []
    for i in range(range_):
        if i % divide_by == 0:
            lis_.append(i)
        
    print("found numbers for range {}, divided by {}".format(range_, divide_by))
    return lis_

if __name__ == "__main__":
    start = time()
    with Pool(3) as p:
        p.map(multi_run_wrapper,[(50800000, 341313),(10005200, 32110),(50000340, 31238)])
    
    
    print(time()-start)
  

Вывод многопроцессорного кода составляет 3 секунды, что лучше, чем при обычном подходе к вызову функции.

Подход Asyncio — (подход 4)

 from time import time, sleep

import asyncio

async def find_div(range_, divide_by):
    lis_ = []
    for i in range(range_):
        if i % divide_by == 0:
            lis_.append(i)
        
    print("found numbers for range {}, divided by {}".format(range_, divide_by))
    return lis_


async def task():

    tasks = [find_div(50800000, 341313),find_div(10005200, 32110),find_div(50000340, 31238)]
    result = await asyncio.gather(*tasks)
    print(result)

if __name__ == "__main__":
    start = time()
    asyncio.run(task())
    print(time()-start)
  

Приведенный выше код также занимает около 6 секунд, что соответствует обычному вызову функции выполнения, который является подходом 1.

Проблема-
Почему мой подход Asyncio не работает должным образом и сокращает общее время?
Что не так в коде?

Комментарии:

1. Я предполагаю, что это просто пример кода? Потому что есть ГОРАЗДО более эффективный способ получить список всех кратных числу, чем проверка по модулю для каждого числа в диапазоне

2. Ваши «асинхронные» функции ничего не ожидают, поэтому они даже не пытаются переключить выполнение, они просто выполняются как обычный код синхронизации. Если вы добавите что-то вроде await asyncio.sleep(0) в цикл, они начнут выдавать результаты и выполняться одновременно. Обратите внимание, что таким образом вы не получите ускорения, может даже оказаться, что async — самый медленный из всех протестированных вариантов. Как объясняется в ответе, это связано с тем, что асинхронное выполнение предназначено для параллельного ожидания, а не для параллельного выполнения, и ваш код явно привязан к процессору.

3. Если вы хотите использовать asyncio для других задач в вашей программе (например, для сетевого взаимодействия), но у вас есть привязанный к процессору код, который должен выполняться на нескольких ядрах, вы можете объединить многопроцессорную обработку и asyncio с помощью run_in_executor .

4. Макс, да, это всего лишь пример кода. пользователь4815162342, спасибо, эта ссылка кажется полезной, я проверю это.

Ответ №1:

У вас есть код, который использует исключительно процессор. Подобный код нельзя ускорить с помощью async.

Асинхронность срабатывает, когда у вас есть задачи, которые ожидают чего-то, не связанного с процессором, например, сетевого запроса или чтения с диска. Как правило, это верно для всех языков.

В Python подход, основанный на потоках, также не поможет вам, поскольку это по-прежнему ограничивает вас одним ядром, а не настоящим параллельным выполнением. Это связано с глобальной блокировкой интерпретатора (GIL). Накладные расходы на запуск и переключение между потоками делают его медленнее, чем простая версия. В этом отношении потоки аналогичны асинхронности в python, это помогает только в том случае, если время ожидания не тратится в основном на процессор или если вы вызываете код, который не связан с GIL, например, расширениями C.

Использование multiprocessing действительно использует несколько ядер процессора, поэтому это быстрее, чем обычное решение.

Комментарии:

1. Итак, если у меня есть подключение к базе данных, которое может быть асинхронной библиотекой и выполнять некоторый SQL, эти вещи могут быть добавлены асинхронным способом, верно? Не могли бы вы указать мне хороший ресурс для функциональности pymssql async?

2. @johnmich вы можете использовать aiomysql, который является асинхронной библиотекой для базы данных.

Ответ №2:

 asyncio def run(time):
     await asyncio.sleep(time)
  

Этот код занимает 1 минуту 40 секунд

 from datetime import datetime
now = datetime.now()

task=[]
for i in range(10):
    await run(10)  

now1=datetime.now()
print(now1-now)
  

ОПТИМИЗИРОВАНО С ИСПОЛЬЗОВАНИЕМ async—>

Это занимает всего 10 секунд

 from datetime import datetime
now = datetime.now()

task=[]
for i in range(10):
    task.append(asyncio.create_task(run(10)))
await asyncio.gather(*task)   

now1=datetime.now()
print(now1-now)