AndreyMelnikov.MyBlog # мой блог: IT-марафон.

Все мои посты

Вселенная программирования. Ключевые концепции ч3 - Параллелизм.

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

Параллелизм - независимое и потенциально одновременное выполнение инструкций. Программа конструируется из независимых частей, которые, в свою очередь, могут быть исполнены в любом порядке и это не приведет к проблеме коллизии.
Когда мы говорим о параллелизме, стоит сразу сделать оговорку. Параллелизм – как концепция независимого, одновременного выполнения, применительно к программированию, не совсем подходит. Потому, что независимые части кода могут выполняться условно одновременно на единственном процессоре, используя механизм разделения времени между процессами.
Ну, например, в языке Python (по крайней мере в его версиях на момент написания статьи) нет, как таковой, поддержки многопоточности (параллелизма) именно самим языком. Но есть много фреймворков и библиотек, которые эту возможность предоставляют, например, модуль threading или acyncio.
Однако, не стоит забывать, что интерпретатор языка - CPython использует GIL (Global Interpreter Lock). Суть GIL заключается в том, что выполнять байт код может только один поток. Это нужно для того, чтобы упростить работу с памятью (на уровне интерпретатора). В этой ситуации, условно, все задачи которые программист хочет решить при помощи параллельных вычислений можно разделить на две большие группы:

  • CPU-bound (те, что преимущественно используют процессор для своего выполнения, например, математические) – в данном случае необходимо использовать модуль multiprocessing. Этот модуль использует весь потенциал всех ядер в процессоре.

  • IO-bound (задачи, работающие с вводом-выводом: диск, сеть и т.п.) для повышения производительности в данном классе задач, необходимо использовать модули threading или acyncio.

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

Существуют две основные парадигмы внутри концепции параллелизма:

  • параллелизм с разделяемым состоянием.
  • параллелизм с обменом сообщениями.

При параллелизме с разделяемыми состоянием мы имеем дело с понятиями мониторов и транзакций.
Мониторы – специальные контролирующие структуры, через которые параллельные потоки получают доступ к общим данным.
Транзакции – операции, при которых потоки выполняют обновление общих структур данных. Но в это время доступ других потоков к этим данным блокируется.
Эта парадигма очень популярна и реализована во всех массовых языках, например, Java и C#.

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

На практике существуют несколько видов параллелизма:

  • Распределенная система. Набор компьютеров, объединенных сетью (например, структура интернета). Тут каждая независимая параллельная активность – это отдельный компьютер.

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

  • Потоки (нити). Каждая независимая параллельная активность – это поток (или нить) - thread. Потоки выполняются независимо, однако используют общую область памяти. Например, закладки внутри браузера (активности внутри одной родительской программы, выполняющейся внутри одного процесса) обычно работают в разных потоках.

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

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

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

Ниже пример кода Python с использованием многопоточности (модуль threading).

import random
import time
from threading import Thread

class MyThread(Thread):
    # создаем свой класс – наследник от класса Thread модуля threading
    def __init__(self, name):
        # инициализация потока
        Thread.__init__(self)
        self.name = name
    
    def run(self):
        # переопределяем метод запуска потока
        amount = random.randint(3, 15)
        time.sleep(amount)
        message = "%s is running" % self.name
        print(message)
    
def create_threads():
    # создаем группу потоков
    for i in range(5):
        name = "Thread #%s" % (i+1)
        my_thread = MyThread(name)
        my_thread.run()

if __name__ == "__main__":
    create_threads()

После запуска данного кода мы получим следующее:

Thread #2 is running
Thread #3 is running
Thread #1 is running
Thread #4 is running
Thread #5 is running

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

Назад к списку постов