2015-08-21 2 views
5

Я пытаюсь создать новое приложение на основе PyQt5 и asyncio (с python 3.4, с нетерпением жду, чтобы в итоге обновить до 3.5 с помощью async/await). Моя цель - использовать asyncio, чтобы графический интерфейс оставался отзывчивым, даже когда приложение ждет, когда какое-то подключенное оборудование завершит операцию.PyQt5 и asyncio: выход из никогда не заканчивается

Когда вы смотрите, как объединить циклы событий Qt5 и asyncio, я нашел mailing list posting, предлагая использовать quamash. Однако при выполнении этого примера (немодифицированный)

yield from fut 

Невер, похоже, возвращается. Я вижу выход «Тайм-аут», поэтому обратный вызов таймера, очевидно, срабатывает, но будущее не может разбудить метод ожидания. При ручном закрытии окна, он говорит мне, что есть незавершенные фьючерсы:

Yielding until signal... 
Timeout 
Traceback (most recent call last): 
    File "pyqt_asyncio_list.py", line 26, in <module> 
    loop.run_until_complete(_go()) 
    File "/usr/local/lib/python3.5/site-packages/quamash/__init__.py", line 291, in run_until_complete 
    raise RuntimeError('Event loop stopped before Future completed.') 
RuntimeError: Event loop stopped before Future completed. 

Я испытал это на Ubuntu с питоном 3.5 и на Windows, с 3.4, такое же поведение на обеих платформах.

Во всяком случае, так как это не то, что я на самом деле пытаются достичь, я испытал некоторый другой код, а также:

import quamash 
import asyncio 
from PyQt5.QtWidgets import * 
from PyQt5.QtCore import * 

@asyncio.coroutine 
def op(): 
    print('op()') 

@asyncio.coroutine 
def slow_operation(): 
    print('clicked') 
    yield from op() 
    print('op done') 
    yield from asyncio.sleep(0.1) 
    print('timeout expired') 
    yield from asyncio.sleep(2) 
    print('second timeout expired') 

def coroCallHelper(coro): 
    asyncio.ensure_future(coro(), loop=loop) 

class Example(QWidget): 

    def __init__(self): 
    super().__init__() 
    self.initUI() 

    def initUI(self): 
    def btnCallback(obj): 
     #~ loop.call_soon(coroCallHelper, slow_operation) 
     asyncio.ensure_future(slow_operation(), loop=loop) 
     print('btnCallback returns...') 

    btn = QPushButton('Button', self) 
    btn.resize(btn.sizeHint()) 
    btn.move(50, 50) 
    btn.clicked.connect(btnCallback) 

    self.setGeometry(300, 300, 300, 200) 
    self.setWindowTitle('Async')  
    self.show() 

with quamash.QEventLoop(app=QApplication([])) as loop: 
    w = Example() 
    loop.run_forever() 
#~ loop = asyncio.get_event_loop() 
#~ loop.run_until_complete(slow_operation()) 

Программа должна отображать окно с кнопкой в ​​нем (что он делает) , при нажатии кнопки slow_operation() без блокировки GUI. При запуске этого примера я могу нажимать кнопку так часто, как хочу, поэтому графический интерфейс не блокируется. Но

yield from asyncio.sleep(0.1) 

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

btnCallback returns... 
clicked 
op() 
op done 
btnCallback returns... 
clicked 
op() 
op done 

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

#~ with quamash.QEventLoop(app=QApplication([])) as loop: 
    #~ w = Example() 
    #~ loop.run_forever() 
loop = asyncio.get_event_loop() 
loop.run_until_complete(slow_operation()) 

Теперь два вопроса:

  1. Это разумный способ достичь развязку длительных операций с помощью графического интерфейса , в общем? Мое намерение заключается в том, что обратный вызов кнопки отправляет вызов coroutine в цикл событий (с дополнительным уровнем вложенности или без него, cf. coroCallHelper()), где он затем запланирован и выполнен. Мне не нужны отдельные потоки, так как на самом деле только входы/выходы занимают время, а не фактическая обработка.

  2. Как я могу исправить это поведение?

Спасибо, Philipp

ответ

6

Хорошо, что один плюс SO: Записывая вопрос заставляет вас думать, снова обо всем. Как-то я просто понял это:

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

app = QApplication(sys.argv) 
loop = QEventLoop(app) 
asyncio.set_event_loop(loop) # NEW must set the event loop 

# ... 

with loop: 
    loop.run_until_complete(master()) 

Ключ кажется asyncio.set_event_loop(). Также важно отметить, что упоминается упомянутый QEventLoop один из пакета quamash, а не Qt5.Так что мой пример теперь выглядит следующим образом:

import sys 
import quamash 
import asyncio 
from PyQt5.QtWidgets import * 
from PyQt5.QtCore import * 

@asyncio.coroutine 
def op(): 
    print('op()') 


@asyncio.coroutine 
def slow_operation(): 
    print('clicked') 
    yield from op() 
    print('op done') 
    yield from asyncio.sleep(0.1) 
    print('timeout expired') 
    yield from asyncio.sleep(2) 
    print('second timeout expired') 

    loop.stop() 

def coroCallHelper(coro): 
    asyncio.ensure_future(coro(), loop=loop) 

class Example(QWidget): 

    def __init__(self): 
    super().__init__() 
    self.initUI() 

    def initUI(self): 
    def btnCallback(obj): 
     #~ loop.call_soon(coroCallHelper, slow_operation) 
     asyncio.ensure_future(slow_operation(), loop=loop) 
     print('btnCallback returns...') 

    btn = QPushButton('Button', self) 
    btn.resize(btn.sizeHint()) 
    btn.move(50, 50) 
    btn.clicked.connect(btnCallback) 

    self.setGeometry(300, 300, 300, 200) 
    self.setWindowTitle('Async')  
    self.show() 

app = QApplication(sys.argv) 
loop = quamash.QEventLoop(app) 
asyncio.set_event_loop(loop) # NEW must set the event loop 

with loop: 
    w = Example() 
    w.show() 
    loop.run_forever() 
print('Coroutine has ended') 

И это просто работает "сейчас:

btnCallback returns... 
clicked 
op() 
op done 
timeout expired 
second timeout expired 
Coroutine has ended 

Может быть, это какой-то помощи для других. Я доволен этим, по крайней мере;) Замечания по общей схеме по-прежнему приветствуются, конечно!

С уважением, Philipp

+1

Да. По умолчанию 'asyncio.get_event_loop()' возвращает значение по умолчанию 'SelectorEventLoop'. В 'asyncio' есть много функций, которые используют' get_event_loop() 'для получения цикла и присоединения к нему обратных вызовов. Если вы не задаете «set_event_loop» свой настраиваемый цикл событий, все фьючерсы и сопрограммы получат запланированный цикл событий по умолчанию, который никогда не запускается. –

+0

Спасибо за объяснение, теперь это имеет смысл. –

+0

@PhilippBurch не возражаете принять свой собственный ответ, это совершенно нормально на SO/SE, чтобы сделать это. – Killah

Смежные вопросы