Предотвращение состояния гонки в Python
В этой статье вы узнаете о состоянии гонки (race condition) и о том, как использовать класс Lock модуля threading для его предотвращения.
Что такое состояние гонки в Python
Состояние гонки (race condition) возникает, когда два потока одновременно пытаются получить доступ к общей переменной.
Представьте, что у нас есть два потока. Один поток считывает значение из некой общей переменной, а потом то же самое делает второй поток.
Затем оба потока пытаются изменить значение общей переменной. Они начинаются соревоваться в том, какой поток запишет значение в переменную последним.
Сохранится только значение от потока, который записывает значение в общую переменную последним, потому что он перезапишет предыдущее значение.
Пример состояния гонки
Следующий код демонстрирует состояние гонки:
from threading import Thread
from time import sleep
counter = 0
def increase(by):
global counter
local_counter = counter
local_counter += by
sleep(0.1)
counter = local_counter
print(f'Значение counter: {counter}')
# создаем потоки
t1 = Thread(target=increase, args=(10,))
t2 = Thread(target=increase, args=(20,))
# запускаем потоки
t1.start()
t2.start()
# ждем завершения потоков
t1.join()
t2.join()
print(f'Значение counter в итоге: {counter}')
В этой программе оба потока пытаются одновременно изменить значение переменной counter
. Значение переменной counter
зависит от того, какой поток завершится последним.
Вывод 1
Если поток t1
завершится раньше потока t2
, вывод будет таким:
Значение counter: 10
Значение counter: 20
Значение counter в итоге: 20
Вывод 2
Если поток t2
завершится раньше потока t1
, вывод будет таким:
Значение counter: 20
Значение counter: 10
Значение counter в итоге: 10
Как это работает
1. Импортируем класс Thread
из модуля threading
и функцию sleep()
из модуля time
:
from threading import Thread
from time import sleep
2. Создаем глобальную переменную counter
со начальным значением 0:
counter = 0
3. Создаем функцию increase
, которая увеличивает значение переменной counter
на некое число by
:
def increase(by):
global counter
local_counter = counter
local_counter += by
sleep(0.1)
counter = local_counter
print(f'Значение counter: {counter}')
4. Создаем два потока. Первый поток будет увеличивать counter
на 10, а второй — на 20:
t1 = Thread(target=increase, args=(10,))
t2 = Thread(target=increase, args=(20,))
5. Запускаем потоки:
t1.start()
t2.start()
6. Дожидаем в основном потоке завершения потоков t1
и t2
:
t1.join()
t2.join()
7. Выводим на экран конечное значение переменной counter
:
print(f'Значение counter: {counter}')
Класс Lock для предотвращения состояния гонки
Для предотвращения условий гонки можно использовать класс Lock
из модуля threading
. У экземпляра класса Lock есть два состояния: он может быть заблокирован и разблокирован.
Для начала нужно создать экземпляр класса Lock
:
lock = Lock()
По умолчанию lock
разблокирован — пока вы прямо не укажете обратного.
Чтобы заблокировать lock
, нужно использовать метод acquire()
.
Когда поток завершит изменение общей переменной, lock
нужно разблокировать — с помощью метода release()
.
Пример предотвращения состояния гонки
В следующем примере показано, как использовать объект класса Lock
для предотвращения состояния гонки в предыдущей программе:
from threading import Thread, Lock
from time import sleep
counter = 0
def increase(by, lock):
global counter
lock.acquire()
local_counter = counter
local_counter += by
sleep(0.1)
counter = local_counter
print(f'Значение counter: {counter}')
lock.release()
lock = Lock()
# создаем потоки
t1 = Thread(target=increase, args=(10, lock))
t2 = Thread(target=increase, args=(20, lock))
# запускаем потоки
t1.start()
t2.start()
# ждем завершения потоков
t1.join()
t2.join()
print(f'Значение counter в итоге: {counter}')
Вывод
Значение counter: 10
Значение counter: 30
Значение counter в итоге: 30
Как это работает
1. Добавляем второй параметр в функцию increase()
.
2. Создаем экземпляр класса Lock
.
3. Блокируем экземпляр перед обращением к переменной counter и разблокируем его после записи нового значения.
Пишем класс Counter, который использует объект Lock
Ниже показано, как создать класс Counter
, который использует объект класса Lock
:
from threading import Thread, Lock
from time import sleep
class Counter:
def __init__(self):
self.value = 0
self.lock = Lock()
def increase(self, by):
self.lock.acquire()
current_value = self.value
current_value += by
sleep(0.1)
self.value = current_value
print(f'Значение counter: {self.value}')
self.lock.release()
counter = Counter()
# создаем потоки
t1 = Thread(target=counter.increase, args=(10, ))
t2 = Thread(target=counter.increase, args=(20, ))
# запускаем потоки
t1.start()
t2.start()
# ждем завершения потоков
t1.join()
t2.join()
print(f'Значение counter в итоге: {counter.value}')
Что нужно запомнить
1. Состояние гонки возникает, когда два потока одновременно обращаются к общей переменной.
2. Для предотвращения состояния гонки можно использовать класс Lock
.
3. Для блокировки объекта класса Lock
используйте метод acquire()
.
4. Для разблокировки объекта класса Lock
используйте метод release()
.