Потоки в приложении PyQt: использовать потоки Qt или потоки Python?

118

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

[Да, я знаю, теперь у меня две проблемы .]

В любом случае, приложение использует PyQt4, поэтому я хотел бы знать, что лучше: использовать потоки Qt или использовать threadingмодуль Python ? Каковы преимущества / недостатки каждого? Или у вас есть совсем другое предложение?

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

Каковы преимущества и недостатки использования потоков PyQt4 (т.е. Qt) над собственными потоками Python (из threadingмодуля)?


Изменить 2: Спасибо всем за ответы. Хотя нет 100% согласия, похоже, существует широко распространенное мнение, что ответ - «используйте Qt», поскольку преимуществом этого является интеграция с остальной частью библиотеки, при этом не вызывая реальных недостатков.

Для тех, кто хочет выбрать между двумя реализациями потоков, я настоятельно рекомендую прочитать все ответы, представленные здесь, включая ветку списка рассылки PyQt, на которую ссылается аббат .

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

Еще раз спасибо.

balpha
источник

Ответы:

107

Это обсуждалось не так давно в списке рассылки PyQt. Цитируя комментарии Джованни Баджо по этому поводу:

В основном то же самое. Основное отличие состоит в том, что QThreads лучше интегрированы с Qt (асинхронные сигналы / слоты, цикл событий и т. Д.). Кроме того, вы не можете использовать Qt из потока Python (вы не можете, например, отправлять событие в основной поток через QApplication.postEvent): вам нужен QThread, чтобы это работало.

Общее практическое правило может заключаться в использовании QThreads, если вы собираетесь как-то взаимодействовать с Qt, и в противном случае использовать потоки Python.

И некоторые более ранние комментарии по этому поводу от автора PyQt: «они обе являются оболочкой для одной и той же реализации нативного потока». И обе реализации используют GIL одинаково.

аббат
источник
2
Хороший ответ, но я думаю, вам следует использовать кнопку цитаты, чтобы четко показать, где вы на самом деле не резюмируете, а цитируете Джованни Баджо из списка рассылки :)
c089,
2
Интересно, почему вы не можете отправлять события в основной поток через QApplication.postEvent () и для этого нужен QThread? Думаю, я видел, как люди делали это, и это сработало.
Триларион
1
Я звонил QCoreApplication.postEventиз потока Python со скоростью 100 раз в секунду в приложении, которое работает на нескольких платформах и было протестировано в течение 1000 часов. Я никогда не видел от этого никаких проблем. Я думаю, это нормально, если целевой объект находится в MainThread или QThread. Я также завернул его в красивую библиотеку, см. Qtutils .
three_pineapples
2
Учитывая высоко оцененный характер этого вопроса и ответа, я думаю, стоит указать на недавний ответ SO от ekhumoro, в котором подробно рассматриваются условия, при которых безопасно использовать определенные методы Qt из потоков Python. Это согласуется с наблюдаемым поведением, которое наблюдали я и @Trilarion.
three_pineapples
33

Потоки Python будут проще и безопаснее, и, поскольку они предназначены для приложений на основе ввода-вывода, они могут обходить GIL. Тем не менее, рассматривали ли вы неблокирующий ввод-вывод с использованием Twisted или неблокирующих сокетов / select?

РЕДАКТИРОВАТЬ: подробнее о потоках

Потоки Python

Потоки Python - это системные потоки. Однако Python использует глобальную блокировку интерпретатора (GIL), чтобы гарантировать, что интерпретатор одновременно выполняет только блок инструкций байтового кода определенного размера. К счастью, Python выпускает GIL во время операций ввода / вывода, делая потоки полезными для моделирования неблокирующего ввода / вывода.

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

QT потоки

Когда Python передает управление стороннему скомпилированному модулю, он освобождает GIL. Ответственность за обеспечение атомарности там, где это необходимо, ложится на модуль. Когда управление будет передано обратно, Python будет использовать GIL. Это может сбивать с толку использование сторонних библиотек в сочетании с потоками. Еще сложнее использовать внешнюю библиотеку потоков, потому что это добавляет неопределенности относительно того, где и когда управление находится в руках модуля, а не интерпретатора.

Потоки QT работают с выпущенным GIL. Потоки QT могут выполнять код библиотеки QT (и другой код скомпилированного модуля, который не получает GIL) одновременно. Однако код Python, выполняемый в контексте потока QT, по- прежнему получает GIL, и теперь вам нужно управлять двумя наборами логики для блокировки вашего кода.

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

Неблокирующий ввод / вывод

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

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

Хорошим решением для событийного неблокирующего ввода-вывода является новая библиотека Diesel . На данный момент он ограничен Linux, но он необычайно быстр и довольно элегантен.

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

Джефф Обер
источник
Re Twisted и т.д .: Я использую стороннюю библиотеку, которая сама занимается сетью; Я бы хотел, чтобы в нем не было исправлений. Но я все равно посмотрю на это, спасибо.
balpha
2
Ничто на самом деле не обходится без GIL. Но Python выпускает GIL во время операций ввода-вывода. Python также выпускает GIL при «передаче» скомпилированным модулям, которые сами отвечают за получение / выпуск GIL.
Джефф Обер,
2
Обновление просто неправильное. Код Python выполняется в потоке Python точно так же, как в QThread. Вы получаете GIL, когда запускаете код Python (а затем Python управляет выполнением между потоками), вы выпускаете его, когда запускаете код C ++. Вообще нет никакой разницы.
Лукаш Лалински
1
Нет, дело в том, что независимо от того, как вы создаете поток, интерпретатору Python все равно. Все, о чем он заботится, это то, что он может получить GIL и после инструкций X он может освободить / повторно получить его. Например, вы можете использовать ctypes для создания обратного вызова из библиотеки C, которая будет вызываться в отдельном потоке, и код будет работать нормально, даже не зная, что это другой поток. В модуле thread действительно нет ничего особенного.
Лукаш Лалински
1
Вы говорили, чем отличается QThread в отношении блокировки и как «вам нужно управлять двумя наборами логики для блокировки вашего кода». Я говорю, что это совсем не другое. Я могу использовать ctypes и pthread_create для запуска потока, и он будет работать точно так же. Код Python просто не должен заботиться о GIL.
Лукаш Лалински
21

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

Если вы обращаетесь к HTTP-серверу, вам следует подумать QNetworkAccessManager.

Лукаш Лалински
источник
1
Помимо того, что я прокомментировал ответ Джеффа Обера, QNetworkAccessManagerвыглядит многообещающим. Спасибо.
balpha
14

Я задавал себе тот же вопрос, когда работал с PyTalk .

Если вы используете Qt, вам нужно использовать, QThreadчтобы иметь возможность использовать структуру Qt и особенно систему сигналов / слотов.

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

Более того, этот выбор не вызывает большого сомнения в производительности, поскольку оба являются привязками C ++.

Вот мой опыт работы с PyQt и потоком.

Я рекомендую вам использовать QThread.

Natim
источник
9

У Джеффа есть хорошие моменты. Только один основной поток может выполнять любые обновления графического интерфейса. Если вам действительно нужно обновить графический интерфейс из потока, сигналы подключения Qt-4 в очереди упрощают отправку данных между потоками и будут автоматически вызываться, если вы используете QThread; Я не уверен, будут ли они, если вы используете потоки Python, хотя добавить параметр в connect().

Калеб Педерсон
источник
5

Я тоже не могу рекомендовать, но я могу попытаться описать различия между потоками CPython и Qt.

Прежде всего, потоки CPython не выполняются одновременно, по крайней мере, код Python. Да, они действительно создают системные потоки для каждого потока Python, однако разрешено запускать только поток, в настоящее время содержащий глобальную блокировку интерпретатора (расширения C и код FFI могут обходить его, но байт-код Python не выполняется, пока поток не содержит GIL).

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

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

Надеюсь, это поможет с вашими проблемами :)

P_L
источник
7
Здесь важно отметить: PyQt QThreads действительно принимает глобальную блокировку интерпретатора . Весь код Python блокирует GIL, и любые QThreads, которые вы запускаете в PyQt, будут запускать код Python. (Если они этого не делают, вы фактически не используете PyQt-часть :). Если вы решите перенести этот код Python во внешнюю библиотеку C, GIL будет выпущен, но это верно независимо от того, используете ли вы поток Python или поток Qt.
кварк
На самом деле это было то, что я пытался передать, что весь код Python принимает блокировку, но это не имеет значения для кода C / C ++, работающего в отдельном потоке
p_l
0

Я не могу комментировать точные различия между потоками Python и PyQt, но я делал то, что вы пытаетесь сделать QThread, QNetworkAcessManagerи обязательно вызываю, QApplication.processEvents()пока поток жив. Если проблема, которую вы пытаетесь решить, действительно связана с быстродействием графического интерфейса, то последнее поможет.

brianz
источник
1
QNetworkAcessManagerне требует потока или processEvents. Он использует асинхронные операции ввода-вывода.
Лукаш Лалински
Упс ... да, я использую комбинацию QNetworkAcessManagerи httplib2. Мой асинхронный код использует httplib2.
brianz