Глава 13. Зависание

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

В данной главе будут рассмотрены такие темы:

  • Основная мысль, стоящая за зависаниями, корни их появления и некоторые более существенные понятия

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

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

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

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

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

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

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

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

Понятие зависания

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

Что такое зависание?

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

  • Один отвечает за обработку находящихся под большим давлением инструкций, которые необходимо запускать как только необходимые им ресурсы становятся доступными

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

  • Самый последний обрабатывает различные, причём очень редкие задачи

Более того, этим трём процессам требуется применять одни и те же ресурсы чтобы исполнять свои соответствующие предписания.

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

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

Составление расписания

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

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

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

Допустим, что в вашей параллельной программе у вас имеется процесс A с наивысшим приоритетом, процесс B со средним приоритетом и, наконец, процесс C с самым низким приоритетом; с большой долей вероятности процесс C будет помещён в состояние зависания. Кроме того, если само исполнение процесса A, самого приоритетного процесса, зависит от исполнения процесса C, который уже завис, тогда процесс A может иметь возможность никогда не завершить своё исполнение, причём даже не взирая на то что ему придан самый высокий приоритет в данной параллельной программе.

Наша следующая схема предоставляет дополнительную иллюстрацию изменения порядка приоритета: задаче с высоким приоритетом требуется с момента времени t2 до t3 доступ к неким ресурсам, которые используются задачей с низким приоритетом:

 

Рисунок 13-1


Схема изменения порядка приоритетов

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

Ситуации зависания

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

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

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

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

Взаимосвязь зависаний с взаимной блокировкой

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

 

Рисунок 13-2


Иллюстрация проблемы Обедающих философов

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

Проблема читатели- писатели

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

Постановка задачи

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

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

Следующая схема дополнительно иллюстрирует настройки задачи читатели- писатели:

 

Рисунок 13-3


Схема проблемы читатели- писатели

Первая проблема читатели- писатели

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

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

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

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

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

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


# Chapter13/example1.py

def writer():
    global text

    while True:
        with resource:
            print(f'Writing being done by 
                   {threading.current_thread().name}.')
            text += f'Writing was done by 
                    {threading.current_thread().name}. '

def reader():
    global rcount

    while True:
        with rcounter:
            rcount += 1
            if rcount == 1:
                resource.acquire()

        print(f'Reading being done by 
               {threading.current_thread().name}:')
        print(text)

        with rcounter:
            rcount -= 1
            if rcount == 0:
                resource.release()
 	   

В нашем предыдущем примере функция writer(), которая подлежит вызову неким экземпляром threading.Thread (говоря иначе, обособленным потоком), определяет ту логику наших потоков писателей, которую мы обсуждали ранее: доступ к некоторому совместному ресурсу (в данном случае к глобальной переменной, text, которая является просто некоторой строкой Python) и запись неких данных в этот ресурс. Заметим, что мы помещаем все его инструкции внутри некоторого цикла while, для имитации природы постоянства данного приложения (писатели и читатели постоянно предпринимают попытки доступа к данному совместному ресурсу).

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

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

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


# Chapter13/example1.py

text = 'This is some text. '
rcount = 0

rcounter = threading.Lock()
resource = threading.Lock()

threads = [threading.Thread(target=reader) for i in range(3)] + [threading.Thread(target=writer) for i in range(2)]

for thread in threads:
    thread.start()
 	   

Важно отметить, что поскольку все инструкции потоков читателей и писателей завёрнуты вовнутрь циклов while, этот сценарий, после своего запуска, будет исполняться бесконечно. Вам потребуется прекратить исполнение Python по истечению 3- 4 секунд, после того как будет воспроизведён достаточный вывод с тем чтобы составить общее представление о поведении данной программы.

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


> python3 example1.py
Reading being done by Thread-1:
This is some text. 
Reading being done by Thread-2:
Reading being done by Thread-1:
This is some text. 
This is some text. 
Reading being done by Thread-2:
Reading being done by Thread-1:
This is some text. 
This is some text. 
Reading being done by Thread-3:
Reading being done by Thread-1:
This is some text. 
This is some text. 
...
		

Как вы можете обратить внимание, существует определённый шаблон в нашем предыдущем выводе: всеми теми потоками, которые выполнили доступ к нашему совместному ресурсу были читатели. Фактически, на протяжении всего моего вывода ни один из писателей не получил возможности доступа к данному файлу и следовательно, наша переменная text содержит только первоначальную строку, This is some text., и не изменялась ни коим образом. Тот вывод, который вы получите, также должен иметь тот же самый шаблон (наш совместный ресурс не подвергался изменениям).

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

Данный алгоритм непреднамеренно предоставляет преимущество всем читателям над писателями и, следовательно, именуется как отдающий предпочтение читателям. Поэтому данное решение нежелательно.

Вторая проблема читатели- писатели

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

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

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

Наш файл Chapter13/example2.py содержит сам код для такой реализации:


# Chapter13/example2.py

import threading

def writer():
    global text
    global wcount

    while True:
        with wcounter:
            wcount += 1
            if wcount == 1:
                read_try.acquire()

        with resource:
            print(f'Writing being done by 
                  {threading.current_thread().name}.')
            text += f'Writing was done by 
                  {threading.current_thread().name}. '

        with wcounter:
            wcount -= 1
            if wcount == 0:
                read_try.release()

def reader():
    global rcount

    while True:
        with read_try:
            with rcounter:
                rcount += 1
                if rcount == 1:
                    resource.acquire()

            print(f'Reading being done by 
                  {threading.current_thread().name}:')
            print(text)

            with rcounter:
                rcount -= 1
                if rcount == 0:
                    resource.release()

text = 'This is some text. '
wcount = 0
rcount = 0

wcounter = threading.Lock()
rcounter = threading.Lock()
resource = threading.Lock()
read_try = threading.Lock()

threads = [threading.Thread(target=reader) for i in range(3)] + 
           [threading.Thread(target=writer) for i in range(2)]

for thread in threads:
    thread.start()
 	   

По сравнению с нашим первым решением данной проблемы, наша основная программа остаётся относительно той же самой (за исключением инициализации блокирования read_try, счётчика wcount и его блокировки, wcounter) однако в нашей функции writer() мы блокируем read_try, как только имеется по крайней мере один писатель, дожидающийся доступа к этому файлу; когда самый последний писатель завершит своё исполнение, он высвободит свою блокировку с тем, чтобы любой ожидающий данного файла читатель смог теперь выполнить к нему доступ.

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


> python3 example2.py
Reading being done by Thread-1:
This is some text. 
Reading being done by Thread-1:
This is some text. 
Writing being done by Thread-4.
Writing being done by Thread-5.
Writing being done by Thread-4.
Writing being done by Thread-4.
Writing being done by Thread-4.
Writing being done by Thread-5.
Writing being done by Thread-4.
...
		

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

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

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

Такое решение также не является желательным, ибо оно предоставляет более высокий приоритет потокам писателей в нашей программе.

Третья проблема читатели- писатели

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

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

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

Давайте рассмотрим свою функцию writer() из своего файла Chapter13/example3.py для реализации её на Python:


# Chapter13/example3.py

def writer():
    global text

    while True:
        with service:
            resource.acquire()

        print(f'Writing being done by 
              {threading.current_thread().name}.')
        text += f'Writing was done by 
              {threading.current_thread().name}. '

        resource.release()
 	   

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

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

Вот функция reader(), которая содержит данную спецификацию:


# Chapter13/example3.py

def reader():
    global rcount

    while True:
        with service:
            rcounter.acquire()
            rcount += 1
            if rcount == 1:
                resource.acquire()
        rcounter.release()

        print(f'Reading being done by 
              {threading.current_thread().name}:')
        #print(text)

        with rcounter:
            rcount -= 1
            if rcount == 0:
                resource.release()
 	   

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


# Chapter13/example3.py

text = 'This is some text. '
rcount = 0

rcounter = threading.Lock()
resource = threading.Lock()
service = threading.Lock()

threads = [threading.Thread(target=reader) for i in range(3)] + [threading.Thread(target=writer) for i in range(2)]

for thread in threads:
    thread.start()
 	   

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


> python3 example3.py
Reading being done by Thread-3:
Writing being done by Thread-4.
Reading being done by Thread-1:
Writing being done by Thread-5.
Reading being done by Thread-2:
Reading being done by Thread-3:
Writing being done by Thread-4.
...
		

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

Отметим, что если вам придётся работать с задачей читатель- писатель в своей программе совместной обработки, вам не придётся повторно изобретать новое колесо в отношении тех подходов, которые мы только что обсудили. PyPI в действительности имеет некую внутреннюю библиотеку с названием readerwriterlock, которая содержит соответствующие реализации всех трёх обсуждённых нами подходов на Python, а также поддержку для таймаутов. Для поиска дополнительных сведений относительно этой библиотеки и её документации перейдите на https:/​/​pypi.​org/​project/​readerwriterlock/.

Решения зависаний

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

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

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

  • Очередь потоков первый -пришёл- первый- ушёл: Для обеспечения гарантии того, что поток, начавший ожидать некий совместный ресурс прежде другого потока, будет иметь возможность получить доступ к этому ресурсу также раньше этого второго потока, мы можем отслеживать все свои потоки, запрашивающие доступ, в очередях FIFO (first-in-first-out).

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

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

Выводы

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

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

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

Вопросы

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

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

  • В чём состоит взаимосвязь между взаимным блокированием и зависанием?

  • В чём состоит задача читатели- писатели?

  • Каков первый подход к проблеме читателей- писателей? Почему зависание возникает в этом случае?

  • В чём состоит второй подход к проблеме читатели- писатели? Почему в этой ситуации появляется зависание?

  • Расскажите о третьем подходе к проблеме читатели- писатели. Почему он успешно разрешает проблему зависания?

  • Каковы наиболее распространённые решения проблемы зависания?

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

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