Еще раз о буферизации потока вывода: взаимоблокировки и выталкивание буферов

eshhe raz o buferizacii potoka vyvoda vzaimoblokirovki i vytalkivanie buferov Системные инструменты параллельного выполнения

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

Более тонкая особенность состоит в том, что и родительский, и дочерний процессы после вывода текста в поток stdout вызывают функцию sys.stdout.flush. Запрос ввода из канала обычно блокирует вызывающий процесс, если в канале нет данных, но в нашем примере из-за этого не должно возникать проблем, потому что запись производится столько же раз, сколько чтение на другом конце канала. Однако по умолчанию поток sys.stdout буферизуется, поэтому выведенный текст в действительности может оказаться переданным только через некоторое время (когда до конца будут заполнены буферы вывода). На практике, если принудительно не выталкивать содержимое буфера, оба процесса могут зависнуть в ожидании данных друг от друга — входных данных, находящихся в буфере и не сбрасываемых в канал. Это приводит к состоянию взаимоблокировки (deadlock), когда оба процесса блокируются в вызове функции input и ожидают события, которое никогда не произойдет.

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

Буферизация выходных данных в действительности производится системными библиотеками, используемыми для доступа к каналам, а не самими каналами (каналы помещают выходные данные в очередь, но не скрывают их от чтения!). На самом деле в данном примере буферизация выполняется только потому, что мы передаем информацию для канала через sys.stdout — встроенный объект файла, по умолчанию выполняющий буферизацию. Однако такие аномалии могут происходить и при использовании других инструментов взаимодействия процессов.

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

     Выталкивание буферов: Как показано в примерах 5.22 и 5.23, выталкивание выходных буферов потоков вывода в канал с помощью метода flush объекта файла является простым способом принудительной очистки буферов. Для выталкивания выходного буфера потока вывода, используемого функцией print, используйте метод sys. stdout.flush.

     Аргументы: Как говорилось выше в этой главе, если вызывать интерпретатор Python с ключом u командной строки, он отключит полную буферизацию потока вывода sys.stdout в выполняемых им программах. Запись любого непустого значения в переменную окружения PYTHONUNBUFFERED эквивалентна передаче этого ключа в команду запуска всех программ.

     Режимы открытия: Имеется также возможность использовать каналы в небуферизованном режиме. Для этого можно использовать низкоуровневые функции из модуля os для чтения и записи в дескрипторы канала или передавать в аргументе функции os.fdopen, определяющем размер буфера, значение 0 (небуферизованный режим) или 1 (режим построчной буферизации), чтобы отключить буферизацию в объекте файла, обертывающем дескриптор. Для управления режимом буферизации вывода в файлы fifo (описываются в следующем разделе) можно также использовать аргументы функции open. Обратите внимание, что в Python 3.X полностью небу- феризованный режим возможен только для двоичных файлов и невозможен для текстовых.

     Каналы команд: Как упоминалось выше в этой главе, точно так же можно определять аргументы, управляющие буферизацией, для каналов командной строки, когда они создаются функциями os.popen и subprocess.Popen, но они воздействуют на конец канала в вызывающем процессе, и не влияют на режим буферизации в порожденных программах. Следовательно, этот прием не в состоянии предотвратить задержку вывода из последних, но может использоваться для передачи текстовых данных в каналы ввода других программ.

     Сокеты: Как мы увидим далее, функция socket.makefile принимает похожий аргумент, определяющий режим буферизации для сокетов (описываются далее в этой главе и книге), но в Python 3.X требует обязательную буферизацию для текстовых данных и, похоже, не поддерживает построчный режим буферизации (подробнее об этом в главе 12).

     Инструменты: Для решения более сложных задач можно также использовать высокоуровневые инструменты, которые фактически обманывают программу, заставляя ее полагать, что она подключена к терминалу. Эти инструменты предназначены для работы с программами не на языке Python, в которых невозможно организовать выталкивание буферов вручную или использовать ключ u. Дополнительные подробности приводятся во врезке «Подробнее о буферизации потоков ввода-вывода: pty и Pexpect» ниже.

Использование дополнительных потоков выполнения позволяет избежать блокирования главного потока, управляющего графическим интерфейсом, но в действительности это решение лишь переносит проблему из одного места в другое (дочерний поток точно так же может оказаться заблокированным). Из предложенных решений, перечисленных выше, первые два — выталкивание буферов вручную и аргументы командной строки — часто являются наиболее простыми. Фактически, благодаря удобству в использовании, второй из перечисленных выше приемов заслуживает, чтобы сказать о нем несколько слов. Попробуйте следующее: закомментируйте все вызовы метода sys.stdout.flush в примерах 5.22 и 5.23 (в файлах pipes.py и pipestestchild.py) и измените вызов функции, порождающий дочерний процесс в файле pipes.py, как показано ниже (то есть добавьте ключ u командной строки):

spawn(‘python’, ‘-u’, ‘pipes-testchild.py’, ‘spam’)

После этого запустите программу с помощью командной строки pythonu pipes.py. Работа будет происходить так же, как при выталкивании выходного буфера потока вывода stdout вручную, потому что теперь поток вывода stdout будет действовать в небуферизованном режиме.

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

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

Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011

Каталог сайтов Всего.ру
Оцените статью
Секреты программирования
Добавить комментарий