thread vorzeitig beenden?

Fragen zu Tkinter.
Antworten
ottto
User
Beiträge: 15
Registriert: Donnerstag 9. Februar 2017, 16:36

Hallo,
in meinem Programm hab ich eine zeitaufwendige Abarbeitung in einen extra Thread "ausgelagert", so dass ich meine Oberfläche noch bedienen kann. Mit break komme ich auch vorzeitig aus meiner Schleife und aus dem entsprechendem Thread raus. Dabei muss ich aber warten bis die Zeit des aktuellen Schleifendurchlaufs runter gelaufen ist. Weiterhin bekomme ich eine "RuntimeError: main thread is not in main loop" wenn ich das HauptFenster beende (Beenden-Button) bevor der Thread beendet ist. Gibt es eine Möglichkeit den Thread vorzeitig und sauber zu verlassen?

Danke.
Gruß.
ottto

Code: Alles auswählen

import tkinter as tk
import os, glob, time
import threading

class Thread1GUI:
	def __init__(self, master):
		self.master=master
		master.title("+++ test thread +++")
		self.start = tk.Button(master, text=" Ausgabe ! ", command = self.start_click)
		self.start.grid(row=0, column=0)
		self.ausgabe = tk.Label(master)
		self.ausgabe.grid(row=1, column=0)
		self.abbrechen = tk.Button(master, text=" Abbrechen ! ", command = self.abbrechen_click)
		self.abbrechen.grid(row=2, column=0)
		self.beenden = tk.Button(master, text=" Beenden ! ", command = master.quit)
		self.beenden.grid(row=3, column=0)
		self.mein_break = False
		
	def start_click(self):
		def thread_verarbeitung():
			for i in range(6):
				if self.mein_break == True:
					self.ausgabe.config(text="Abbruch !!!")
					break
				print(i)
				self.text=i
				self.ausgabe.config(text=self.text)
				time.sleep(5)
		t1 = threading.Thread(target=thread_verarbeitung)
		t1.start()
		print("t1 Fertig!")
		
	def abbrechen_click(self):
		self.mein_break = True
		
def main():
  root = tk.Tk()
  mein_Thread1GUI = Thread1GUI(root)
  root.mainloop()

if __name__ == '__main__':
  main()

__deets__
User
Beiträge: 14540
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du hast zwei Probleme:

- du hast blockierende Aufrufe in deinem Worker-Thread
- dein Worker-Thread manipuliert GUI-Objekte.

Ersteres ist nicht zu aendern. Threads zu terminieren ist OS-spezifisch, und kann den Prozess in einem unbekannten Zustand hinterlassen, der dann zu Folgefehlern fuehrt. Du musst also entweder kollaborativ sein, und oft genug in deinem Thread pruefen, ob der sich beenden soll, oder einen externen Prozess nutzen.

Und zweiteres ist streng verboten, weil es undefiniertes Verhalten ist & zu abstuerzen fuehren kann. Andere GUI-Toolkits erlauben da etwas elegantere Loesungen (zB Qt mit inter-thread-signalen), aber bei Tk bleibt dir nur, einen Timer im Main-Thread anzulegen (mit after), und dort regelmaessig zu pruefen, ob der Thread noch laeuft.
ottto
User
Beiträge: 15
Registriert: Donnerstag 9. Februar 2017, 16:36

Vielen Dank für die schnelle Hilfe!
Ich hab den Timer zerteilt (Zeile 31). Damit komme ich im Sekundentakt aus der Scleife raus. Das würde mir so reichen. Den Beenden-Button hab ich, so lange der Thread läuft, deaktiviert (Zeile 23 u. 35). Man kann aber immer noch das Fenster selbst schließen. Also mit dem x. Kann man das auch unterbinden?
Danke.
Gruß.
ottto



Code: Alles auswählen

import tkinter as tk

import os, glob, time
import threading

class Thread1GUI:
	def __init__(self, master):
		self.master=master
		master.title("+++ test thread +++")
		self.start = tk.Button(master, text=" Ausgabe ! ", command = self.start_click)
		self.start.grid(row=0, column=0)
		self.ausgabe = tk.Label(master)
		self.ausgabe.grid(row=1, column=0)
		self.abbrechen = tk.Button(master, text=" Abbrechen ! ", command = self.abbrechen_click)
		self.abbrechen.grid(row=2, column=0)
		self.beenden = tk.Button(master, text=" Beenden ! ", command = master.quit)
		
		self.beenden.grid(row=3, column=0)
		self.mein_break = False
		
	def start_click(self):
		def thread_verarbeitung():
			self.beenden.config(state=tk.DISABLED)
			for i in range(6):
				if self.thread_abbrechen():
					break
				print(i)
				self.text=i
				self.ausgabe.config(text=self.text)
				#time.sleep(5)
				for sekunde in range(5):
					if self.thread_abbrechen():
						break
					time.sleep(1)
			self.beenden.config(state=tk.NORMAL)	
		t1 = threading.Thread(target=thread_verarbeitung)
		t1.start()
		print("t1 Fertig!")
		
	def abbrechen_click(self):
		self.mein_break = True
		
	def thread_abbrechen(self):	
		if self.mein_break == True:
			self.ausgabe.config(text="Abbruch !!!")
			return True
		else:
			return False
		
def main():
  root = tk.Tk()
  mein_Thread1GUI = Thread1GUI(root)
  root.mainloop()

if __name__ == '__main__':
  main()




Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@ottto: wie schon __deets__ geschrieben hat, darf man aus einem Thread heraus nicht die GUI ändern. Variablen threadübergreifend zu benutzen ist auch nicht gut. Statt des mein_break-Flags solltest Du Events nehmen.
ottto
User
Beiträge: 15
Registriert: Donnerstag 9. Februar 2017, 16:36

Hallo zusammen,
ich versuche gerade das Prog so zu ändern, dass die GUI nicht mehr aus dem Worker-Thread heraus manipuliert wird. Mein Ansatz dazu ist jetzt nur den zeitaufwendigen Teil in je einen Worker-Thread auszulagern. Es gelingt mir aber leider nicht, die Worker-Threads hintereinander und nicht nebeneinander ablaufen zu lassen. Was ich hier mit den Print-Anweisungen simuliere, ist im eigentlichen Programm je ein Datei-rename.
(Mir ist klar, dass "hintereinander" irgendwie am Sinn vom threading vorbei ist.... )
Durch meinen after-timer (Z.33) in thread_timer() sollte der Folge-Thread warten bis der Vorgänger-Thread fertig ist. Kann mir mal jemand mitteilen wo hier mein Denkfehler liegt?


Danke.
Gruß.
ottto

Code: Alles auswählen

import tkinter as tk
import os, glob, time
import threading

class Thread1GUI:
	def __init__(self, master):
		self.master=master
		master.title("+++ test thread +++")
		self.start = tk.Button(master, text=" Ausgabe ! ", command = self.start_click)
		self.start.grid(row=0, column=0)
		self.ausgabe = tk.Label(master)
		self.ausgabe.grid(row=1, column=0)
		self.abbrechen = tk.Button(master, text=" Abbrechen ! ", command = self.abbrechen_click)
		self.abbrechen.grid(row=2, column=0)
		self.beenden = tk.Button(master, text=" Beenden ! ", command = master.quit)
		
		self.beenden.grid(row=3, column=0)
		self.mein_break = False
		self.thread_fertig = True
	
	def start_click(self):
		def worker_thread():
			self.thread_fertig = False
			mein_i = i
			print("Anfang Thread " + str(mein_i))
			time.sleep(3)
			print("Ende Thread" + str(mein_i))
			self.thread_fertig = True
			
		def thread_timer():
			if self.thread_fertig:
				return
			self.master.after(500, thread_timer)
			print("Timer")			
		
		self.beenden.config(state=tk.DISABLED)
		for i in range(6):
			if self.thread_abbrechen():
				break
			t1 = threading.Thread(target=worker_thread)
			t1.start()
			thread_timer()
			self.text=i
			self.ausgabe.config(text=self.text)
		self.beenden.config(state=tk.NORMAL)	
	
	def abbrechen_click(self):
		self.mein_break = True
		
	def thread_abbrechen(self):	
		if self.mein_break == True:
			self.ausgabe.config(text="Abbruch !!!")
			return True
		else:
			return False
		
def main():
  root = tk.Tk()
  mein_Thread1GUI = Thread1GUI(root)
  root.mainloop()

if __name__ == '__main__':
  main()



__deets__
User
Beiträge: 14540
Registriert: Mittwoch 14. Oktober 2015, 14:29

Da ist so manches Quer. Zum einen startest du einen thread_timer per Thread. Statt einen. Dann startest du schnell mehrere Threads hintereinander, die gleichzeitig auf der gleichen Variable thread_fertig rumhaemmern. Und nirgendwo steht etwas, das verhindert, das der naechste Thread gestartet wird. Woher kommt also deinen Annahme, das waere irgendwie anders?

Wenn du eh sequentiell Tasks abarbeiten willst, nimm

- einen Thread.
- eine Queue.
- stecke Arbeitsauftraege in die Queue (durch den Button-Handler zB).
- der thread wartet mit queue.get() auf einen neuen Auftrag.
- arbeitet den ab, und kann dann pruefen, ob neue Auftraege oder ein Abbruchkriterium vorliegen.

Code: Alles auswählen

import time
import Queue
import threading

def worker(queue):
      while True:
            task = queue.get() # blockiert bis etwas kommt
            if task is None: 
                 break # Ende durch None 
            else:
                 print "working"
                 task()
              
              
def main():
      tasks = Queue.Queue()
      t = threading.Thread(target=worker, (tasks,))
      t.daemon = True
      t.start()
      for i in xrange(5):
           tasks.put(lambda: time.sleep(1.0))
      tasks.put(None)
      t.join()
ottto
User
Beiträge: 15
Registriert: Donnerstag 9. Februar 2017, 16:36

Vielen Dank für die Hilfe.
Wie beschrieben werden die Queue-Aufträge abgearbeitet. Leider wird mein Fenster bei der Abarbeitung weiterhin blockiert. Der Abbrechen-Button läst sich erst betätigen wenn die Queue abgearbeitet ist.

Code: Alles auswählen


import tkinter as tk
import os, glob, time
from queue import Queue
from threading import Thread

class Thread1GUI:
	def __init__(self, master):
		self.master=master
		master.title("+++ test thread +++")
		self.start = tk.Button(master, text=" Ausgabe ! ", command = self.start_click)
		self.start.grid(row=0, column=0)
		self.ausgabe = tk.Label(master)
		self.ausgabe.grid(row=1, column=0)
		self.abbrechen = tk.Button(master, text=" Abbrechen ! ", command = self.abbrechen_click)
		self.abbrechen.grid(row=2, column=0)
		self.beenden = tk.Button(master, text=" Beenden ! ", command = master.quit)
		
		self.beenden.grid(row=3, column=0)
	
	def start_click(self):
		def worker(q):
			while True:
				task = q.get()
				if task is None:
					break
				else:
					print(str(q.qsize()))  
					q.task_done()
					task()
					
		self.ausgabe.config(text = "Ausgabe - Button")
		tasks = Queue()
		t = Thread(target=worker, args=(tasks,))	
		t.Daemon = True
		t.start()
		for i in range(20):
			tasks.put(lambda: time.sleep(0.5))
		tasks.put(None)	
		t.join()	
	
	def abbrechen_click(self):
		self.ausgabe.config(text = "Abbrechen - Button")
		
def main():
  root = tk.Tk()
  mein_Thread1GUI = Thread1GUI(root)
  root.mainloop()

if __name__ == '__main__':
  main()





Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@otto: was erwartest Du, wenn Du t.join() aufrufst?
BlackJack

@ottto: `Thread.join()` wartet bis der Thread beendet ist. Dann hättest Du keinen Thread starten brauchen wenn Du nach dem Starten nicht weitermachst, sondern wartest bis der fertig ist.
ottto
User
Beiträge: 15
Registriert: Donnerstag 9. Februar 2017, 16:36

so einfach ....
funktioniert.
Danke Euch.
Antworten