«У параллельных линий так много общего - жаль, что они никогда не встретятся». Аноним

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

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

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

Основы потоков

В Python есть два основных модуля, реализующих потоки - thread и threading. Разница между ними в том, что последняя является объектно-ориентированной реализацией - и это то, что мы будем использовать в этой статье.

Класс Thread создается следующим образом (target - это функция, которую мы хотим выполнить в потоке):

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

После запуска поток считается активным. Поток перестает быть активным после завершения его выполнения (либо при успешном завершении задания, либо при прерывании / ошибке). Мы можем проверить состояние потока с помощью .is_alive().

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

У потоков могут быть имена (имена по умолчанию или пользовательские, передаваемые в качестве аргумента) и машинные идентификационные номера. Мы можем получить к ним доступ, используя t.name и t.ident соответственно. Более того, любой данный поток может знать свое собственное имя - это возможно с помощью функции модуля threading.currentThread().

Мы также можем передавать аргументы потокам, как в (довольно глупом) примере ниже:

Проблемы с общими данными

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

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

Один простой способ решить эту проблему - использовать блокировки с классом Python threading.Lock. Идея состоит в том, что каждый поток должен получить блокировку, если блокировка свободна. Если блокировка занята (т. Е. Была получена другим потоком), другие потоки должны ждать снятия блокировки. Мы можем проверить, свободна ли блокировка, используя метод .locked().

Итак, возвращаясь к нашему коду, мы можем решить указанную выше проблему с помощью блокировок:

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

Вы можете найти Jupyter Notebook, использованный для этой статьи, здесь.

Щелкните здесь для части II: взаимоблокировки, модель производитель-потребитель и GIL.

Использованная литература: