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.