Графические интерфейсы, потоки выполнения и очереди

graficheskie interfejsy potoki vypolneniya i ocheredi Приемы программирования графических интерфейсов

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

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

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

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

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

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

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

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

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

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