воскресенье, 14 августа 2011 г.

PyQt Пару слов о потоках.

Графический интерфеейс очень не любит потоки. Ну это естественно, его основная цель - пеерисовываться, медленно последовательно прорисовать каждый пиксель каждой кнопочки, ему далеко до одновременной перерисоки, Он как библиотекарь к которому приходишь за учебниками в 8 класс, и он по очереди приносит тебе каждый учебник. А когда его терзают одноверменно 4 процессора по поводу перерисовки, он ничего не понимает. И поэтому всех кроме первого посылает В (|) И вываливается с ошибкой. Попробуем организовать что-то типо очереди.

Так-Так. Лабръораторная работа номер N, Работа потоков с графическим интерфейсом.
Сначала я думал так:"а что если сделать действительно очередь". Тоетсь сделать класс Lock примерно таким

class Lock():
def __init__(self):
self.locked = False
def begin(self):
while self.locked:
pass
self.locked = True
def end(self)
self.locked = False
## только потом я обнаружил что
## этот велосипед изобрели до меня 
## в threading.Lock()
сделать обьект такого класса, и обличить изменение интерфейса в бегины,энды. Былобы неплохо.
Так програнывает в консоли в критических секциях, или протсо в выполнении какого-то кода. но не с Qt.Ребята создававшие PyQt видимо подумали что я буду небезопасно изменять их графический интерфейс. Ну вот они и запретили потокам его трогать. Так что трогать интерфейс может только главный поток. Ну и что делать спроситте вы? Можно посылать сигналы главному потоку. Вот это мы и попытаемся сделать. И так упрощённая задача. Есть пользователь User который выглядит гдето так.

class User():
def __init__(self,name):
self.name = name
def event(self):
pass
def do_some(self):
print('user.do some')
T = threading.Thread(None,self.event,None)
T.start()
time.sleep(10)
print(self.name)
Это упрощённый вариант,функциональностью не кишит. Но зато каждый пользователь исключительный. Для нас отличается только именем. Мы видем что в функция метода do_some вызывает event(событие , которое мы потом заменим в процессе написания кода) и потом работает ещё 10 секунд.И печатает имя своё.

Формочка наша, это PlanText и pushButton. большего для тестов не надо( я ленив, не люблю писать формы, даже в Дизайнере)

сlass MainForm(QMainWindow):
def __init__(self):
super(MainForm, self).__init__()
uic.loadUi("test.ui", self)

self.connect(self.pushButton,
SIGNAL("clicked()"),
self.pushButton_Click)

self.engine = Engine(self)


self.connect(self.engine,
SIGNAL('send(int)'), self.receiver)

def pushButton_Click(self):
T = threading.Thread(None,self.engine.userlist[0].do_some,None)
T.start()
print('pushButton_Click')
pass
def receiver(self, value):
self.plainTextEdit.appendPlainText(str(value))
Пожалуй вы в замешательстве, тчо такое engine... engine это класс в котором живёт программа. Если немного посмотреть то Графический интерфейс у нас в MainWindow, Данные( обьекты) с которыми мы будем работать это User. Ну а как мы с ними будем работать, это лежит в engine.
Вот он

class Engine(QObject):
 def __init__(self,form):
  QObject.__init__(self)
  self.form = form
  self.userlist = []
  self.userlist.append(User('Alex'))
  self.userlist[0].event = self.event
 def event(self):
  print('Engine.event')
  for x in range(10):
   self.emit(SIGNAL('send(int)'), x)
   time.sleep(0.1)

Я его наследую от QObject почему? потому что он должен порождать сигналы(emit) а это умеет делать только QObject в конце концов мы ведь с Qt работаем. И сссылку на engine мы помещаем в MainWindow, чтоб можно было вызывать его методы в реакциях на кнопки, и enginу мы сообщаем ссылку на форму, чтоб он мог её изменять.Что у нас есть в engine? У нас есть userlist в котором за жизнь программы будет жить только 1 пользователь Alex. И обратите внимание на строку self.userlist[0].event = self.event Таким образом я тому самому event в Userе присваиваю мой собственный переопределённый event, в котором содержится посылка сигнала на изменение интерфейса.

 def do_some(self):
  print('user.do some')
  T = threading.Thread(None,self.event,None)# да,да вот тут и вызовится
  T.start()
  time.sleep(10)
  print(self.name)

Теперь ещё раз прочитите код в MainWindow Видите, по нажатию кнопки вызывается метод do_some единственного нашего пользователя. В нутри метода do_some вызовется переопроеделённое event в отдельном потоке.
И 10 раз выполнится строка self.emit(SIGNAL('send(int)'), x) что это значит? это значит что класс engine генерирует Сигнал 'send(int)', с параметром x. В это самое время основной поток увидит что сигнал сгенерирован вот это строкой

  self.connect(self.engine,
  SIGNAL('send(int)'), self.receiver)

Что у нас тут? мы приконектили сигнал 'send(int)' от self.engine к функции self.receiver.
Это значит тчо если вдруг self.engine скажет 'send(int)' , то вызовется функция self.receiver с параметром x.

А что у нас в receiverе? а там у нас добавление в PlainText переданного параметра.
И ву-аля, это основной потом, при этом наш Юзер в это время спокойно отрабатывает свои 10 секунд, и никому не надо ждать. Но вы не могли не заметить что do some я вызываю в отдельном потоке, почему? Ведь сигнал я уже с потока посылаю. Потому что если я не вызову отдельный поток, то 10  секунд работы прийдётся выполнять основному потоку, а это значит что в это время он не сможет принимать сигналы и изменять граф. интерфейс,  Сигналы будут строится в очередь,  и только когда 10 секунд работы завершится , сигналы приймутся, и PlanText обновится. А когда я запускаю работу Юзера в потоке, то основной поток свободен для наших нужд. Ну вот. Теперь я и вы, можем делать сложные программы с длинными асинхронными вычислениями и связями, и вовремя отображать на экране всё это, без падений программы, потому , что вся работа лежит на новых потоках. Основной поток занимается только по сути обновлением Окна.




PS: Обратите внимание на передаваемый сигнал с параметром, в сигнале я явно указываю ТИП передаваемого параметра. К примеру если вам нужно передать строку , советую использовать QString. Если надо передать параметр своего типа, то этот тип надо специально зарегестрировать до его передачи с помощью qRegisterMetaType()


1 комментарий: