Рубрики
Без рубрики

Введение в параллелизм в Python

Итак, что мы имеем в виду под запуска кода одновременно? Ну, определение на уровне поверхности просто работает … С тегом Python, Codenewbie, новичков, учебным пособием.

Итак, что мы имеем в виду под запуска кода одновременно? Ну, определение на уровне поверхности просто запускает код параллельно. Но вскоре мы узнаем, что это не всегда так в некоторых сценариях. Другой вопрос: зачем нам одновременно запустить наш код? Ну, во -первых, добавляет повышение производительности в ваш код, а также заставляет работать с вашим программным обеспечением плавным. Поэтому в этом посте я собираюсь показать вам, как добавить параллелизм в ваш код Python, чтобы повысить его производительность, но во -первых, давайте получим фон того, как код Python работает нормально.

Введение

Интерпретатор Python запускает ваш код вверху внизу, что означает, что весь код зависит от кода, который выше, чтобы запустить, прежде чем он выполняется. Но это не всегда так, возьмите, например, этот фрагмент кода ниже.

def do_something():
    time.sleep(2)
    print(10)

do_something()
print(20)

В приведенном выше коде do_something Функция вызывается до того, как мы печатаем 10, она спит в течение 2 секунд, что делает интерпретатор холостое время, а затем печатает 10, прежде чем мы получим 20. Итак, представьте себе сценарий, в котором мы хотим делать что -то, когда наш переводчик бездействует в нашем случае, скажите, что мы хотим напечатать 20 или сделать что -то еще. Одним из решений этого является создание потоки Анкет

Резьба

Что такое Тема ? Хорошо, согласно Википедии, «поток исполнения – это наименьшая последовательность запрограммированных инструкций, которыми можно независимо управлять планировщиком, который обычно является частью операционной системы». О, это был полный рот, позвольте мне дать менее техническое определение, основанное на моих знаниях. Поток – это блок кода, который может работать независимо друг от друга и может быть выполнен по одному. В Python только один поток может иметь доступ к интерпретатору за раз из -за глобальной блокировки интерпретатора (GIL), поэтому он ограничивает потоки работы в параллель что мы бы обсудили позже Но вы можете прочитать больше о Gil Здесь Анкет Так как же работают темы? Что ж, когда вы слышите параллелизм, первое, что приходит на ум, – это запуск кода одновременно или параллельно, но это не так с потоками, которые они просто дают иллюзии, работающего параллельно. Потоки хранятся в памяти и запускаются только тогда, когда интерпретатор холосто настал, как в случае отправки запроса на сервер и ожидания ответа, или открыть файл и ожидание потока, любая задача ввода -вывода, которая требует ожидания, является отличным примером когда интерпретатор пролетел. Изображение ниже иллюстрирует, как работает синхронный или нормальный код Python.

Если вы соблюдаете изображение выше, вы увидите, что наша вторая операция ввода/вывода зависела от предыдущего и ждала 2 секунды, когда первая операция завершится, прежде чем он запустился, и использовал еще 2 секунды до завершения операции. Вся операция заняла около 4 секунд, и это нехорошо, взгляните на то, как использует потоки решает это.

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

Резьба на практике.

Ну, все это звучит хорошо, но давайте напишем какой -то код, чтобы по -настоящему практическое использование потоков. Сначала я напишу немного синхронного кода, чтобы увидеть, как ведет себя наш пример кода.

import time

def do_something(sec):
    print("Sleeping for %d seconds..."%sec)
    time.sleep(sec)
    print("Done Sleeping for %d seconds"%sec)

start = time.time()
do_something(1)
do_something(2)
do_something(3)
do_something(4)
do_something(5)

print("Finished in %s seconds"%(time.time() - start))

Выходы

Sleeping for 1 seconds...
Done Sleeping for 1 seconds
Sleeping for 2 seconds...
Done Sleeping for 2 seconds
Sleeping for 3 seconds...
Done Sleeping for 3 seconds
Sleeping for 4 seconds...
Done Sleeping for 4 seconds
Sleeping for 5 seconds...
Done Sleeping for 5 seconds
Finished in 15.0119268894 seconds

Мы создали функцию, которая принимает аргумент в пользу SEC, затем печатает, сколько секунд он будет спать, спят, а затем выходы, сделанные со сном для значения SEC. Затем мы назвали нашу функцию с аргументами от 1 до 5 включительно. Наша функция будет спать в течение 1, 2, 3, 4, 5 секунд, всего за 15 секунд до того, как наш код останется. И если вы соблюдаете вывод, вы увидите, что вывод функции находится в других, поэтому 1 пробежал до 2, что также продолжалось до 3 и так далее. Посмотрите, как мы можем оптимизировать наш код с помощью потоков.

import threading
import time

def do_something(sec):
    print("Sleeping for %d seconds..."%sec)
    time.sleep(sec)
    print("Done Sleeping for %d seconds"%sec)

start = time.time()
t1 = threading.Thread(target=do_something, args=(1,))
t2 = threading.Thread(target=do_something, args=(2,))
t3 = threading.Thread(target=do_something, args=(3,))
t4 = threading.Thread(target=do_something, args=(4,))
t5 = threading.Thread(target=do_something, args=(5,))

t1.start()
t2.start()
t3.start()
t4.start()
t5.start()
print("Finished in %s seconds"%(time.time() - start))

Выход

Sleeping for 5 seconds...
Sleeping for 4 seconds...
Sleeping for 3 seconds...
Sleeping for 2 seconds...
Sleeping for 1 seconds...
Finished in 0.00273704528809 seconds
Done Sleeping for 1 seconds
Done Sleeping for 2 seconds
Done Sleeping for 3 seconds
Done Sleeping for 4 seconds
Done Sleeping for 5 seconds

Я утверждаю, что код потока кажется более крупным Но уверяю вас, что это сэкономит вам время выполнения, а также с петлями вы можете сократить количество написанного кода. Итак, давайте пройдемся по коду выше, на этот раз мы импортируем потоковой модуль, он содержит все, что нам нужно для запуска потоков в Python. Наша функция остается той же, но на этот раз мы создаем Тема Объект и передайте нашу целевую функцию. Обратите внимание, что мы не используем паратезис, когда мы передаем его целевому аргументу, который автоматически вызовет функцию Итак, мы просто передаем функциональный объект, как это target = do_something вместо target = do_something () Анкет Итак, как мы можем передать аргумент в пользу нашей целевой функции? Ну, объект потока принимает аргумент args который предназначен для передачи в позиционных аргументах, а также Kwargs Для аргументов ключевых слов. Мы использовали ARG, чтобы пройти в нашем значении SEC, как SO args = (2,) В нашем примере ARG принимают любой итерабильный в качестве аргумента, например, список, Tuple, Range E.T.C. При желании мы также можем использовать Kwargs Аргумент, который принимает словарь в качестве аргумента, так что в нашем примере будет выглядеть так kwargs = {"sec": 2} Это основные аргументы, которые вам нужно знать о Тема учебный класс. Мы запускаем каждый поток с метода начала, так как мы видим, что все потоки запускаются по порядку, но обратите внимание, что мы получаем нашу среду выполнения, прежде чем все наши вызовы функции остановятся. Это преднамеренное, позвольте мне объяснить, поэтому после всех потоков создается метод start (), интерпретатор не останавливается, он продолжается, и выполняет любой синхронный код Печать “Спин за секунды”. Как мы позволим переводчику ждать, пока наши потоки закончат, прежде чем запустить какой -либо код? Ну, решение заключается в том, что мы используем Присоединяйтесь Метод для блокировки интерпретатора до тех пор, пока наши потоки не закончатся, наш код будет выглядеть так.

import threading
import time

def do_something(sec):
    print("Sleeping for %d seconds..."%sec)
    time.sleep(sec)
    print("Done Sleeping for %d seconds"%sec)

start = time.time()
t1 = threading.Thread(target=do_something, args=(1,))
t2 = threading.Thread(target=do_something, args=(2,))
t3 = threading.Thread(target=do_something, args=(3,))
t4 = threading.Thread(target=do_something, args=(4,))
t5 = threading.Thread(target=do_something, args=(5,))

t1.start()
t2.start()
t3.start()
t4.start()
t5.start()

t1.join()
t2.join()
t3.join()
t4.join()
t5.join()
print("Finished in %s seconds"%(time.time() - start))

Выходы

Sleeping for 1 seconds...
Sleeping for 2 seconds...
Sleeping for 3 seconds...
Sleeping for 4 seconds...
Sleeping for 5 seconds...
Done Sleeping for 1 seconds
Done Sleeping for 2 seconds
Done Sleeping for 3 seconds
Done Sleeping for 4 seconds
Done Sleeping for 5 seconds
Finished in 5.00275301933 seconds

Как вы можете видеть, общее время выполнения теперь выводится после того, как все потоки завершены. Таким образом, метод соединения блокирует интерпретатора от чего -либо, пока нить не будет вызовом, который не будет выполнен до завершения. Вы можете попробовать немного экспериментировать с этим кодом, попробовать перестроить другие начальные вызовы и соединения, посмотрите, что произойдет. Это также поможет вам понять, как все работает интуитивно. Мы рассмотрели потоки, теперь мы попробовали бы другой метод, который многопроцесс.

Многопроцессорная

Таким образом, в то время как потоки не позволяют вам запустить код в параллельной многопроцессорной сети. Использование многопроцессорной работы позволяет вам использовать ваш процессор. Современные процессоры поставляются с несколькими ядрами. Типичный современный ПК имеет в среднем 4 ядра, но из-за Python Gil мы не можем использовать все ядер одновременно, мы используем только одно ядро на операцию. Многопроцестра позволяет нам использовать все наши ядра, создавая процессы, которые работают на каждом ядре одновременно, опять же, это параллельно. Многообразование лучше всего сияет при выполнении задач, связанных с процессором, поэтому в этом примере вместо использования нашего манекена do_something функция Я создам функцию, которая находит все идеальные квадраты в данном диапазоне, чтобы наш код выглядел так.

import time
import math

def find_perfect_squares(n):
    print(f"Finding perfect squares for values from 1 to {n}")
    res = []
    for i in range(1, n+1):
        if math.sqrt(i) in range(1, n+1):
            res.append(i)
    print(f"Found {len(res)} values in range of {n}")
    return res


start = time.time()
find_perfect_squares(1000)
find_perfect_squares(2000)
find_perfect_squares(5000)
find_perfect_squares(3000)
find_perfect_squares(100)
finished = time.time() - start 

print("Finished in %s seconds"%(finished))

Выход

Finding perfect squares for values from 1 to 1000
Found 31 values in range of 1000
Finding perfect squares for values from 1 to 2000
Found 44 values in range of 2000
Finding perfect squares for values from 1 to 5000
Found 70 values in range of 5000
Finding perfect squares for values from 1 to 3000
Found 54 values in range of 3000
Finding perfect squares for values from 1 to 100
Found 10 values in range of 100
Finished in 8.968952894210815 seconds

Если вы заметите, вы заметите, что наша функция здесь очень вычислительно и имеет сложность времени O (n^2) Что не самое лучшее. Общее вычисление наших функциональных вызовов составляет приблизительно 9 секунд. И некоторые менее вычислительные вызовы, например, для диапазона 100 должны ждать других вычислительных вызовов, таких как диапазон 5000, мы можем запустить все это в их индивидуальном процессе, чтобы повысить нашу производительность. Multiprocessing Python API похож на то, что у потоков, поэтому обучение не будет проблемой, дайте взглянуть на то, как оптимизировать это, используя многопроцесс.

import multiprocessing
import time
import math

def find_perfect_squares(n):
    print(f"Finding perfect squares for values from 1 to {n}")
    res = []
    for i in range(1, n+1):
        if math.sqrt(i) in range(1, n+1):
            res.append(i)
    print(f"Found {len(res)} values in range of {n}")
    return res


if __name__ == '__main__':

    start = time.time()
    args = [1000, 2000, 5000, 3000, 100]
    processes = []

    for val in args:
        p = multiprocessing.Process(target=find_perfect_squares, args=(val, ))
        processes.append(p)

    for p in processes:
        p.start()

    for p in processes:
        p.join()
    finished = time.time() - start 

    print("Finished in %s seconds"%(finished))

Выход

Finding perfect squares for values from 1 to 100
Found 10 values in range of 100
Finding perfect squares for values from 1 to 1000
Found 31 values in range of 1000
Finding perfect squares for values from 1 to 2000
Found 44 values in range of 2000
Finding perfect squares for values from 1 to 3000
Found 54 values in range of 3000
Finding perfect squares for values from 1 to 5000
Found 70 values in range of 5000
Finished in 3.448301315307617 seconds

Для использования многопроцессорной работы мы импортируем многопроцессорный модуль, как и для потока. Поэтому я попытался очистить свой код на этот раз, используя для создания, запуска и присоединения к процессу. Я также сохранил весь свой объект процесса в списке. Мы использовали метод соединения так же, как и раньше, чтобы заблокировать интерпретатора, пока процесс не будет завершен. Кроме того, если мы заметим вывод, мы видим, что другой выходной сигнал не совпадает с входом здесь, первое завершение выполнения выводится, за которым следует второе, пока не наименьшее время, которое не имеет самого медленного времени выполнения.

Убедитесь, что вы создали процесс внутри Если __name__ блокировать. Смотрите Документы Больше подробностей.

Существует более простой способ создания процессов в Python, мы используем класс пула, чтобы создать пул процессов и отображать их так.

from multiprocessing import Pool
#snippet
if __name__ == "__main__":
    #snippet
    p = Pool(processes=5)
    p.map(find_perfect_squares, [1000, 2000, 5000, 3000, 100])

Мы также можем получить возвращенное значение вызова функции в нашем процессе, получив возвращенное значение метода карты, которое является списком наших возвращенных значений.

#snippet
pool = p.map(find_perfect_squares, [1000, 2000, 5000, 3000, 100])
print(pool)

Выход

[[1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961], [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 1]...]

Это обрезанный выход из -за размера, индекс возвращаемого значения совпадает с индексом входа, поэтому пул [0] будет возвращенным значением find_perfect_squares (1000) и так далее.

Когда использовать какой

Так, как я уже говорил, прежде чем потоки хорошо работают на IO, как отправка запросов на сервер, работа с файлами и любые сетевые задачи. Это не очень хорошо работает с задачей вычислительной или процессора, а иногда и худшего в этих задачах. В то время как многопроцессорная передача может быть использована для ввода -вывода задач, она также имеет свои собственные недостатки, один из которых является то, что каждая сфера памяти процесса отличается и не может получить доступ к другому. так что это ограничивает нас тем, что мы можем с этим сделать И именно поэтому мы в основном используем его для вычислений. А также в большинстве случаев вам не нужно их использовать, если вам действительно не нужно оптимизировать свой код. Но что использовать, зависит от типа вашей проблемы.

Дальнейшее чтение ресурсов

Оригинал: “https://dev.to/freddthink/an-introduction-to-concurrency-in-python-5cec”