Предотвращение состояния гонки в 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().

codechick

СodeСhick.io - простой и эффективный способ изучения программирования.

2024 ©