Säikeistys - TiViOpetus/Autolainaus GitHub Wiki

Säikeistys

Monimutkaiset ja prosessori-intensiiviset toiminnot voivat aiheuttaa käyttöliittymän jäätymisen. Sen estämiseksi sovellus voi käyttää säikeitä (thread). Moniajoa (Multitasking) tukevat käyttöjärjestelmät kuten MacOS, Windows ja Linux mahdollistavat ohjelman eri toimintojen sijoittamisen erillisiksi säikeiksi. Moniajossa prosessori suorittaa hetken jokaista säiettä vuorollaan. Ohjelmistokehittäjä voi tarvittaessa määritellä kuinka usein prosessoriaikaa hänen koodaamansa säie saa. Käyttöjärjestelmä kuitenkin muuttaa ohjelmoijan määrittämiä asetuksia tarpeen mukaan. Esim. Windows koneessa aktiivisen ikkunan säie saa prosessoriaikaa useammin kuin muut säikeet (foreground boost).

Koodista, joka estää käyttöliittymän käyttämisen käytetään usein termiä blocking code ja sellaisesta koodista, jonka suorittaminen ei estä pääohjelman suoritusta tai käyttöliittymää toimimasta termiä non-blocking code. Helpoin tapa hallita koodia on säikeistäminen (multithreading). Kaikki ohjelmointikielet eivät kuitenkaan tue säikeistystä. Esim. JavaScript ei tue sitä, ja siinä joudutaan totetuttamaan non-blocking code käyttämällä erilaisia asynkronisia funktiorakenteita. Yleensä ne tekevät koodista vaikeasti luettavaa. Pythonissa on suora säikeistyksen tuki. Lisäksi QtCore tarjoaa omat säikeistysvälineet graafisiin sovelluksiin.

Windows-maailmassa käytettävät käsitteet:

  • Prosessi (process) on ohjelman tekemä muistivaraus koneen keskusmuistissa, sitä ei suoriteta.
  • Säie on ohjelman osa, jonka prosessoriydin (core) suorittaa.
  • Jokaisella ohjelmalla on vähintään yksi säie ns. pääsäie (thread 0), vaikkei ohjelmointikielessä olisikaan säikeistyksen tukea.
  • Jos pääsäikeessä tapahtuu virhe, koko ohjlema kaatuu.
  • Pääsäie voi käynnistää alisäikeitä (subthread), joiden virheet eivät kaada pääsäiettä.
  • Alisäikeitä luodaan yleensä tiedostonkäsittelyä, video- ja audiovirtaa (stream) varten.
  • Prioriteetti (priority) määrittelee kuinka usein käyttöjärjestelmä antaa säikeelle prosessoriaikaa. Ohjelmoija määrittelee säikeen ns. perusprioriteetin (base priority), jota käyttöjärjestelmä nostaa tai laskee tarpeen mukaan. Useimmissa tilanteissa prioriteetti jätetään määrittelemättä, jolloin se on normaali (normal).

QThread-säikeistys

PySide6-sovelluksissa kannattaa käyttää Qt:n omaa säikeistystä. Se saadaan käyttöön ottamalla QThreadPool ja Slot osaksi sovellusta. Toiminnolle, jota halutaan suorittaa omana säikeenään, luodaan slotti. Sen lisäksi luodaan toinen slotti, joka muodostaa säikeen ja puolestaan kutsuu ensimmäistä slottia, joka suorittaa varsinaisen toiminnon. Seuraavassa esimerkissä toistetaan wav-tiedosto omana säikeenään, jolloin käyttöliittymä pysyy responsiivisena myös äänitiedoston toiston aikana. playWavFile(self)-metodi on määritelty slotiksi käyttämällä erillistä dekoraattoria @Slot(). Tämä on varsinainen äänitiedoston toistorutiini. Sitä kutsutaan toisesta metodista playWavFileThread(self), joka luo säikeen, jossa kutsutaan playWavFile(self)-metodia.

# PYSIDE6-MALLINE SOVELLUKSEN PÄÄIKKUNAN LUOMISEEN
# KÄÄNNETYSTÄ KÄYTTÖLIITTYMÄTIEDOSTOSTA (mainWindow_ui.py)
# =====================================================

# KIRJASTOJEN JA MODUULIEN LATAUKSET
# ----------------------------------
import os # Polkumääritykset
import sys # Käynnistysargumentit

from PySide6 import QtWidgets # Qt-vimpaimet
from PySide6.QtCore import QThreadPool, Slot # Säikeistys ja Slot-dekoraattori
from threadedSound_ui import Ui_MainWindow # Käännetyn käyttöliittymän luokka

from lendingModules import sound # Äänipalaute

# Määritellään luokka, joka perii QMainWindow- ja Ui_MainWindow-luokan
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    """A class for creating main window for the application"""
    
    # Määritellään olionmuodostin ja kutsutaan yliluokkien muodostimia
    def __init__(self):
        super().__init__()

        # Luodaan säievaranto (thread pool)
        self.threadPool = QThreadPool()

        # Luodaan käyttöliittymä konvertoidun tiedoston perusteella MainWindow:n ui-ominaisuudeksi. Tämä suojaa lopun MainWindow-olion ylikirjoitukselta, kun ui-tiedostoa päivitetään
        self.ui = Ui_MainWindow()

        # Kutsutaan käyttöliittymän muodostusmetodia setupUi
        self.ui.setupUi(self)

        # OHJELMOIDUT SIGNAALIT
        # ---------------------
        
        # Kun Lainaaja-kentästä poistutaan soitetaan äänitiedosto readKey.WAV
        self.ui.lenderLineEdit.returnPressed.connect(self.playWavFileThread)

    # OHJELMOIDUT SLOTIT
    # ------------------


    # Soitetaan äänitiedosto, huom! @Slot() dekoraattori
    # Äänelle luodaan oma slot dekoraattoria käyttäen, jotta säikeistys
    # onnistuu. Jos yritetään kutsua suoraan sound.playWav-metodia, sen suoritus
    # jäädyttää käyttöliittumän. Tästä syystä on tehty erillinen slot, jota ei kutsuta
    # suoraan vaan playWavFileThread slotin kautta.

    @Slot()
    def playWavFile(self):
        sound.playWav('sounds\\readKey.WAV')

    # Luodaan säie, joka suorittaa äänitiedoston soittamisen   
    @Slot()
    def playWavFileThread(self):
        self.ui.carLineEdit.setFocus()
        self.threadPool.start(self.playWavFile)


# Luodaan sovellus
app = QtWidgets.QApplication(sys.argv)

# Luodaan objekti pääikkunalle ja tehdään siitä näkyvä
window = MainWindow()
window.show()

# Käynnistetään sovellus ja tapahtumienkäsittelijä
app.exec()    

Python-säikeistys

Itse ohjelmointikieli sisältää säikeistyksen. Sitä voi käyttää suoraan tai säievarantoa hyödyntämällä. Seuraavassa esimerkissä on luotu säie, jota suoritetaan varsinaisen pääsäikeen rinnalla:

# PYTHONIN PERUSSÄIKEISTYS
# ========================

import threading # Säikeistyksen tuki
import time

def longLastingFucntion(parameter):
    time.sleep(10) # Odotetaan 10 sekuntia
    print(parameter)

if __name__ == "__main__":

    # Luodaan säie, joka kutsuu longLastingFunktiota, funktion argumentit annetaan monikkona, vaikka niitä on vain yksi -> args=('hippopotamus',)
    thread1 = threading.Thread(target=longLastingFucntion, args=('Hippopotamus',))
    thread1.start()
    print('Valmis')

Ohjelma tulostaa ensin valmis, koska pääsäiettä suoritetaan samalla kun thread1-säikeen suoritus on edelleen käynnissä

Jos halutaan, että pääsäie odottaa alisäikeen valmistumista käytetään join()-funktiota:

import threading # Säikeistyksen tuki
import time

def longLastingFucntion(parameter):
    time.sleep(10) # Odotetaan 10 sekuntia
    print(parameter)

if __name__ == "__main__":

    # Luodaan säie, joka kutsuu longLastingFunktiota, funktion argumentit annetaan monikkona, vaikka niitä on vain yksi -> args=('hippopotamus',)
    thread1 = threading.Thread(target=longLastingFucntion, args=('Hippopotamus',))
    thread1.start()
    thread1.join() # Odotetaan kunnes säikeen suoritus päättyy
    print('Valmis')

Jos ei ole varmuutta säikeen suorituksen onnistumisesta, säikeen valmistusmisen odottamiseen voidaan määritellä aikakatkaisu (timeout) sekunteina, esim. thread1.join(30), jolloin valmistumista odotetaan enintään 30 sekuntia. Jos säie ei valmistu siihen mennessä, pääsäikeen suoritusta jatketaan.

Edellisessä esimerkissä käyttöliittymät ovat yksinkertaisia ja niiden ajoituksessa ei ole ongelmia. PySide6:n @Slot()-dekoraattorin kanssa saattaa joutua käyttämään perinteistä lambda-välittäjäfunktiota ja luoda säievaranto käyttämällä globalInstance()-metodia, kuten seuraavassa esimerkissä on tehyty

# Määritellään luokka, joka perii QMainWindow- ja Ui_MainWindow-luokan
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    """A class for creating a main window for the application"""
    
    # Määritellään olionmuodostin ja kutsutaan yliluokkien muodostimia
    def __init__(self):
        super().__init__()

        # Luodaan säikeistystä varten uusi säievaranto
        self.threadPool = QThreadPool().globalInstance()
...

    # OHJELMOIDUT SLOTIT
    # ------------------
   
    # Soita parametrina annettu äänistiedosto (työfunktio)
    @Slot(str)
    def playSoundFile(self, soundFileName):
        fileAndPath = 'sounds\\' + soundFileName
        sound.playWav(fileAndPath)
    
    # Säikeen käynnistävä funktio 
    @Slot(str)
    def playSoundInTread(self, soundFileName):
        self.threadPool.start(lambda: self.playSoundFile(soundFileName))