Глава 14. Условия состязательности

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

Данная глава рассмотрит следующие вопросы:

  • Базовое понятие условия состязательности и как оно возникает в приложениях одновременной обработки совместно с определением критического раздела

  • Имитация некого условия состязательности на Python и как реализовать решение условия состязательности

  • Понятия из повседневных задач информатики, которые обычно взаимодействуют и работают с условиями состязательности

Технические требования

Вот перечень предварительных требований для данной главы:

  • Убедитесь что на вашем компьютере уже установлен Python 3

  • Вам следует иметь установленными OpenCV и NumPy для вашего дистрибутива Python 3

  • Выгрузите необходимый репозиторий из GitHub

  • На протяжении данной главы мы будем работать с вложенной папкой, имеющей название Chapter14

  • Ознакомьтесь со следующими видеоматериалами Code in Action

Концепция состояния состязательности

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

Условие состязательности может появляться как в системе электронного оборудования, так и в некотором программном приложении; в данной главе мы будем обсуждать исключительно условия состязательности в контексте разработки программного обеспечения - в частности, программных приложениях совместной обработки. Данный раздел обсудит теоретические основы состязательности и и лежащие в их корне причины, а также понятие критического раздела.

Критичные разделы

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

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

В своём следующем подразделе мы обсудим взаимосвязь между критическими разделами и причинами условий состязательности.

Как появляется состязательность

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

Допустим, что нашим разделяемым численным значением изначально было 2, а затем поток 1 осуществляет доступ и взаимодействует с этим численным значением; значением разделяемого ресурса становится 3. После того как поток 1 успешно изменит и покинет данный ресурс, свои инструкции начнёт исполнять поток 2, а являющийся численным значением совместный ресурс обновится до 4. На протяжении данного процесса, численное значение, изначально равное 2, было увеличено дважды (всякий раз обособленным потоком) и по окончанию содержит значение 4. В данном случае разделяемое численное значение не подвергалось неверной обработке и разрушениям.

Далее представим себе некую ситуацию, при которой данное разделяемое численное значение в начале также составляло 2, но в этот раз оба имеющихся потока получили к нему доступ одновременно. Теперь, когда каждый из потоков считал из этого совместного ресурса 2, каждый из из них по отдельности увеличивает значение с 2 до 3, а затем каждый записывает это значение 2 обратно в свой разделяемый ресурс. Даже несмотря на то, что данный совместный ресурс испытал доступ и взаимодействие с потоками дважды, по окончанию данного процесса он содержит только значение 3.

Это некий пример условия состязательности,происходящего в программе совместной обработки: так как второй поток выполнил доступ к совместному ресурсу до того как первый поток завершил своё исполнение (иначе: записал своё новое значение в данный разделяемый ресурс), наш второй поток не смог получить некое обновлённое значение. Это приводит к тому факту, что когда наш второй поток выполняет запись в разделяемый ресурс, то значение, которое было выработано и обновлено первым потоком перезаписывается. По окончанию исполнения этих двух потоков, наш совместный ресурс технически был обновлён только вторым потоком.

Приводимая ниже схема дополнительно иллюстрирует противопоставление процесса правильной обработки данных и ситуацию с условием состязательности:

 

Рисунок 14-1


Неверная обработка разделяемых данных

Интуитивно ясно, что мы можем наблюдать в результате условия состязательности неверную обработку и разрушение данных. В своём предыдущем примере мы могли наблюдать, что условие состязательности могло произойти только с двумя обособленными потоками, осуществляющими доступ к общему ресурсу, что явилось причиной неверного обновления разделяемого ресурса и неверной фиксации значения по окончанию данной программы. Мы знаем, что большинство совместных приложений в реальной практике содержит значительно больше потоков и процессов и больше совместных ресурсов, а чем больше потоков/ процессов взаимодействует в разделяемыми ресурсами, тем более вероятно что случится условие состязательности. {Прим. пер.: более динамичный и запоминающийся пример состязательности можно найти в нашем переводе Ресторан Серийных ботов из вышедшей в апреле 2018 в издательстве O’Reilly Media, Inc. книги Цалеб Хаттингх "Asyncio в Python 3"}

Имитация состояния состязательности в Python

Прежде чем приступать к обсуждению решения, которое мы можем реализовать для решения проблемы условий состязательности, давайт попробуем смоделировать эту проблему на Python. Если вы уже выгрузили код для данной книги с его страницы в GitHub, пройдите далее и переместитесь в папку Chapter14. Давайте рассмотрим свой файл Chapter14/example1.py, а в частности, следующую функцию update():


# Chapter14/example1.py

import random
import time

def update():
    global counter

    current_counter = counter # reading in shared resource
    time.sleep(random.randint(0, 1)) # simulating heavy calculations
    counter = current_counter + 1 # updating shared resource
 	   

Основная цель предыдущей функции update() состоит в приращении значения глобальной переменной с названием counter, причём она будет вызываться обособленными потоками в нашем сценарии. Внутри этой функции мы взаимодействуем с неким разделяемым ресурсом - в данном случае с counter. Затем мы назначаем значение переменной counter другой локальной переменной с названием current_counter (которое является также приращением на единицу).

Далее мы приостанавливаем исполнение данной функции при помощи метода time.sleep(). Продолжительность периода, на протяжении которого данная программа будет пребывать в паузе выбирается случайным образом из 0 и 1, что вырабатывается вызовом функции random.randint(0, 1), поэтому данная программа либо будет задержана в паузе на одну секунду, либо нет. Наконец, мы назначаем вновь вычисленное значение current_counter (которое увеличивается на единицу) первоначальному разделяемому ресурсу (своей переменной counter).

Теперь мы можем перейти к своей основной программе:


# Chapter14/example1.py

import threading

counter = 0

threads = [threading.Thread(target=update) for i in range(20)]

for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(f'Final counter: {counter}.')
print('Finished.')
 	   

Здесь мы инициализируем свою глобальную переменную counter с помощью настройки объектов threading.Thread, для того чтобы исполнять свою функцию update() параллельно; мы инициализируем двадцать объектов потоков для взаимодействия со своим разделяемым счётчиком двадцать раз. После запуска и соединения всех имеющихся у нас потоков мы, наконец, можем вывести на печать своё окончательное значение совместной переменной counter.

Теоретически, хорошо спроектированная программа совместной обработки инкрементально увеличивает данный разделяемый счётчик в целом двадцать раз и, так как его первоначальным значением было 0, окончательным значением данного счётчика по окончанию наше программы должно быть 20. Тем не менее, после того как вы исполните данный сценарий, получаемое вами значение counter, скорее всего по окончанию программы не будет содержать значение 20. Ниже приводится вывод, полученный мной после запуска данного сценария:


> python3 example1.py
Final counter: 9.
Finished.
		

Это вывод показывает, что наш счётчик был успешно увеличен на 1 девять раз. Именно это является прямым результатом условия состязательности, которое имеет наша программа совместной обработки. Это условие состязательности происходит, когда некий конкретный поток тратит своё время на считывание и обработку данных из нашего совместного ресурса (в частности, одну секунду пр помощи метода time.sleep()), а другой поток считывает то же самое численное текущее значение из совместной переменной counter, которое к этому моменту пока не было изменено нашим первым потоком, так как он пока не завершил своё исполнение.

Что интересно, если некий поток не тратит какое бы то ни было время на обработку этих данных (иными словами, когда нашим псевдо- случайным методом random.randint() выбирается 0), данное значение совместного ресурса может быть потенциально изменено именно в то время, когда наш следующий поток считывает и обрабатывает его. Это явление иллюстрируется тем фактом, что наше окончательное значение счётчика изменяется при различных запусках данной программы. Например, после запуска данного сценария три раза я полуследующие выходные данные. После первого раза я получил:


> python3 example1.py
Final counter: 9.
Finished.
		

Вывод второго исполнения таков:


>  python3 example1.py
Final counter: 12.
Finished.
		

На третий раз я получил следующее:


> python3 example1.py
Final counter: 5.
Finished.
		

И опять же, окончательное значение счётчика зависит от числа потоков, затрачивающих одну секунду на паузу и общего числа не тратящего своего времени на паузу. Так как эти два числа, в свою очередь, зависят от метода random.randint(), окончательное значение нашего счётчика изменяется при различных запусках нашей программы. Мы всё ещё будем иметь условие состязательности в своей программе, за исключением тех случаев, когда окончательное значение счётчика всегда будет составлять 20 (наш счётчик в сумме увеличился на единицу двадцать раз).

Блокирование в качестве решения состояния состязательности

В этом разделе мы обсудим наиболее распространённое решение для условий состязательности: блокировки. Интуитивно ясно, что так как условия состязательности, которые наблюдаются возникающими при одновременном доступе или записи в разделяемые ресурсы со стороны множества потоков или процессов, ключевая мысль для решения условий состязательности состоит в изоляции исполнения различных потоков/ процессов, в особенности, когда они взаимодействуют с некими совместно используемыми ресурсами. В частности, нам требуется гарантировать что некий поток/ процесс сможет получить доступ к определённому совместному ресурсу только после того как другой поток/ процесс, взаимодействующий с данным ресурсом завершит своё взаимодействие с этим ресурсом.

Эффективное блокирование

при применении блокирования мы можем превратить некий разделяемый ресурс в какой- то программе совместной обработки в критический раздел, целостность данных которого гарантированно будет защищена. Некий критический раздел обеспечивает взаимное исключение совместного ресурса и доступ к нему не может осуществляться одновременно множеством процессов или потоков; ото предотвратит обновление или изменение любых защищённых данных несовместимой информацией, получающейся в результате условий состязательности.

На следующей схеме Потоку B заблокирован доступ к совместному ресурсу - нашему критическому разделу с названием var - неким блокирующим семафором (mutex, mutual exclusion, взаимным исключением), так как Поток A уже осуществляет доступ к этому ресурсу.

 

Рисунок 14-2


Блокировки предотвращают одновременный доступ к некоторому критическому разделу

Теперь мы определим что для того чтобы получить доступ к некоторому критическому разделу в программе совместной обработки потоку или процессу требуется выполнить блокирование некоторого объекта, который связан с этим критическим разделом; аналогично, этому потоку или процессу также требуется высвободить такую блокировку в процессе оставления данного критического раздела. Такая настройка действенно избавляет от множественного доступа к данному критическому разделу и тем самым предотвращает условия состязательности. приводимая ниже схема иллюстрирует имеющийся поток исполнения множества потоков, взаимодействующих со множеством критических разделов с реализацией в этом месте блокировок:

 

Рисунок 14-3


Блокировки и критические разделы во множестве потоков

Как вы можете видеть на этой схеме, и поток T1, и поток T2, оба взаимодействуют с критическими разделами в процессе исполнения своих инструкций: CS1, CS2 и CS3. Здесь T1 и T2 пытаются выполнить доступ к CS1 почти одновременно и, так как CS1 защищена блокировкой L1, только T1 способен осуществить блокирование L1 и, тем самым, получить доступ/ взаимодействовать с данным критическим разделом, в то время как T2 проводит своё время в ожидании пока T1 выйдет из данного критического раздела и высвободит это блокирование до доступа к самому разделу. Аналогичные действия выполняются и для прочих критических разделов, CS2 и CS3, хотя оба потока требуют доступа к критическим разделам почти одновременно, только один может его выполнить, в то время как другому приходится дожидаться получения блокировки, связанной с конкретным критическим разделом.

Реализация в Python

Теперь давайте реализуем это описание из предыдущего примера чтобы разрешить имеющуюся проблему условий состязательности. Перейдите к файлу Chapter14/example2.py и рассмотрите нашу исправленную следующим образом функцию update():


# Chapter14/example2.py

import random
import time

def update():
    global counter

    with count_lock:
        current_counter = counter # reading in shared resource
        time.sleep(random.randint(0, 1)) # simulating heavy calculations
        counter = current_counter + 1
 	   

Вы можете видеть, что все необходимые для исполнения инструкции, определяемые в нашей функции update() находятся под диспетчером контекста некоторого объекта блокировки с названием count_lock. Поэтому всякий раз когда некий поток вызывается для запуска данной функции, ему придётся вначале овладеть данным объектом блокировки до того как может быть выполнена какая- то инструкция. В своей основной программе мы просто создаём необходимый объект блокировки в добавление к тому что у нас имеется следующим манером:


# Chapter14/example2.py

import threading

counter = 0
count_lock = threading.Lock()

threads = [threading.Thread(target=update) for i in range(20)]

for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(f'Final counter: {counter}.')
print('Finished.')
 	   

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


> python3 example2.py
Final counter: 20.
Finished.
		

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

Обратная сторона блокирования

В Главе 12, Взаимные блокировки мы рассмотрели интересное явление при котором применение блокирования приводит к нежелательным результатам. В частности, мы обнаружили, что при достаточной реализации блокировок в некоторой программе совместной обработки вся программа становится последовательной. Рассмотрим следующий пример из файла Chapter14/example3.py:


# ch14/example3.py

import threading
import random; random.seed(0)
import time

def update(pause_period):
    global counter

    with count_lock:
        current_counter = counter # reading in shared resource
        time.sleep(pause_period) # simulating heavy calculations
        counter = current_counter + 1 # updating shared resource

pause_periods = [random.randint(0, 1) for i in range(20)]

###########################################################################

counter = 0
count_lock = threading.Lock()

start = time.perf_counter()
for i in range(20):
    update(pause_periods[i])

print('--Sequential version--')
print(f'Final counter: {counter}.')
print(f'Took {time.perf_counter() - start : .2f} seconds.')

###########################################################################

counter = 0

threads = [threading.Thread(target=update, args=(pause_periods[i],)) for i in range(20)]

start = time.perf_counter()
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print('--Concurrent version--')
print(f'Final counter: {counter}.')
print(f'Took {time.perf_counter() - start : .2f} seconds.')

###########################################################################

print('Finished.')
 	   

Превращение параллельной программы в последовательную

Основная цель данного примера состоит в сопоставлении скоростей параллельной программы и её последовательной версии. Здесь мы всё ещё применяем ту же самую функцию update(), использующую блокировки, причём мы запускаем её двадцать раз и последовательно, и параллельно, как мы это делали и ранее. Мы также создаём некий перечень предопределённых периодов задержек с тем, чтобы эти периоды находились в согласии между тем кода мы имитируем свои последовательную и параллельную версии (по этой причине наша функция update() теперь получает некий параметр, который определяет значение задержки при каждом вызове):


pause_periods = [random.randint(0, 1) for i in range(20)]
 	   

На протяжении следующего шага своей программы мы просто вызываем функцию update() внутри цикла for при помощи двадцати итераций, отслеживая сколько времени потребуется на завершение данного цикла. Отметим, что несмотря на что это всего лишь служит для имитации последовательной версии нашей программы, нашей функции update() всё ещё требуется вначале создавать блокировку объекта, по этой причине нам необходимо проинициализировать её здесь:


counter = 0
count_lock = threading.Lock()

start = time.perf_counter()
for i in range(20):
    update(pause_periods[i])

print('--Sequential version--')
print(f'Final counter: {counter}.')
print(f'Took {time.perf_counter() - start : .2f} seconds.')
 	   

Самый последний шаг состоит в сбросе значения счётчика и и запуске версии программы с совместной обработкой, которую мы уже реализовали. И снова нам потребуется передать соответствующее значение времени задержки при инициализации каждого потока, который запускает нашу функцию update(). Мы также отслеживаем то значение времени, которое требуется для выполнения данной версии программы совместной обработки:


counter = 0

threads = [threading.Thread(target=update, args=(pause_periods[i],)) for i in range(20)]

start = time.perf_counter()
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print('--Concurrent version--')
print(f'Final counter: {counter}.')
print(f'Took {time.perf_counter() - start : .2f} seconds.')
 	   

Теперь, после того как вы исполнили данный сценарий, вы можете обнаружить, что и последовательной и параллельной версии нашей программы потребовалось одно и то же время исполнения. В частности, ниже я привожу полученный мной вывод; в данном случае потребовалось около 12 секунд. Получаемое вами в реальности время может отличаться, однако скорость обеих версий всё ещё будет одинаковой:


> python3 example3.py
--Sequential version--
Final counter: 20.
Took 12.03 seconds.
--Concurrent version--
Final counter: 20.
Took 12.03 seconds.
Finished.
		

Итак, наша версия совместной обработки требует ровно столько же времени, что и её последовательная версия, тем самым теряя одну из самых больших целей реализации в программе совместной обработки: улучшения скорости. Но почему же параллельная и традиционная последовательная версии с одними и теми же наборами наборами инструкций и элементов также имеют и одну и ту же скорость? Должна ли всегда наша программа совместной обработки производить более быструю скорость нежели её последовательный аналог?

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

Именно с этим явлением мы сталкивались при анализе своей проблемы взаимного блокирования: если в некую программу совместной обработки поместить достаточное число блокировок, такая программа превращается в полностью последовательную. Именно по этой причине блокировки порой не желательны для решения проблем в программировании совместной обработки. Тем не менее, данная ситуация происходит только если всё исполнение подобной программы совместной обработки зависит от взаимодействия с её критическим разделом. В большинстве случаев чтение таких данных совместных ресурсов и манипуляции с ними являются всего лишь некоторой частью всей программы в целом, а следовательно совместная обработка всё ещё производит ожидаемую дополнительную скорость нашего приложения.

Блокировки ничего не блокируют

Дополнительной стороной блокировок является тот факт, что на самом деле они ничего не блокируют. Единственный способ которым применяется некий блокируемый объект в отношении какого- то конкретного совместного ресурса, состоит в том, что для взаимодействия потоков и процессов с таким ресурсом и им также необходимо взаимодействовать и самой блокировкой. Иными словами, если такие потоки и процессы останавливаются на том чтобы не проверять необходимую блокировку перед доступом к данному совместному ресурсу и его изменению, этот блокирующий объект сам по себе не способен прекратить их подобные действия.

В нашем примере вы уже видели, что для реализации процесса достижения/ высвобождения некоторого объекта блокирования соответствующие инструкции некоторого потока или процесса должны быть обёрнуты неким диспетчером контекста блокировки; это определение зависит от самой реализации логики исполнения данного потока/ процесса, а не самого ресурса. Это происходит по причине того, что те объекты блокирования, которые мы наблюдаем, не связаны с самими поддерживаемыми ими ресурсами ни коим образом. Следовательно, если логика исполнения определённого потока/ процесса не требует никакого взаимодействия с тем блокирующим объектом, который связан с запрашиваемым совместным ресурсом, такой поток или процесс может без труда запросто получить доступ к данному ресурсу, чо потенциально может приводить к неверному манипулированию и разрушению данных.

Это верно не только в сфере действия наличия множества потоков и процессов в некоторой отдельной программе совместной обработки. Предположим что у нас имеется некая система совместной обработки, состоящая из множества компонентов которые все взаимодействуют и манипулируют определёнными данными неких ресурсов, разделяемых для всей системы, а эти ресурсы связаны с объектами блокирования; отсюда следует, что если любой из этих компонентов прекращает взаимодействовать с данной блокировкой, он может просто обходить ту защиту, которая реализуется данной блокировкой и выполнять доступ к данному совместному ресурсу. Что ещё более важно, такая характеристика блокировки также имеет последствия, относящиеся к безопасности программ совместной обработки. Если некий внешний вредоносный агент подключился к нашей системе (допустим, некий клиент, взаимодействующий с каким- то сервером), причём имеет целью разрушить имеющиеся совместные данные во всей системе, такой агент может иметь инструкции просто игнорировать все объекты блокировки и осуществлять доступ к этим данным неким навязчивым образом.

Такая точка зрения что блокировки на самом деле ничего не блокируют была популяризована Рэймондом Хэттингером, разработчиком ядра Python, который задействован в реализации различных элементов параллельного программирования Python. Он выдвигает аргумент, что использование блокирующих объектов самих по себе не гарантирует никакой безопасности реализации структур данных и систем совместной обработки. Блокировки обязаны быть конкретно связанными с теми ресурсами, что они защищают и ничто не должно иметь возможности получать доступ к некоторому ресурсу без предварительного запроса блокировки, которая связана с таким ресурсом. Для решения данной проблемы, в качестве альтернативы могут предоставляться иные решения, наприме, атомарные очереди обмена сообщениями.

Состояние состязательности в реальной жизни

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

Безопасность

Программирование совместной обработки может иметь значительные последствия для вопросов безопасности системы в целом. Напомним, что некое условие состязательности возникает между имеющимися процессами чтения и изменения данных некоего ресурса; некое условие состязательности в системе аутентификации может приводить к искажению данных между значением временем проверки (когда необходимые полномочия данного агента были удостоверены) и значением времени использования (когда этот агент может применять данный ресурс). Данная проблема также именуется как ошибка TOCTTOU (Time-Of-Check-To-Time-Of-Use), которая несомненно причиняет ущерб системам безопасности.

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

Что интересно, что условия состязательности могут также применяться для реализации вычислительной безопасности. Так как условия состязательности являются результатом определённого не координированного доступа множества потоков/ процессов к каким- то совместным ресурсам, на своём собственном примере мы видели, что при имитации некоего условия состязательности получаемый в конце концов результат имеющегося счётчика разнится для различных исполнений данной программы; это (частично) происходит по причине самой непредсказуемой природы данной ситуации, при которой запущено и осуществляют доступ к определённому совместному ресурсу множество потоков. (Я произнёс частично, так как случайность также является результатом значений произвольных задержек, которые вырабатываются при каждом исполнении данной программы.) Итак, условия состязательности порой провоцируются намеренно и та информация, которая получается при возникновении условий состязательности может применяться для выработки цифровых отпечатков пальцев для процессов безопасности - опять- таки, эта информация достаточно случайная и следовательно может применяться для целей безопасности.

Операционные системы

Условия состязательности могут проявляться в контексте управления некоторой операционной системой файлами и памятью, когда две обособленные программы пытаются осуществить доступ к одному и тому же ресурсу, например, пространству оперативной памяти. Допустим некую ситуацию, при которой два процесса из различных программ исполняются на протяжении значительного времени и, даже несмотря на то, что они инициализировались по- отдельности друг от друга в отношении пространства памяти, был накоплен значительный объём данных и стек исполнения одного процесса теперь вступает в соприкосновение со стеком другого процесса. Это может повлечь за собой то, что такие два процесса совместно используют одну и ту же часть пространства оперативной памяти и могут в конечном счёте иметь результатом непредсказуемые последствия.

Другая сторона сложности условий состязательности иллюстрируется операционной системой Unix в версии 7 - в частности, в команде mkdir. Обычно команда mkdir применяется для создания какого- то нового каталога в самой операционной системе Unix; это выполняется вызовом соответствующей команды mknod для создания некоего нового каталога и команды chown для определения владельца этого каталога. Так как имеются подлежащие исполнению две команды, а также имеется некий зазор между тем как завершится первая команда и будет вызвана вторая, это может повлечь за собой условие состязательности.

На протяжении имеющегося промежутка между этими двумя командами если некто может удалить тот новый каталог, который был создан командой mknod и присоединить свою ссылку на новый файл в момент исполнения команды chown, будет изменён владелец этого файла. Эксплуатируя данную уязвимость некто теоретически может изменять владельца любого файла в операционной системе, когда кто- то создаёт некий новый каталог. Приводимая ниже схема дополнительно иллюстрирует такую эксплуатацию.

Сетевые среды

В сетевых средах условия состязательности могут принимать форму получения многими пользователями уникальных полномочий в некоторой сетевой среде. В частности, допустим что некий определённый сервер должен иметь только в точности одного пользователя с привилегиями администратора. Если два пользователя, оба из которых имеют право стать администраторами данного сервера запрашивают доступ к таким привилегиям одновременно, тогда имеется вероятность что они оба получат такой доступ. Это происходит по той причине, что на тот момент когда були получены запросы обоих этих пользователей данным сервером, ни один из этих пользователей ещё не получил полномочий администратора и данный сервер предполагает, что полномочия такого администратора всё ещё могут быть выданы.

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

Выводы

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

Так как наблюдавшиеся нами условия состязательности проявляются когда множество потоков или процессов одновременно выполняют доступ и запись в некий совместный ресурс, основная ключевая мысль для решения условия состязательности состоит в изолировании различных потоков/ процессов, в особенности при взаимодействии с разделяемым ресурсом. При блокировании мы можем превращать некий совместный ресурс в какой- то программе совместной обработки в критический раздел, целостность данных которого может быть гарантировано защищена. Тем не менее, существует ряд недостатков применения блокировок: при реализации достаточного числа блокировок в некоторой программе совместной обработки вся программа в целом может стать последовательной; блокировки в действительности ничего не блокируют.

В своей следующей главе мы рассмотрим одну из величайших проблем в параллельном программировании Python: имеющем дурную репутацию GIL (Global Interpreter Lock, Глобальном блокировании интерпретатора). Вы ознакомитесь с основной идеей, стоящей за GIL, его целями и тем как действенно работать с ним в параллельных приложениях Python.

Вопросы

  • Что такое критический раздел?

  • Что представляет из себя условие состязательности и почему оно не желательно в программе совместной обработки?

  • В чём состоят причины, лежащие в основе условий состязательности?

  • Как блокирование может разрешить общую проблему условий состязательности?

  • Почему блокировки порой не желательны в программах совместной обработки?

  • Какие существенные условия состязательности имеются в практических системах и приложениях?

Дальнейшее чтение

Для получения дополнительных сведений вы можете воспользоваться следующими ссылками: