Sissejuhatus konkurrentsusesse
Sissejuhatus
Kõik koodinäited, mida antud õppematerjalid näiteks on toonud, on täitnud käske ükshaaval, kindlas järjekorras. See tähendab, et käivitus üks rida, see lõpetas oma töö ning seejärel käivitati järgmine rida. Sellist ühelõimelist mudelit on lihtne mõista, kuid jätab suure osa arvuti võimekusest kasutamata ega sobi olukordadesse, kus mitu tegevust peab tegelikult toimuma samaaegselt.
Kaasaegsetel arvutitel on mitu protsessorituuma - näiteks Apple M4 Pro protsessoril on 14 tuuma. Ühe lõimega programm kasutab neist vaid ühte, sõltumata sellest, kui palju neid tegelikult olemas on. Lisaks toorele jõudlusele peavad paljud programmid reageerima paralleelselt saabuvatele sündmustele. Olgu selleks siis kas veebiserveri klientide teenindamine, failide lugemine kettalt jne.
Konkurrentsus (concurrency) on programmeerimise valdkond, mis tegeleb just nende probleemide lahendamisega. Selle mõistmine aitab koodi üles ehitada nii, et mitu tegevust saaksid olla korraga töös, kuidas neid ohutult koordineerida ning kuidas vältida vigu, mis tekivad ainult siis, kui kasutusel on rohkem kui üks lõim.
Konkurrentsus vs paralleelsus
Neid kahte mõistet kasutatakse sageli vaheldumisi, kuid neil on siiski veidi erinev tähendus.
Paralleelsus (parallelism) tähendab sõna otseses mõttes käskude täitmist samal ajahetkel erineval füüsilisel riistvaral - tavaliselt erinevatel protsessorituumadel. Kui kaks tuuma teevad korraga kahte arvutust, on tegemist paralleelse täitmisega.
Konkurrentsus (concurrency) tähendab programmi ülesehitamist nii, et mitu ülesannet saavad edeneda sama ajavahemiku jooksul, ilma et oleks määratud, kas need töötavad samal ajal. Üksainus protsessorituum saab täita kahte konkureerivat ülesannet, vahetades nende vahel väga kiiresti - mõlemad ülesanded edenevad, kuid igal konkreetsel hetkel töötab neist vaid üks.
Javas võib sama kood töötada mõlemal viisil, sõltuvalt kasutatavast riistvarast. Programmeerija kirjutab konkurrentse koodi, kuid operatsioonisüsteem ja JVM otsustavad, kas seda käivitatakse tõeliselt paralleelselt mitmel protsessorituumal või vaheldumisi ühel tuumal. Seetõttu ei sõltu turvalise konkurrentse koodi kirjutamise reeglid tuumade arvust - samad vead võivad esineda mõlemal juhul, kuid suurema tuumade arvuga on neid lihtsam esile kutsuda.
Protsess vs lõim
Protsess (process) on iseseisev töötav programm. Igal protsessil on oma mäluruum ning see ei saa otseselt lugeda ega muuta teiste protsesside mälu. Operatsioonisüsteem hoiab protsessid turvalisuse huvides üksteisest eraldatuna.
Lõim (thread) on iseseisev täitmisüksus (unit of execution) protsessi sees. Sama protsessi lõimed jagavad ühist mälu ehk need näevad ja saavad muuta samu objekte, mis nende kasutuses on. See teeb lõimed ühtaega kasulikuks (kiire andmevahetus ühise mälu kaudu) ja ohtlikuks (halb koordineerimine võib viia andmete rikkumiseni).
Vaikimisi on igal Java programmil olemas juba vähemalt üks lõim - pealõim (main thread), mis käivitab main meetodi.
Lisalõimed luuakse siis, kui programmil on vaja teha mitut samaaegset asja.
Miks konkurrentsus oluline on
Reaalsed programmid kasutavad konkurrentsust kolmel erineval põhjusel ning igaüks neist nõuab veidi erinevat lähenemist.
Protsessorimahukas töö. Arvutused - näiteks pilditöötlus, füüsikamudeli simuleerimine või masinõppemudeli treenimine - võivad valmida kiiremini, kui töö jaotada mitme tuuma vahel ära. Kaheksa tuuma suudab töö teoreetiliselt lõpetada umbes kaheksa korda kiiremini, kui ülesanne on hästi osadeks jagatav.
Sisendi- ja väljundimahukas töö. Kui lõim ootab ketta-, võrgu- või andmebaasipäringu järgi, ei tee see samal ajal midagi kasulikku. Teised lõimed saavad selle aja jooksul protsessorit kasutada. Näiteks veebiserver, kus igal päringul on oma lõim, suudab teenindada korraga palju kliente, sest enamik lõimi veedab suure osa ajast I/O-d oodates, jättes protsessori vabaks teiste jaoks.
Reageerimisvõime parandamine. Töölauarakendus või mäng peab jääma kasutaja jaoks sujuvaks ka siis, kui taustal laetakse faile või tehakse keerulisi arvutusi. Pikalt kestvad tööd käivitatakse taustalõimedes, et pealõim saaks jätkata kasutaja sisendi töötlemist.
Mida järgnevates peatükkides käsitleme
Järgnevad artiklid loovad samm-sammult aluse mitmelõimeliste Java programmide kirjutamiseks:
- Lõimed - kuidas lõime luua ja käivitada ning kuidas nende lõppemist oodata
- Sünkroneerimine ja tõrkeolukorrad (race conditions) -
synchronizedmärksõna ja miks jagatud muudetav olek on ohtlik - Tupik (deadlock) - mis juhtub siis, kui lõimed jäävad üksteist lõputult ootama
- Atomaarsed muutujad - lukuvabad loendurid ja viited
- wait/notify ja BlockingQueue - tootja-tarbija muster
- ExecutorService ja lõimepuulid - taaskasutatavad lõimed,
CallablejaFuture
Esituse järjekord on teadlik: iga artikkel eeldab eelnevate mõistmist ning enamik tegelikke konkurentsusega seotud vigu tekib vähemalt kahe erineva mehhanismi koosmõjus.
Brian Goetzi raamat Java Concurrency in Practice on selle teema rangelt soovituslik lisalugemine kõigile, kes plaanivad professionaalselt konkurrentset koodi kirjutada.