Глубокое погружение в многопоточность и многопроцессорность в Python и их связь с параллелизмом и параллелизмом.

Введение

Потоки и многопроцессорность — две самые фундаментальные концепции программирования. Если вы какое-то время занимаетесь кодированием, вы уже должны были сталкиваться с вариантами использования, в которых вы хотели бы ускорить определенные операции в некоторых частях вашего кода. Python поддерживает различные механизмы, позволяющие выполнять различные задачи (почти) одновременно.

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

Прежде чем обсуждать многопоточность и многопроцессорность, важно понять два термина, которые часто используются взаимозаменяемо. Параллелизм и параллелизм — тесно связанные, но разные понятия.

Параллелизм и параллелизм

Во многих случаях нам может понадобиться ускорить несколько операций в нашей кодовой базе, чтобы повысить производительность выполнения. Обычно этого можно достичь, выполняя несколько задач параллельно или одновременно (т. е. чередуя несколько задач). Сможете ли вы воспользоваться параллелизмом или параллелизмом, зависит не только от вашего кода, но и от машины, на которой он выполняется.

При параллельном выполнении две или более задачи могут запускаться, выполняться и завершаться в перекрывающиеся периоды времени. Следовательно, эти задачи не обязательно должны выполняться одновременно — они просто должны выполняться перекрывающимся образом.

Параллелизм: условие, которое существует, когда хотя бы два потока работают. Более общая форма параллелизма, которая может включать разделение времени как форму виртуального параллелизма.

Руководство по многопоточному программированию Sun

Теперь давайте рассмотрим вариант использования, когда у нас есть компьютер с одним одноядерным процессором. Это означает, что задачи, которые должны выполняться как часть приложения, не могут выполняться точно в одно и то же время, поскольку процессор способен работать только над одной задачей за раз. Одновременный запуск нескольких задач означает, что процессор выполняет переключение контекста, так что несколько задач могут выполняться одновременно.

Одной из основных целей параллелизма является предотвращение блокировки задач друг друга путем переключения туда и обратно, когда одна из задач вынуждена ждать (скажем, ответа от внешнего ресурса). Например, задача А продолжается до определенного момента, ЦП перестает работать над задачей А, переключается на задачу Б и начинает работать над ней некоторое время, а затем он может переключиться обратно на задачу А, чтобы закончить ее, и, наконец, вернуться обратно к задаче А. Задание B, пока оно не закончит и это задание.

Приложение, состоящее из двух задач, которые выполняются одновременно в одном ядре, показано на диаграмме ниже.

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

Параллелизм: состояние, возникающее, когда одновременно выполняются как минимум два потока.

Руководство по многопоточному программированию Sun

Благодаря параллелизму мы можем максимально использовать аппаратные ресурсы. Рассмотрим сценарий, в котором у нас есть 16 ядер ЦП — вероятно, было бы разумнее запустить несколько процессов или потоков, чтобы использовать все эти ядра, а не полагаться только на одно ядро, пока остальные 15 простаивают.

В многоядерных средах каждое ядро ​​может одновременно выполнять одну задачу, как показано на диаграмме ниже:

Напомним, параллелизм можно рассматривать как свойство системы или программы и относится к тому, как один ЦП (ядро) может выполнять несколько задач на первый взгляд одновременно. (т. е. одновременно), в то время как параллелизм — это фактическое поведение во время выполнения выполнения как минимум двух задач буквально одновременно, параллельно. Кроме того, важно подчеркнуть, что параллелизм и параллелизм могут сочетаться во время выполнения задачи. На самом деле у нас могут быть всевозможные комбинации;

  • Ни параллельное, ни параллельное: это также известно как последовательноевыполнение, когда задачи выполняются строго друг за другом.
  • Одновременно, но не параллельно. Это означает, что задачи выполняются казалось бы одновременно, но на самом деле система переключается между различными задачами, которые выполняются одновременно, до тех пор, пока все из них казнены. Следовательно, настоящего параллелизма не существует, и поэтому никакие две задачи не выполняются в одно и то же время.
  • Параллельный, но не параллельный: это довольно редкий сценарий, когда в любой момент времени выполняется только одна задача, но сама задача разбита на подзадачи, которые обрабатываются параллельно. Однако каждая задача должна быть завершена до того, как будет взята и выполнена следующая задача.
  • Одновременно и параллельно. В основном это может происходить двумя способами. Первый — это простое параллельное и параллельное выполнение, когда приложение запускает несколько потоков, которые выполняются на нескольких процессорах и/или ядрах. Второй способ, которым это может быть достигнуто, — это когда приложение может работать над несколькими задачами одновременно, но в то же время оно также разбивает каждую отдельную задачу на подзадачи, чтобы эти подзадачи в конечном итоге могли выполняться параллельно.

Теперь, когда у нас есть общее представление о том, как работают параллелизм и параллелизм, давайте рассмотрим многопроцессорность и многопоточность, используя несколько примеров в Python.

Потоки в Python

Поток — это последовательность инструкций, которые выполняются в контексте процесса. Один процесс может порождать несколько потоков, но все они будут совместно использовать одну и ту же память.

Экспериментируя с многопоточностью в Python на задачах, привязанных к ЦП, вы в конечном итоге заметите, что выполнение не оптимизировано и может даже работать медленнее при использовании нескольких потоков. Обычно ожидается, что использование многопоточного кода на многоядерной машине должно использовать преимущества доступных ядер и, таким образом, повышать общую производительность.

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

Это ограничение фактически применяется GIL. Глобальная блокировка интерпретатора Python (GIL) предотвращает одновременное выполнение потоков внутри одного и того же процесса.

GIL — это мьютекс, который защищает доступ к объектам Python, не позволяя нескольким потокам одновременно выполнять байт-коды Python — Python Wiki

GIL необходим, потому что интерпретатор Python не является потокобезопасным. Эта глобальная блокировка применяется каждый раз, когда мы пытаемся получить доступ к объектам Python в потоках. В любой момент времени только один поток может получить блокировку для определенного объекта. Следовательно, код, привязанный к ЦП, не будет иметь прироста производительности с многопоточностью Python.

Детали реализации CPython:

В CPython из-за глобальной блокировки интерпретатора только один поток может одновременно выполнять код Python (даже несмотря на то, что некоторые библиотеки, ориентированные на производительность, могут обойти это ограничение).

Если вы хотите, чтобы ваше приложение лучше использовало вычислительные ресурсы многоядерных машин, рекомендуется использовать multiprocessing или concurrent.futures.ProcessPoolExecutor.

- Питон Документы

Вы можете узнать больше о глобальной блокировке интерпретатора Python в одной из моих недавних статей, но информации, представленной на данный момент, должно быть более чем достаточно, чтобы понять, как глобальная блокировка интерпретатора ограничивает возможности многопоточных приложений в Python (и, возможно, почему она был требуется в первую очередь).

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

В Python потоки могут быть реализованы с использованием модуля threading. Теперь давайте рассмотрим функцию, которая используется для загрузки изображения — это явно задача, связанная с вводом-выводом:

Затем давайте попробуем загрузить несколько изображений из Unsplash, используя приведенный ниже фрагмент кода. Обратите внимание, что для более наглядной демонстрации эффекта многопоточности мы намеренно пытаемся загрузить эти изображения 5 раз (см. цикл for):

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

Напомним, что многопоточность в Python позволяет создавать несколько потоков в рамках одного процесса, но из-за GIL ни один из них никогда не будет выполняться в одно и то же время. Поточность по-прежнему является очень хорошим вариантом, когда речь идет об одновременном выполнении нескольких задач, связанных с вводом-выводом. Теперь, если вы хотите воспользоваться преимуществами вычислительных ресурсов на многоядерных машинах, вам подойдет многопроцессорная обработка.

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

Многопроцессорность в Python

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

В Python многопроцессорность может быть реализована с помощью модуля multiprocessing (или concurrent.futures.ProcessPoolExecutor), который можно использовать для порождения нескольких процессов ОС. Таким образом, многопроцессорность в Python обходит GIL и вытекающие из него ограничения, поскольку теперь у каждого процесса будет свой собственный интерпретатор и, следовательно, собственный GIL.

multiprocessing — это пакет, поддерживающий создание процессов с помощью API, аналогичного модулю threading.

Пакет multiprocessing предлагает как локальный, так и удаленный параллелизм, эффективно обходя глобальную блокировку интерпретатора за счет использования подпроцессов вместо потоков.

Благодаря этому модуль multiprocessing позволяет программисту полностью использовать несколько процессоров на данной машине. Он работает как на Unix, так и на Windows.

- Питон Документы

В предыдущем разделе мы говорили о многопоточности и увидели, что многопоточность вообще не улучшает задачи, связанные с процессором. Этого можно достичь с помощью многопроцессорной обработки. Давайте воспользуемся той же функцией append_to_list(), которую мы использовали в предыдущем разделе, но на этот раз вместо threading мы собираемся использовать multiprocessing, чтобы воспользоваться преимуществами нашей многоядерной машины.

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

Теперь давайте предположим, что мы хотим запустить эту функцию дважды, как показано ниже:

И давайте time это выполнение и проверим результаты.

Теперь давайте немного рефакторим наш код и теперь используем два разных процесса, чтобы каждый вызов функции выполнялся в своем собственном процессе:

И, наконец, time и выполнение и проверка результатов:

Мы можем ясно видеть, что (хотя время user и sys осталось примерно одинаковым), время real сократилось даже более чем в два раза (и это ожидаемо, поскольку мы фактически распределили нагрузку на два разных процесса, чтобы они могли работать в параллели).

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

Последние мысли

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

Подводя итог,

Трансировка

  • Потоки совместно используют одну и ту же память и могут записывать и читать из общих переменных.
  • Из-за Python Global Interpreter Lock два потока будут выполняться не одновременно, а одновременно (например, с переключением контекста).
  • Эффективен для задач, связанных с вводом-выводом
  • Может быть реализован с модулем threading

Многопроцессорная обработка

  • Каждый процесс имеет собственное пространство памяти
  • Каждый процесс может содержать один или несколько подпроцессов/потоков.
  • Может использоваться для достижения параллелизма за счет использования преимуществ многоядерных машин, поскольку процессы могут выполняться на разных ядрах ЦП.
  • Эффективен для задач, связанных с ЦП
  • Может быть реализовано с помощью модуля multiprocessing (или concurrent.futures.ProcessPoolExecutor)

Стать участником и читать все истории на Medium. Ваш членский взнос напрямую поддерживает меня и других писателей, которых вы читаете. Вы также получите полный доступ ко всем историям на Medium.



Вам также может понравиться