NB! Kui tekivad probleemid, soovite abi või vihjeid - siis küsige aine Slack kanalis (channel) #praktikum-1-lõimed
Esimene praktikum - Lõimede programmeerimine Pythonis
Hajussüsteemide sissejuhatuseks õpime ja tuletame meelde lõimedega programmeerimise kohta, kuna lõimede kasutamise juures kergivad esile mitmed sünkroniseerimise, eraldi töötavate osapoolde haldamise ja orkestreerimise probleemid, mis on omased hajusalt töötavates süsteemides. Lõimede teema on kaetud ka Objektorienteeritud aines, aga selles praktikumis kasutame lõimede programmeerimiseks Pythonit.
Selle praktikumi eesmärk on anda sissejuhatus lõimedepõhiste rakenduste loomisele Pythonis, kasutades erinevaid lõimehaldusmudeleid ja luues praktilise programmi, millega saab raamatuid alla tõmmata ning nende seest sõnede leidumist otsida.
Viited
- Lõimed Pythonis:
- Python Dokumentatsioon: https://docs.python.org/3/library/threading.html
- PyCharm õpetus: https://www.jetbrains.com/help/pycharm/quick-start-guide.html
- Lõimed Javas (Objektorienteeritud programmeerimine aines): https://courses.cs.ut.ee/2022/OOP/spring/Main/Practice12
Ettevalmistavad tegevused
Võite valida endale sobiva Pythoni programmeerimiskeskkonna, aga soovitan teil kasutada PyCharm Python IDE'd, kuna see lihtsustab projektide haldust, virtuaalsete keskkondade ülesseadmist ning selle Professional versioon on kasutatav Tartu Ülikooli tudengitele tasuta.
Soovituslikuks Python versiooniks on Python 3.7, 3.8 või 3.9, kuna ülesannete lahendused on testitud selles versioonis. 3.12 Pythonis on mitmel tudengil probleeme tekkinud.
- PyCharm Professional alla laadimine: https://www.jetbrains.com/pycharm/download/
- PyCharm kasutamise (inglise keelne) õpetus: https://www.jetbrains.com/help/pycharm/quick-start-guide.html
- PyCharm litsentside info: https://www.jetbrains.com/community/education/#students
Ülesanne 1.1: Raamatute alla laadimine Gutenberg repositooriumist
Kirjutame lihtsa ühelõimelise programmi raamatute alla laadimiseks ning salevstamiseks Gutenberg repositooriumist, kus on avatud litsentsiga ja tasuta raamatud kõigile kasutamiseks. Näiteks saab Sellel lehel nimekira juhuslikest raamatustest Gutenberg repositooriumis: https://www.gutenberg.org/ebooks/search/?sort_order=random
- Loo funktsioon
lae_alla_raamat(gutenberg_id):
, mis saab sisendiks Gutenberg raamatu numbri(ID), ning laeb sellele vastava raamatu (.txt faili) alla ning salvestab selle arvutisse eraldi failina.- gutenberg_id on näiteks 45774
- Ja sellele gutenberg_id'le vastav UTF8 tekstiformaadis raamatu aadress on:
- Faile alla laadimiseks veebist saab kasutada näiteks
response = requests.get()
meetodit Python requests paketist. Jaresponse.text
et tagastatava faili sisu "kätte saada" teksti kujul. - Faili saab salvestada endale sobivasse kausta. Näiteks PyCharm projekti alamkaustas raamatud
- Faili salvestamisel saab kasutada lihtsat Python open() ja write() meetodeid, aga soovitatav on lisada encoding="utf-8" parameeter:
f = open(file_name, 'w+', encoding="utf-8") f.write(content) f.close()
- gutenberg_id on näiteks 45774
- Kirjuta programmi põhimeetod, kus on list raamatute gutenberg_id'st ning kus on tsükkel, mis kutsub
lae_alla_raamat
funktsiooni välja iga listis oleva raamatu gutenberg_id peal.- Testi, et rakendus töötab vabalt valitud raamatute id'de korral.
- Võimalik on kasutada ka vahemikke, näiteks
gutenberg_id_list = range(100,120)
- Võimalik on kasutada ka vahemikke, näiteks
- Näiteks:
if __name__ == '__main__': gutenberg_id_list = range(100,120) for gutenberg_id in gutenberg_id_list:
- Testi, et rakendus töötab vabalt valitud raamatute id'de korral.
- Mõõda, kui palju aega võtab 50 raamatu alla laadimine. Täpsus võiks olla millisekundites.
- Saab kasutada näiteks Python time paketi meetodit
time.time()
- Saab kasutada näiteks Python time paketi meetodit
NB! Salvesta ülesande lahendus eraldi Python skriptina.
- Soovitus:
- Lõimede töö kontrollimiseks ja silumiseks võiks kasutada logimist.
- Näide logimise üles seadmiseks, kus väljastatakse ka praegune aeg:
import logging logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.INFO, datefmt="%H:%M:%S") logging.info("Programm: Enne loimede loomist") logging.info("Lõim alustab tööd")
Ülesanne 1.2: Lõimede loomine
Täiendame eelmise ülesande lahendust, käivitades kõik funktsiooni lae_alla_raamat()
väljakutsed eraldi lõimes.
- Kasuta paketi
threading
meetoditThread()
et luua üks lõim iga funktsioonilae_alla_raamat()
väljakutse kohta- Näide:
x = threading.Thread(target=loime_funktsioon, args=(142,)) x.start()
- Selles näites on:
loime_funktsioon
on funktsioon mida kutsutakse välja eraldi lõimena- 142 on funktsiooni
loime_funktsioon
argument. Argumendid on Python tuple andmestruktuuris( )
komadega eraldatult x.start()
käivitab lõime
- Näide:
- Mõõda, kui palju aega võtab nüüd 50 raamatu alla laadimine. Täpsus võiks olla millisekundites.
- Küsimused:
- Mis toimub nüüd käivitusaja mõõtmisel?
- Kas arvutatud aeg on korrektne? Miks?
- Mis toimub nüüd käivitusaja mõõtmisel?
Skripti käivitamisaega saab mõõta ka väljaspool skripti:
- Windowsis saab kasutada (Kas PowerPoint käsureal või PyCharm terminalis) Measure-Command käsku.
- Näide:
Measure-Command { python ylesanne1.2.py }
- Näide:
- Linuxis saab kasutada
time
käsku- Näide:
time python ylesanne1.2.py
- Näide:
- Küsimused:
- Mis on selle ja eelmise ülesande lahenduse jooksutamise aja erinevus (50 raamatu alla laadimisel) sellisel viisil mõõtes?
- Mis on teie arvates peamine põhjus või mõjutegur, miks lõimede põhine lahendus on aeglasem/kiirem?
NB! Salvesta ülesande lahendus eraldi Python skriptina.
Ülesanne 1.3: Lõimede grupid
Kui me soovima alla laadida sadu faile, siis on ebamõistlik kõikide jaoks individuaalne lõim sama aegselt tööle panna. Selle asemel on tihti mõistlik piirata lõimede maksimaalne arv ning kasutusele võtta lõimede grupid (Thread Pools)
- Uurige kuidas kasutada Pythoni
concurrent.futures.ThreadPoolExecutor
klassi ning looge uus versioon programmist, mis kasutab maksimaalselt 10 lõime suurust lõimede gruppi.- Dokumentatioon ja lihtsad näited: https://docs.python.org/3/library/concurrent.futures.html
- Mõõtke kui palju aega võtab ThreadPoolExecutor't kasutades 50 raamatu alla laadimine ja võrrelge tulemust eelmise kahe lahenduse tulemusega.
- Soovitus: Kasutada ThreadPoolExecutor meetodit
submit()
lõimede loomiseks.
NB! Salvesta ülesande lahendus eraldi Python skriptina.
Ülesanne 1.4: Lõimede töö sünkroniseerimine
Eelmistes ülesannetes on meie lõimed olnud täiesti iseseisvad ning nende töö ei mõjutanud üksteist kuidagi. Selles ülesandes vaatame kuidas panna lõimed koostööd tegema selleks, et arvutada globaalset väärtust.
- Teeme uue versiooni Pythoni programmist, mis laeb raamatud alla ning (selle asemel, et faile salvestada) otsib ja loendab mitu korda failis esineb mingi otsingu sõne. Näiteks "Estonia" või "language".
- Terve programm peab väljastama kogu arvu, mitu korda selline sõne kõigis läbivaadatud raamatutes esines.
- Loendurina kasutame globaalset muutujat, mida jagatakse lõimede vahel.
- Lisaks peaksid lõimed globaalset muutujad jooksvalt uuendama, iga kord kui otsingu sõne leitakse mõnel raamatu real.
- Lõim peaks raamatu sisu läbi vaatama rida-rea haaval.
- Raamatuid enam failidesse salvestama ei pea.
- Lõime funktsioon peab iga kord uuendama globaalse loenduri väärtust. (Mitte ainult üks kord lõime töö lõpus)
- Programmi lõpus peaks välja printima otsingu sõne ning mitu korda see alla tõmmatud raamatutes leidus.
Vihjed:
- Teksti jagamine ridadeks:
for line in content.splitlines():
- Globaalsete muutujate defineerimine:
def lae_alla_raamat_ja_otsi_sone(raamat, sone): global globaalne_summa ... if __name__ == '__main__': global globaalne_summa globaalne_summa = 0
- Selleks et loendada kui mitu korda mõni sõna leidub pikemas tekstis/sõnes saab teksti jagada
split()
meetodi abil sõnade listiks ning kasutadapikem_sone.lower().split().count(otsingusone)
meetodit selleks et leida mitu korda sõne sellises listis leidub.
- Teksti jagamine ridadeks:
NB! Salvesta ülesande lahendus eraldi Python skriptina.
Boonus ülesanne: Lõimede töö sünkroniseerimine jagatud objektide ja lukkudega
See on boonus ülesanne, mis ei ole kohustuslik, et praktikumi lahenduse eest täispunktid saada.
(Näita boonus ülesande kirjeldust)
Asendame globaalse muutuja MeieLoendur
klassi objektiga, kus on lukk sisse ehitatud selleks, et kontrollida, et ainult üks lõim saab korraga loenduri väärtust muuta.
See ei pruugi kõige kasulikum olla selle ülesande raames - lihtsamate loendurite puhul, aga kui samaaegselt on vaja mitut loendurit muuta, siis ei piisa lihtsast globaalsest muutujast.
Loome uue klassi MeieLoendur
mis sisaldab meetodit loenduri väärtuse suurendamiseks ning lukku mis lubab ainult ühel lõimel korraga selle meetodi sees loenduri väärtust muuta.
class MeieLoendur: def __init__(self): self.loendur = 0 self._lock = threading.Lock() def locked_update(self, count): with self._lock: self.loendur += count def print(self): if(self.loendur > 0): print("Praegune loenduri väärtus on: ", self.loendur)
- Muutke programm ümber nii, et globaalse muutuja asmel kasutatakse globaalset MeieLoenduri objekti.
def lae_alla_raamat_ja_otsi_sone(raamat, sone): global loendur ... if __name__ == '__main__': global loendur loendur = MeieLoendur()
- Loenduri väärtuse muutmine
loendur.locked_update(lisatav_vaartus)
- Loenduri seisu printimine programmi lõpus:
loendur.print()
NB! Salvesta ülesande lahendus eraldi Python skriptina.
Mittekohustuslik lisategevus:
- Teeme eraldi lõime (ja funktsiooni), mis iga paari sekundi tagant prindib välja loenduri hetke väärtuse otsingu ajal.
- Ja muul ajal jääb magama (time.sleep(x))
Lahenduse esitamine
Praktikumi lahendusena tuleb esitada:
- Python kood iga ülesande (1.1, 1.2, 1.3, 1.4) kohta.
- Programmi väljundi näide iga ülesande lahenduse kohta, kas tekstfailina (programmi väljundi logi) või ekraanipildina.
- Vastus ülesannete raames püstitatud küsimusetele (Küsimused on esile tõstetud sinise teksti abil)
- Failid tuleks kokku pakkida üheks Zip failiks enne üles laadimist.
- Boonus ülesande lahendamise korral:
- Python kood
- Programmi väljundi näide