Problemi di threading Python di livello principiante
-
20-08-2019 - |
Domanda
Come nuovo per lo sviluppo della GUI in Python (con pyGTK), ho appena iniziato a conoscere il threading. Per testare le mie capacità, ho scritto una semplice piccola interfaccia GTK con un pulsante di avvio / arresto. L'obiettivo è che quando viene cliccato, inizia un thread che aumenta rapidamente un numero nella casella di testo, mantenendo la GUI reattiva.
La GUI funziona bene, ma sto riscontrando problemi con il threading. Probabilmente è un problema semplice, ma la mia mente è fritta per il giorno. Di seguito ho incollato prima il trackback dell'interprete Python, seguito dal codice. Puoi andare su http://drop.io/pxgr5id per scaricarlo. Sto usando bzr per il controllo di revisione, quindi se si desidera apportare una modifica e rilasciarla nuovamente, si prega di confermare le modifiche. Sto anche incollando il codice su http://dpaste.com/113388/ perché può avere numeri di riga e questa roba del ribasso mi sta facendo venire il mal di testa.
Aggiornamento 27 gennaio, 15:52 EST: Codice leggermente aggiornato può essere trovato qui: http://drop.io/threagui / attività / thread-gui-rev3-tar-gz
Traceback
crashsystems@crashsystems-laptop:~/Desktop/thread-gui$ python threadgui.pybtnStartStop clicked
Traceback (most recent call last):
File "threadgui.py", line 39, in on_btnStartStop_clicked
self.thread.stop()
File "threadgui.py", line 20, in stop
self.join()
File "/usr/lib/python2.5/threading.py", line 583, in join
raise RuntimeError("cannot join thread before it is started")
RuntimeError: cannot join thread before it is started
btnStartStop clicked
threadStop = 1
btnStartStop clicked
threadStop = 0
btnStartStop clicked
Traceback (most recent call last):
File "threadgui.py", line 36, in on_btnStartStop_clicked
self.thread.start()
File "/usr/lib/python2.5/threading.py", line 434, in start
raise RuntimeError("thread already started")
RuntimeError: thread already started
btnExit clicked
exit() called
Codice
#!/usr/bin/bash
import gtk, threading
class ThreadLooper (threading.Thread):
def __init__ (self, sleep_interval, function, args=[], kwargs={}):
threading.Thread.__init__(self)
self.sleep_interval = sleep_interval
self.function = function
self.args = args
self.kwargs = kwargs
self.finished = threading.Event()
def stop (self):
self.finished.set()
self.join()
def run (self):
while not self.finished.isSet():
self.finished.wait(self.sleep_interval)
self.function(*self.args, **self.kwargs)
class ThreadGUI:
# Define signals
def on_btnStartStop_clicked(self, widget, data=None):
print "btnStartStop clicked"
if(self.threadStop == 0):
self.threadStop = 1
self.thread.start()
else:
self.threadStop = 0
self.thread.stop()
print "threadStop = " + str(self.threadStop)
def on_btnMessageBox_clicked(self, widget, data=None):
print "btnMessageBox clicked"
self.lblMessage.set_text("This is a message!")
self.msgBox.show()
def on_btnExit_clicked(self, widget, data=None):
print "btnExit clicked"
self.exit()
def on_btnOk_clicked(self, widget, data=None):
print "btnOk clicked"
self.msgBox.hide()
def on_mainWindow_destroy(self, widget, data=None):
print "mainWindow destroyed!"
self.exit()
def exit(self):
print "exit() called"
self.threadStop = 1
gtk.main_quit()
def threadLoop(self):
# This will run in a thread
self.txtThreadView.set_text(str(self.threadCount))
print "hello world"
self.threadCount += 1
def __init__(self):
# Connect to the xml GUI file
builder = gtk.Builder()
builder.add_from_file("threadgui.xml")
# Connect to GUI widgets
self.mainWindow = builder.get_object("mainWindow")
self.txtThreadView = builder.get_object("txtThreadView")
self.btnStartStop = builder.get_object("btnStartStop")
self.msgBox = builder.get_object("msgBox")
self.btnMessageBox = builder.get_object("btnMessageBox")
self.btnExit = builder.get_object("btnExit")
self.lblMessage = builder.get_object("lblMessage")
self.btnOk = builder.get_object("btnOk")
# Connect the signals
builder.connect_signals(self)
# This global will be used for signaling the thread to stop.
self.threadStop = 1
# The thread
self.thread = ThreadLooper(0.1, self.threadLoop, (1,0,-1))
self.threadCounter = 0
if __name__ == "__main__":
# Start GUI instance
GUI = ThreadGUI()
GUI.mainWindow.show()
gtk.main()
Soluzione
Il threading con PyGTK è un po 'complicato se vuoi farlo nel modo giusto. Fondamentalmente, non dovresti aggiornare la GUI da nessun altro thread oltre al thread principale (limitazione comune nelle librerie della GUI). Normalmente questo viene fatto in PyGTK usando il meccanismo dei messaggi in coda (per la comunicazione tra i lavoratori e la GUI) che vengono letti periodicamente usando la funzione di timeout. Una volta che ho avuto una presentazione sul mio LUG locale su questo argomento, puoi prendere il codice di esempio per questa presentazione da Codice Google repository. Dai un'occhiata alla MainWindow
classe in forms/frmmain.py
, specialmente per il metodo _pulse()
e cosa viene fatto in on_entry_activate()
(il thread viene avviato lì e viene creato il timer di inattività).
def on_entry_activate(self, entry):
text = entry.get_text().strip()
if text:
store = entry.get_completion().get_model()
if text not in [row[0] for row in store]:
store.append((text, ))
thread = threads.RecommendationsFetcher(text, self.queue)# <- 1
self.idle_timer = gobject.idle_add(self._pulse)# <- 2
tv_results = self.widgets.get_widget('tv_results')
model = tv_results.get_model()
model.clear()
thread.setDaemon(True)# <- 3
progress_update = self.widgets.get_widget('progress_update')
progress_update.show()
thread.start()# <- 4
In questo modo, l'applicazione aggiorna la GUI quando è " idle " (con GTK significa) che non causa blocchi.
- 1: crea thread
- 2: crea il timer di inattività
- 3: daemonize thread in modo che l'app possa essere chiusa senza attendere il completamento del thread
- 4: avvia discussione
Altri suggerimenti
In genere è meglio evitare discussioni quando è possibile. È molto difficile scrivere correttamente un'applicazione filettata e ancora più difficile sapere che hai capito bene. Dal momento che stai scrivendo un'applicazione GUI, è più facile per te visualizzare come farlo, dal momento che devi già scrivere l'applicazione in un framework asincrono.
La cosa importante da capire è che un'applicazione GUI non sta facendo molto. Trascorre la maggior parte del tempo in attesa che il sistema operativo gli dica che è successo qualcosa. Puoi fare molte cose in questo tempo di inattività fintanto che sai come scrivere codice di lunga durata in modo che non si blocchi.
Puoi risolvere il tuo problema originale usando un timeout; chiedendo al framework GUI di richiamare alcune funzioni dopo un ritardo, quindi reimpostare quel ritardo o avviare un'altra chiamata ritardata.
Un'altra domanda comune è come comunicare in rete in un'applicazione GUI. Le app di rete sono come le app della GUI in quanto attendono molto. L'uso di un framework IO di rete (come Twisted ) semplifica l'attesa cooperativa di entrambe le parti anziché competitiva, e di nuovo allevia la necessità di discussioni extra.
I calcoli a esecuzione prolungata possono essere scritti in modo iterativo anziché sincrono, ed è possibile eseguire l'elaborazione mentre la GUI è inattiva. Puoi usare un generatore per farlo abbastanza facilmente in Python.
def long_calculation(param, callback):
result = None
while True:
result = calculate_next_part(param, result)
if calculation_is_done(result):
break
else:
yield
callback(result)
La chiamata a long_calculation
ti darà un oggetto generatore e la chiamata .next()
sull'oggetto generatore eseguirà il generatore fino a quando non raggiunge yield
o return
. Diresti al framework GUI di chiamare long_calculation(some_param, some_callback).next
quando ha tempo, e alla fine il tuo callback verrà chiamato con il risultato.
Non conosco molto bene GTK, quindi non posso dirti quali funzioni del gobject dovresti chiamare. Con questa spiegazione, tuttavia, dovresti essere in grado di trovare le funzioni necessarie nella documentazione o, nel peggiore dei casi, chiedere su un canale IRC pertinente.
Sfortunatamente non esiste una buona risposta al caso generale. Se chiarisci esattamente cosa stai cercando di fare, sarebbe più facile spiegare perché non hai bisogno di discussioni in quella situazione.
Non è possibile riavviare un oggetto thread arrestato; non provarci. Invece, crea una nuova istanza dell'oggetto se desideri riavviarlo dopo che è stato effettivamente arrestato e unito.
Ho giocato con diversi strumenti per aiutare a ripulire il lavoro con thread, elaborazione inattiva, ecc.
make_idle è un decoratore di funzioni che ti consente di eseguire un'attività in background in modo cooperativo. Questa è una buona via di mezzo tra qualcosa di abbastanza breve da eseguire una volta nel thread dell'interfaccia utente e non influire sulla reattività dell'app e fare un thread completo in una sincronizzazione speciale. All'interno della funzione decorata si usa & Quot; yield & Quot; per restituire l'elaborazione alla GUI in modo che possa rimanere reattiva e la prossima volta che l'interfaccia utente è inattiva, riprenderà nella tua funzione da dove avevi interrotto. Quindi, per iniziare, basta chiamare idle_add alla funzione decorata.
def make_idler(func):
"""
Decorator that makes a generator-function into a function that will
continue execution on next call
"""
a = []
@functools.wraps(func)
def decorated_func(*args, **kwds):
if not a:
a.append(func(*args, **kwds))
try:
a[0].next()
return True
except StopIteration:
del a[:]
return False
return decorated_func
Se è necessario eseguire un po 'più di elaborazione, è possibile utilizzare un gestore di contesto per bloccare il thread dell'interfaccia utente ogni volta che è necessario per rendere il codice un po' più sicuro
@contextlib.contextmanager
def gtk_critical_section():
gtk.gdk.threads_enter()
try:
yield
finally:
gtk.gdk.threads_leave()
con quello puoi semplicemente
with gtk_critical_section():
... processing ...
Non l'ho ancora finito, ma nel combinare le cose puramente in idle e puramente in un thread, ho un decoratore (non ancora testato quindi non pubblicato) che puoi dire se la sezione successiva dopo la resa è da eseguire nel tempo di inattività dell'interfaccia utente o in un thread. Ciò consentirebbe di eseguire alcune impostazioni nel thread dell'interfaccia utente, passare a un nuovo thread per eseguire operazioni in background e quindi passare al tempo di inattività dell'interfaccia utente per eseguire la pulizia, riducendo al minimo la necessità di blocchi.
Non ho cercato in dettaglio il tuo codice. Ma vedo due soluzioni al tuo problema:
Non usare discussioni. Utilizzare invece un timeout, come questo:
import gobject
i = 0
def do_print():
global i
print i
i += 1
if i == 10:
main_loop.quit()
return False
return True
main_loop = gobject.MainLoop()
gobject.timeout_add(250, do_print)
main_loop.run()
Quando usi i thread, devi assicurarti che il tuo codice GUI sia chiamato da un solo thread contemporaneamente proteggendolo in questo modo:
import threading
import time
import gobject
import gtk
gtk.gdk.threads_init()
def run_thread():
for i in xrange(10):
time.sleep(0.25)
gtk.gdk.threads_enter()
# update the view here
gtk.gdk.threads_leave()
gtk.gdk.threads_enter()
main_loop.quit()
gtk.gdk.threads_leave()
t = threading.Thread(target=run_thread)
t.start()
main_loop = gobject.MainLoop()
main_loop.run()