Cvičenie 4 - Multithreading a kreslenie v Pythone
Pri riešení tohto cvičenia vám možno pomôžu:
Úvod (tak trocha prednáška):
Spustite si tento program:
import
tkinter, time
def doit():
x = 10
y = 10
for i in range(4):
canvas.create_oval(x, y, x+60, y+60,width=5,fill="green")
time.sleep(1)
x+=60
canvas = tkinter.Canvas(height = 300, width = 1000)
canvas.pack()
doit()
Program má kresliť štyri zelené kruhy a za každým počkať 1 sekundu.
Ale keď ho spustíme, tak sa 10 sekúnd nič nedeje, až potom sa naraz
nakreslia štyri kruhy.
Je to preto, lebo zmeny v canvase sa zobrazujú až keď hlavný thread
(resp. thread v ktorom je mainloop(), viď ďalej, čo je to
mainloop90) "nemá čo robiť" (resp. vykonáva mainloop()).
Mali by ste vedieť z prednášok z Pythonu, že ak chceme vyžiadať
zobrazenie zmien ihneď, musíme zavolať canvas.update(), teda
treba zmeniť funkciu doit takto:
import
tkinter, time
def doit():
x = 10
y = 10
for i in range(4):
canvas.create_oval(x, y, x+60, y+60,width=5,fill="green")
canvas.update() # aby sa kruh hneď ukázal
time.sleep(1)
x+=60
canvas = tkinter.Canvas(height = 300, width = 1000)
canvas.pack()
doit()
Skúsme teraz spustiť funkciu doit v threade:
import
tkinter, time
import
threading # pridali sme threading
def doit():
x = 10
y = 10
for i in range(4):
canvas.create_oval(x, y, x+60, y+60,width=5,fill="green")
canvas.update()
time.sleep(1)
x+=60
canvas = tkinter.Canvas(height = 300, width = 1000)
canvas.pack()
threading.Thread(target =
doit).start() # funkcia doit sa spustí v ďalšom
threade
Nepôjde to, IDLE vyhlási chybu: RuntimeError:
main thread is not in main loop.
Keď budete googliť túto chybu, tak nájdete rôzne rady. Mnohé z nich
tvrdia, že v Pythone smieme kresliť (resp. robiť hocičo s
GUI) len v hlavnom threade. Ale to už pre Python 3.x nie
je pravda. Na rozdiel od grafických knižníc niektorých iných
programovacích jazykov, tkinter pre Python 3.x sa považuje za thread-safe,
teda jeho funkcie sa dajú vyvolávať z rôznych threadov.
Formulácia chybovej správy teda nie je úplne správna, mala by
hovoriť napr. there is no main loop running
at this moment.
Lebo to je podstata chyby: canvas.create_oval vyvolaný v threade
hľadal nejaký iný thread, v ktorom beží mainloop. Hoci v IDLE
existuje "automatický mainloop", ten sa v tomto prípade akosi
"neráta", je v čase vyvolania asi v zlom stave (možno lebo čaká na
vstup z klávesnice).
Pridanie explicitného canvas.mainloop() chybu vyrieši:
import
tkinter, time
import threading
def doit():
x = 10
y = 10
for i in range(4):
canvas.create_oval(x, y, x+60, y+60,width=5,fill="green")
canvas.update()
time.sleep(1)
x+=60
canvas = tkinter.Canvas(height = 300, width = 1000)
canvas.pack()
threading.Thread(target = doit).start()
canvas.mainloop() # pridali sme volanie
mainloop
Tu by sme mohli skončiť...ale naše ukážkové programy často v
hlavnom threade spustia nejaké iné thready a potom čakajú na ich
skončenie, aby prípadne niečo ešte potom spravili (v reálnom prípade
napr. otestovali správnosť):
import
tkinter, time
import threading
def doit():
x = 10
y = 10
for i in range(4):
canvas.create_oval(x, y, x+60, y+60,width=5,fill="green")
canvas.update()
time.sleep(1)
x+=60
canvas = tkinter.Canvas(height = 300, width = 1000)
canvas.pack()
t=threading.Thread(target = doit) #
zmena, thread si uložíme do prememnej, aby sme mohli naň čakať
t.start()
t.join() # čakanie na thread
print('koniec') # "niečo spravíme" po skončení
canvas.mainloop() # tu nám už
mainloop() nepomôže
Toto zase nepôjde, lebo mainloop() na konci nám nepomôže, a
keby bol niekde skôr, tak ďalej by sa už program nedostal (lebo
mainloop je v podstate nekonečný cyklus).
Musíme vymyslieť niečo iné.
Metóda 1: Dajme všetko to, čo sme chceli pôvodne robiť v
hlavnom threade, do ďalšieho threadu a nechajme v hlavnom threade
len vytvorenie canvasu a vyvolanie mainloop:
import
tkinter, time
import threading
def doit():
x = 10
y = 10
for i in range(4):
canvas.create_oval(x, y, x+60, y+60,width=5,fill="green")
canvas.update()
time.sleep(1)
x+=60
def hlavny():
t=threading.Thread(target = doit)
t.start()
t.join()
print('koniec')
canvas = tkinter.Canvas(height = 300, width = 1000)
canvas.pack()
threading.Thread(target =
hlavny).start() # spustíme funkciu hlavny v ďalšom threade
canvas.mainloop()
A je to! Všimnite si, že počas kreslenia nie je v okne IDLE
blikajúci kurzor lebo beží mainloop(), ale keď zavrieme grafické
okno, tak sa tam objaví, lebo zavretie grafického okna skončí
mainloop.
Zavrieť okno môžeme aj programovo volaním canvas.quit(), ktorý len
ukončí mainloop a canvas zostane alebo volaním canvas.destroy(),
ktorý aj odstráni canvas.
import
tkinter, time
import threading
def doit():
x = 10
y = 10
for i in range(4):
canvas.create_oval(x, y, x+60, y+60,width=5,fill="green")
#canvas.update() # toto už netreba lebo thread, kde beží
mainloop, už nerobí nič iné
time.sleep(1)
x+=60
def hlavny():
t=threading.Thread(target = doit)
t.start()
t.join()
#print('koniec') # print môžeme teraz premiestniť do
hlavného threadu
canvas.quit() #
pridali sme quit, mainloop skončí, ale canvas nám zostane,
canvas.destroy() by aj odstránil canvas
canvas = tkinter.Canvas(height = 300, width = 1000)
canvas.pack()
threading.Thread(target = hlavny).start() # spustíme
funkciu hlavny v ďalšom threade
canvas.mainloop()
print('koniec') # teraz môžeme dať niečo aj za mainloop lebo
volanie canvas.quit() ho ukončí.
Všimnite si aj, že canvas.update už netreba lebo thread, kde
beží mainloop, už nerobí nič iné.
Metóda 2: Ak nechceme zmeniť hlavný program na thread alebo
sa nám nepáči konzola zoablokovaná mainthread-om, máme aj druhú
možnosť - vytvoriť canvas a spustiť jeho mainloop v inom threade
(pozor: mainloop musí byť v rovnakom threade ako vznik canvasu):
import
tkinter,time,threading
def doit():
x = 10
y = 10
for i in range(4):
canvas.create_oval(x, y, x+60, y+60,width=5,fill="green")
time.sleep(1)
x+=60
def canvasloop():
global canvas # chceme premennú
canvas sprístupniť aj iným funkciám
canvas = tkinter.Canvas(height =
300, width = 1000)
canvas.pack()
canvas.mainloop()
threading.Thread(target = canvasloop).start()
t=threading.Thread(target = doit)
t.start()
t.join()
print('koniec')
Neviem, ako vám, ale mne toto nefunguje správne, dostanem chybu: NameError: name 'canvas' is not defined.
Je to preto, lebo canvasloop bežiaci v jednom threade ešte nestihol
vytvoriť canvas keď doit bežiaci v druhom threade ho už chcel
použiť.
Opraviť sa to dá napríklad takto (alebo máte lepší nápad?):
import
tkinter,time,threading
def doit():
x = 10
y = 10
for i in range(4):
canvas.create_oval(x, y, x+60, y+60,width=5,fill="green")
canvas.update()
time.sleep(1)
x+=60
canvas = None # na začiatku canvas
ešte neexistuje
def canvasloop():
global canvas
canvas = tkinter.Canvas(height = 300,
width = 1000)
canvas.pack()
canvas.mainloop()
threading.Thread(target = canvasloop).start()
while not canvas: pass #
počkajme, kým sa vytvorí canvas
t=threading.Thread(target = doit)
t.start()
t.join()
print('koniec')
A už to zase funguje.
Máme teda dva konkrétne návody ako programovať
multithreadové programy, ktoré potrebujú aj kresliť,
v Pythone 3.x, .
Úloha 1
Zoberte ukážku producent_konzument.py z UkazkyThready.zip, link na
stiahnutie je na stránke predmetu pod 4. témou.
Vyberte si jednu zo správnych implementácií (Producent_konzument
alebo Producent_konzument_monitor_opraveny) a upravte ju tak, aby po
každej zmene obsahu frontu sa front nakreslil (napr. ako postupnosť
guličiek s číslami vo vnútri) a počkalo sa 1 sekundu.
Pomôcka: kresliť aj čakať musíte vo vnútri kritickej sekcie.
Úloha 2:
Táto úloha je prémia za max. 2 body (o prémiách sa píše na stránke
predmetu).
Termín je do 23.11.2020 12:20.
Použite vedomosti z tohto cvičenia a naprogramujte v Pythone
animáciu programu riešiaceho problém večerajúcich filozofov. Teda,
aby sme na animácii videli stavy jednotlivých filozofov v reálnom
čase, aj históriu či štatistiku jedenia. Dôležitým kritériom
hodnotenia je použiteľnosť programu na vysvetlenie pri výučbe.
Použite ukážku z prednášky. Môžete sa inšpirovať aj
animáciami/videami z Webu, ale nie kompletnými programami.
Hodnotím len unikátne riešenia, nie kópie už skôr zaslaných
programov (vrátane minulých rokov).
Hodnotím aj čiastočné riešenia, prípadne postupne vylepšené riešenia
na základe vzájomnej komunikácie.