Python: синхронизация между потоками |
![]() |
Добавил(а) microsin |
В этой статье (перевод [1]) обсуждается концепция синхронизации потоков для многопоточного программирования на языке Python [2]. [Синхронизация между потоками] Синхронизация потоков определена как механизм, который обеспечивает невозможность для двух или большего количества потоков одновременно выполнять один и тот же сегмент программы, известный как критическая секция. Критическая секция относится к частям программы, где осуществляется доступ к общему ресурсу (такому как файл или последовательный порт). На диаграмме ниже показан пример, где 3 потока пытаются одновременно обратиться к общему ресурсу, или к критической секции. Это также называют конкурентным доступом к общему ресурсу. Без синхронизации конкурентный доступ к общему ресурсу может привести к гонке между потоками (так называемый рейсинг, race condition, и состязание потоков), когда есть риск потери целостности общего ресурса. Рейсинг происходит, когда два или большее количество потоков пытаются обратиться к общему ресурсу и изменить его одновременно. В результате значения переменных общего ресурса могут стать непредсказуемыми, и меняться в зависимости от интервалов времени переключения контекста потоков. Рассмотрим программу, которая поможет понять концепцию рейсинга: import threading Вывод программы: Итерация 0: x = 175005 В этой программе выполняются следующие действия: • В функции main_task создаются два потока t1 и t2, и глобальная переменная x устанавливается в 0. Ожидается, что в результате работы обоих потоков на каждой итерации цикла переменная x должна будет содержать значение 200000. Но очевидно, что это получается далеко не всегда. Что происходит?.. Примечание: на самом деле результат может отличаться от версии операционной системы. Показанный выше вывод с отличающимися значениями x был для программы, запущенной на Ubuntu. На Windows в каждой итерации значение x получалось 200000. Не одинаковые вычисленные значения x получаются из-за конкурентного доступа к общей переменной x. Эта непредсказуемость в значении x - не что иное, как состояние гонки. Ниже приведена диаграмма, которая показывает, как состояние гонки может возникнуть в вышеуказанной программе: Обратите внимание, что ожидаемое значение x на приведенной выше диаграмме равно 12, но из-за состояния гонки оно оказывается равным 11! Таким образом, нам нужен инструмент для правильной синхронизации между потоками. [Реализация критической секции с помощью Lock] Модуль threading предоставляет класс Lock, предназначенный для устранения гонки. Lock реализован с использованием объекта Semaphore, предоставляемого операционной системой. Что такое семафор. Семафор это объект синхронизации, который управляет доступом нескольких процессов/потоков к общему ресурсу в рабочем окружении параллельного выполнения кода. Это просто значение в назначенном месте в хранилища операционной системы (или ядра), которое каждый процесс/поток может проверить, а затем изменить. В зависимости от этого значения процесс/поток либо может использовать общий ресурс, либо обнаружит, что он уже кем-то используется, и нужно подождать некоторое время, прежде чем повторить попытку доступа. Семафоры могут быть двоичными (со значениями 0 или 1), либо могут иметь дополнительные значения (такие семафоры называются семафорами со счетчиком). Обычно процесс/поток, использующий семафор, проверяет его значение. Если семафор сигнализирует (== 0), что ресурс в настоящее время свободен, то поток изменяет значение семафора (== 1), показывая тем самым другим процессам/потокам, что ресурс занят. Класс Lock предоставляет следующие методы: acquire([blocking]): захват семафора. Захват может быть блокирующим и не блокирующим. - Когда аргумент blocking установлен в True (по умолчанию), то выполнение вызвавшего потока блокируется до тех пор, пока захват не будет снят. В момент освобождения семафор переводится в состояние захвата, и acquire возвратит True. release(): освобождение захвата семафора. Когда семафор захвачен, он освобождается, и release выполнит возврат. Если при этом любой другой поток находится в состоянии блокировки, ожидая освобождения семафора, то он выйдет из блокировки, захватит семафор и продолжит выполнение. Если семафор уже в разблокированном состоянии, то будет выброшено исключение ThreadError. Рассмотрим следующий пример, исправляющий недостаток предыдущего примера: import threading Критическая секция в этой программе (т. е. код, выполняющийся непрерывно и атомарно), находится между вызовами lock.acquire() и lock.release(). Вывод программы теперь правильный: Итерация 0: x = 200000 Давайте разберемся по шагам, что происходит в этом коде: • Сначала создается экземпляр объекта Lock вызовом его конструктора: lock = threading.Lock() • Затем lock передается как аргумент функцию потока: t1 = threading.Thread(target=thread_task, args=(lock,)) t2 = threading.Thread(target=thread_task, args=(lock,)) • В критической секции функции потока мы применили захват семафора вызовом метода lock.acquire(). Как только произошел захват семафора, никакой другой поток не сможет получить доступ к коду критической секции (в нашем примере это функция increment) пока блокировка не будет освобождена вызовом метода lock.release(). lock.acquire() increment() lock.release() Как вы можете видеть программа дает теперь каждый раз правильный результат 200000 для переменной x. Следующая диаграмма показывает, что происходит при выполнении этого кода: [Пример эффективного использования background-потока] В следующем простом примере создается фоновый поток, который читает из последовательного порта данные в глобальный буфер. Таким образом создается двухпоточное приложение - фоновый поток и основной поток. Основной поток программы читает данные из этого буфера и выводит их на экран консоли. import serial [Достоинства и недостатки многопоточности] Достоинства: • Позволяет писать программы, в которых интерфейс взаимодействия с пользователем не блокируется (программа не зависает в результате выполнения длительных вычислений). Это потому, что потоки в программе получают некоторую независимость друг от друга. Многопоточные серверы и интерактивные GUI обязательно используют многопоточность. Недостатки: • С повышением количества потоков увеличивается сложность, повышаются накладные расходы на переключение контекста. [Ссылки] 1. Multithreading in Python | Set 2 (Synchronization) site:geeksforgeeks.org. |