Hlavní navigace

Spracovanie dlhotrvajúcej úlohy v Tkinteri

Ján Bodnár 10. 2. 2016

V článku si ukážeme, ako vytvoriť časovo náročnú úlohu v Tkinteri pomocou modulu multiprocessing. Našou úlohou bude výpočet Pi. Paralelné programovanie je náročné a v jednoduchom príklade narazíme na technické limity.

Tkinter

Tkinter je grafická knižnica pre tvorbu desktopových aplikácií. Je súčasťou štandardnej distribúcie jazyka Python. Ide o multiplatformovú knižnicu, ktorá beží na Linuxe, systéme Windows a OS X. Z technického hľadiska je Tkinter obálkou nad grapickou knižnicou Tk, ktorá bola vytvorená pre jazyk Tcl. Viac informácií o Tkinter si môžete naštudovať na oficiálnej stránke dokumentácie k Tk, tutoriály na ZetCode, alebo na tkinter.programujte.com.

Podobne ako väčšina knižníc pre tvorbu grafického užívateľského rozhrania, Tkinter využíva jednovláknový modul udalostí. Hlavná slučka udalostí a všetky metódy reagujúce na udalosti sú vykonávané v tomto jednom vlákne. Preto musia byť metódy udalostí rýchle. Inak by si užívatelia nášho programu mysleli, že program nereaguje dostatočne pružne alebo že mrzne. Dlhotrvajúce úlohy musia preto byť vykonávané mimo hlavné vlákno aplikácie.

My si to budeme demonštrovať na príklade výpočtu čísla Pi. Čím viac číslic bude mať Pi, tým dlhšie bude trvať výpočet. Ak by sme nedelegovali výpočet do zvláštneho procesu, náš program by sa počas výpočtu prestal prekresľovať.

Modul multiprocessing

V našom programe využijeme modul multiprocessing, pomocou ktorého vytvoríme časovo náročnú úlohu v separátnom procese. Tento modul poskytuje aplikačné programové rozhranie na tvorbu procesov podobnú modulu threading. Modul multiprocessing funguje rozdielne na Linuxe a vo Windows. Na Linuxe sa používa systémové volanie fork(), kým na systéme Windows je použitý modul pickle. To má dôležité dôsledky pre náš program (za predpokladu, že budeme chcieť, aby fungoval pod oboma systémami.) Náš program preto napíšeme pre oba systémy zvlášť.

Pri tvorbe programu narazíme na technické obmedzenia. Program využíva objekt Queue pre výmenu hodnôt medzi procesmi. Tento objekt má však limit pre veľkosť hodnoty, ktorú môže prijať. Veľkosť tejto hodnoty súvisí s interným fungovaním operačného systému; presnejšie ide o dátovod. Ak sa prekročí veľkosť tejto hodnoty, dôjde k uviaznutiu (deadlock). Vlastným testovaním dospel autor k nasledovným hodnotám: na systéme Windows 7 sa podarilo vypočítať Pi s 8156 číslicami. Na Linuxe leží maximum niekde medzi 60 000 až 70 000 číslicami.

Výpočet Pi na Linuxe

Nasledujúci program vypočíta Pi na Linuxe. Pri výpočte čísla Pi je využitý dátový typ Decimal, pretože implicitný dátový typ float nie je dostatočne presný pre naše výpočty.

calculate_pi.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
This script produces a long-running task of calculating
a large Pi number, while keeping the GUI responsive.

Author: Jan Bodnar
Last modified: January 2016
Website: www.zetcode.com
"""

from tkinter import (Tk, BOTH, Text, E, W, S, N, END,
    NORMAL, DISABLED, StringVar)
from tkinter.ttk import Frame, Label, Button, Progressbar, Entry
from tkinter import scrolledtext

from multiprocessing import Queue, Process
import queue
from decimal import Decimal, getcontext

DELAY1 = 80
DELAY2 = 20

class Example(Frame):

    def __init__(self, parent, q):
        Frame.__init__(self, parent)

        self.queue = q
        self.parent = parent
        self.initUI()


    def initUI(self):

        self.parent.title("Pi computation")
        self.pack(fill=BOTH, expand=True)

        self.grid_columnconfigure(4, weight=1)
        self.grid_rowconfigure(3, weight=1)

        lbl1 = Label(self, text="Digits:")
        lbl1.grid(row=0, column=0, sticky=E, padx=10, pady=10)

        self.ent1 = Entry(self, width=10)
        self.ent1.insert(END, "4000")
        self.ent1.grid(row=0, column=1, sticky=W)

        lbl2 = Label(self, text="Accuracy:")
        lbl2.grid(row=0, column=2, sticky=E, padx=10, pady=10)

        self.ent2 = Entry(self, width=10)
        self.ent2.insert(END, "100")
        self.ent2.grid(row=0, column=3, sticky=W)

        self.startBtn = Button(self, text="Start",
            command=self.onStart)
        self.startBtn.grid(row=1, column=0, padx=10, pady=5, sticky=W)

        self.pbar = Progressbar(self, mode='indeterminate')
        self.pbar.grid(row=1, column=1, columnspan=3, sticky=W+E)

        self.txt = scrolledtext.ScrolledText(self)
        self.txt.grid(row=2, column=0, rowspan=4, padx=10, pady=5,
            columnspan=5, sticky=E+W+S+N)


    def onStart(self):

        self.startBtn.config(state=DISABLED)
        self.txt.delete("1.0", END)

        self.digits = int(self.ent1.get())
        self.accuracy = int(self.ent2.get())

        self.p1 = Process(target=self.generatePi, args=(self.queue,))
        self.p1.start()
        self.pbar.start(DELAY2)
        self.after(DELAY1, self.onGetValue)


    def onGetValue(self):

        if (self.p1.is_alive()):

            self.after(DELAY1, self.onGetValue)
            return
        else:

            try:
                self.txt.insert('end', self.queue.get(0))
                self.txt.insert('end', "\n")
                self.pbar.stop()
                self.startBtn.config(state=NORMAL)

            except queue.Empty:
                print("queue is empty")


    def generatePi(self, queue):

        getcontext().prec = self.digits

        pi = Decimal(0)
        k = 0
        n = self.accuracy

        while k < n:
            pi += (Decimal(1)/(16**k))*((Decimal(4)/(8*k+1)) - \
                (Decimal(2)/(8*k+4)) - (Decimal(1)/(8*k+5))- \
                (Decimal(1)/(8*k+6)))
            k += 1
            print (self.p1.is_alive())

        queue.put(pi)
        print("end")


def main():

    q = Queue()

    root = Tk()
    root.geometry("400x350+300+300")
    app = Example(root, q)
    root.mainloop()


if __name__ == '__main__':
    main()

Program využíva nasledovné komponenty: Label, Button, Progressbar a Entry. O správne rozloženie komponent v hlavnom ráme aplikácie sa stará grid manažér.

lbl1 = Label(self, text="Digits:")
lbl1.grid(row=0, column=0, sticky=E, padx=10, pady=10)

Grid manažér umiestňuje komponenty v mriežke, priestor delí na stĺpce a rady. V tomto prípade je komponenta Label umiestnená v ľavom hornom rohu. Parameter sticky spôsobí, že je komponenta v rámci svojej bunky zarovnaná vpravo. Pomocou parametrov padx a pady pridáme medzery okolo komponenty.

self.pbar = Progressbar(self, mode='indeterminate')
self.pbar.grid(row=1, column=1, columnspan=3, sticky=W+E)

Tu vytvárame komponentu Progressbar, ktorá je aktvína počas nášho výpočtu. Takto dávame užívateľovi najavo, že naša aplikácia beží. Progressbar je v móde indeterminate, ktorý použijeme, keď nedokážeme dopredu určiť dĺžku výpočtu.

self.txt = scrolledtext.ScrolledText(self)
self.txt.grid(row=2, column=0, rowspan=4, padx=10, pady=5,
    columnspan=5, sticky=E+W+S+N)

Päťdesiattisíc číslic je riadne veľké číslo, preto zobrazíme výpočet (ak nám vyjde) v textovej komponente. Použijeme ScrolledText, ktorá má už zabudované rolovanie.

def onStart(self):

    self.startBtn.config(state=DISABLED)
    self.txt.delete("1.0", END)
...

Na začiatku výpočtu deaktivujeme tlačítko Start a zmažeme obsah komponenty ScrolledText.

self.digits = int(self.ent1.get())
self.accuracy = int(self.ent2.get())

Vo výpočte čísla Pi máme dve dôležité premenné. Hodnota digits určuje veľkosť počítaného čísla Pi. Hodnota accuracy určuje presnosť výpočtu.

self.p1 = Process(target=self.generatePi, args=(self.queue,))
self.p1.start()

Vytvárame nový Process. Voľba target je metóda generatePi(), ktorá vykonáva časovo náročný výpočet. Druhým argumentom sa predáva metóde fronta, ktorá je určená na výmenu hodnôt medzi procesmi.

self.after(DELAY1, self.onGetValue)

Pomocou metódy after() štartujeme časovač. Časovač periodicky zisťuje, či je náš výpočet už ukončený.

def onGetValue(self):

    if (self.p1.is_alive()):

        self.after(DELAY1, self.onGetValue)
        return

Pomocou metódy is_alive() zisťujeme, či náš proces ešte stále beží. Keďže Tkinter využíva jednorázový časovač, je potrebné vytvoriť pri tejto príležitosti nový časovač.

else:

    try:
        self.txt.insert('end', self.queue.get(0))
        self.txt.insert('end', "\n")
        self.pbar.stop()
        self.startBtn.config(state=NORMAL)

    except queue.Empty:
        print("queue is empty")

Keď sa naša úloha skončila, získame vypočítané číslo z fronty a vložíme ho do komponenty ScrolledText. Zároveň ukončíme činnosť komponenty Progressbar a aktivujeme tlačítko Start. Tak môžeme potom začať nový výpočet.

def generatePi(self, queue):

    getcontext().prec = self.digits

    pi = Decimal(0)
    k = 0
    n = self.accuracy

    while k < n:
        pi += (Decimal(1)/(16**k))*((Decimal(4)/(8*k+1)) - \
            (Decimal(2)/(8*k+4)) - (Decimal(1)/(8*k+5))- \
            (Decimal(1)/(8*k+6)))
        k += 1

    queue.put(pi)

Metóda generatePi() obsahuje výpočet čísla Pi. Existuje viacero vzorcov na výpočet; tu sme použili Bailey–Borwein–Plouffov vzorec. Na konci výpočtu je hodnota uložená do fronty, z ktorej ju môžeme v našom hlavnom udalostnom vlákne získať. Je dôležité mať na pamäti, že sa nesmieme vo vnútri tejto metódy dotýkať ničoho z hlavného udalostného vlákna. Všimnite si použite dátového typu Decimal pri výpočte.

Výpočet Pi na Linuxe

Na obrázku vidíme výsledok výpočtu pre 63000 číslic.

Výpočet Pi na systéme Windows

Nasledujúci program počíta Pi na systéme Windows. Keďže na tomto systéme využíva multiprocessing modul pickle, platia pre nás obmedzenia modulu pickle. Funkcia generatePi() musí byť funkciou najvyššej úrovne v rámci modulu (nesmie byť metódou v rámci objektu) a objekt Queue je potrebné deklarovať ako globálny.

calculate_pi_windows.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
This script produces a long-running task of calculating
a large Pi number, while keeping the GUI responsive.
This is an example written for Windows.

Author: Jan Bodnar
Last modified: January 2016
Website: www.zetcode.com
"""

from tkinter import (Tk, BOTH, Text, E, W, S, N, END,
    NORMAL, DISABLED, StringVar)
from tkinter.ttk import Frame, Label, Button, Progressbar, Entry
from tkinter import scrolledtext

from multiprocessing import Process, Manager, Queue
from decimal import Decimal, getcontext


DELAY1 = 80
DELAY2 = 20

# Queue must be global
q = Queue()

class Example(Frame):

    def __init__(self, parent):
        Frame.__init__(self, parent, name="frame")

        self.parent = parent
        self.initUI()


    def initUI(self):

        self.parent.title("Pi computation")
        self.pack(fill=BOTH, expand=True)

        self.grid_columnconfigure(4, weight=1)
        self.grid_rowconfigure(3, weight=1)

        lbl1 = Label(self, text="Digits:")
        lbl1.grid(row=0, column=0, sticky=E, padx=10, pady=10)

        self.ent1 = Entry(self, width=10)
        self.ent1.insert(END, "4000")
        self.ent1.grid(row=0, column=1, sticky=W)

        lbl2 = Label(self, text="Accuracy:")
        lbl2.grid(row=0, column=2, sticky=E, padx=10, pady=10)

        self.ent2 = Entry(self, width=10)
        self.ent2.insert(END, "100")
        self.ent2.grid(row=0, column=3, sticky=W)

        self.startBtn = Button(self, text="Start",
            command=self.onStart)
        self.startBtn.grid(row=1, column=0, padx=10, pady=5, sticky=W)

        self.pbar = Progressbar(self, mode='indeterminate')
        self.pbar.grid(row=1, column=1, columnspan=3, sticky=W+E)

        self.txt = scrolledtext.ScrolledText(self)
        self.txt.grid(row=2, column=0, rowspan=4, padx=10, pady=5,
            columnspan=5, sticky=E+W+S+N)


    def onStart(self):

        self.startBtn.config(state=DISABLED)
        self.txt.delete("1.0", END)

        digits = int(self.ent1.get())
        accuracy = int(self.ent2.get())

        self.p1 = Process(target=generatePi, args=(q, digits, accuracy))
        self.p1.start()
        self.pbar.start(DELAY2)
        self.after(DELAY1, self.onGetValue)


    def onGetValue(self):

        if (self.p1.is_alive()):

            self.after(DELAY1, self.onGetValue)
            return
        else:

           try:

                self.txt.insert('end', q.get(0))
                self.txt.insert('end', "\n")
                self.pbar.stop()
                self.startBtn.config(state=NORMAL)

           except:
                print("queue is empty")

# Generate function must be a top-level module funtion
def generatePi(q, digs, acc):

    getcontext().prec = digs

    pi = Decimal(0)
    k = 0
    n = acc

    while k < n:
        pi += (Decimal(1)/(16**k))*((Decimal(4)/(8*k+1)) - \
            (Decimal(2)/(8*k+4)) - (Decimal(1)/(8*k+5))- \
            (Decimal(1)/(8*k+6)))
        k += 1

    print(q.put(pi))


def main():

    root = Tk()
    root.geometry("400x350+300+300")
    app = Example(root)
    root.mainloop()


if __name__ == '__main__':
    main()

Tento kód bol prepísaný pre platformu Windows.

Výpočet Pi na systéme Windows

Na obrázku vidíme číslo Pi, ktoré má 8156 číslic. Ak by sme chceli vypočítať väčšie Pi, nastane uviaznutie kvôli obmedzeniam objektuQueue a dátovodu operačného systému.

Zdroje

V tomto článku sme si ukázali, ako vykonávať časovo náročné úlohy v knižnici Tkinter a jazyku Python 3. Využili sme modul multiprocessing. Počas tvorby programu sme narazili na nízkoúrovňové technické obmedzenia. Článok vyšiel v angličtine na autorovej stránke: Long-running task in Tkinter.

Našli jste v článku chybu?

10. 2. 2016 10:04

pet (neregistrovaný)

Původně jsem myslel, že autor chce veřejnost upozornit na to, že při použití Tkinter nefunguje automatické uvolňování paměti zabrané automaticky zrušenými grafickými objekty, ale je potřeba každý objekt v rušeném stromu explicitně zrušit, případně že na to má nějaký „workaround“. Je to to největší omezení Tkinter, na které navíc není nikde upozorněno.

10. 2. 2016 7:45

Doli (neregistrovaný)

Krom výše zmíněného wtf o tom, že omezení velikosti Message v queue by rozhodně nemělo být technická překážka (i bez spigota by stačilo převést na string a poslat ve více částech, které hlavní vlákno zase spojí), mě ještě zaráží přístup autora k "multiplatformnímu kódu". Psát dvě verze téže aplikace je obvykle cesta do pekel. Obzvláště, když ji lze snadno napsat tak aby fungovala na linuxu i na windows. Tak snadno, že to dokonce autor v článku sám bezděky dokázal. Verze pro windows totiž v pohod…

Lupa.cz: Na koho se v Křišťálové Lupě nedostalo?

Na koho se v Křišťálové Lupě nedostalo?

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Vitalia.cz: To nejhorší při horečce u dětí: Febrilní křeče

To nejhorší při horečce u dětí: Febrilní křeče

Vitalia.cz: Baletky propagují zdravotní superpostel

Baletky propagují zdravotní superpostel

Lupa.cz: Co se dá měřit přes Internet věcí

Co se dá měřit přes Internet věcí

Vitalia.cz: Mondelez stahuje rizikovou čokoládu Milka

Mondelez stahuje rizikovou čokoládu Milka

Lupa.cz: Google měl výpadek, nejel Gmail ani YouTube

Google měl výpadek, nejel Gmail ani YouTube

Lupa.cz: Seznam mění vedení. Pavel Zima v čele končí

Seznam mění vedení. Pavel Zima v čele končí

Root.cz: Certifikáty zadarmo jsou horší než za peníze?

Certifikáty zadarmo jsou horší než za peníze?

Vitalia.cz: Často čůrá a má žízeň? Příznaky dětské cukrovky

Často čůrá a má žízeň? Příznaky dětské cukrovky

Měšec.cz: Air Bank zruší TOP3 garanci a zdražuje kurzy

Air Bank zruší TOP3 garanci a zdražuje kurzy

Podnikatel.cz: Podnikatelům dorazí varování od BSA

Podnikatelům dorazí varování od BSA

Lupa.cz: Avast po spojení s AVG propustí 700 lidí

Avast po spojení s AVG propustí 700 lidí

Podnikatel.cz: Babiš: E-shopy z EET možná vyjmeme

Babiš: E-shopy z EET možná vyjmeme

Root.cz: Vypadl Google a rozbilo se toho hodně

Vypadl Google a rozbilo se toho hodně

Podnikatel.cz: Prodává přes internet. Kdy platí zdravotko?

Prodává přes internet. Kdy platí zdravotko?

Vitalia.cz: To není kašel! Správná diagnóza zachrání život

To není kašel! Správná diagnóza zachrání život

Vitalia.cz: Jsou čajové sáčky toxické?

Jsou čajové sáčky toxické?

Vitalia.cz: Dáte si jahody s plísní?

Dáte si jahody s plísní?

120na80.cz: Horní cesty dýchací. Zkuste fytofarmaka

Horní cesty dýchací. Zkuste fytofarmaka