Millistel eesmärkidel kasutatakse mitme keermega süsteeme. Kaheksa lihtsat reeglit mitme lõimega rakenduste arendamiseks

Milline teema tekitab algajatele kõige rohkem küsimusi ja raskusi? Kui küsisin selle kohta oma õpetajalt ja Java programmeerijalt Aleksander Pryakhinilt, vastas ta kohe: "Multithreading". Täname teda idee ja abi eest selle artikli ettevalmistamisel!

Vaatleme rakenduse ja selle protsesside sisemaailma, selgitame välja, mis on multithreadingu olemus, millal see on kasulik ja kuidas seda rakendada - kasutades Java näitel. Kui õpite mõnda teist OOP-keelt, ärge muretsege: põhiprintsiibid on samad.

Ojadest ja nende tekkest

Mitmelõimelisuse mõistmiseks mõistame kõigepealt, mis on protsess. Protsess on osa virtuaalmälust ja ressurssidest, mille OS eraldab programmi käitamiseks. Kui avate sama rakenduse mitu eksemplari, eraldab süsteem igaühe jaoks protsessi. Kaasaegsetes brauserites võib iga vahekaardi eest vastutada eraldi protsess.

Tõenäoliselt olete kohanud Windowsi "Task Manager" (Linuxis on see "System Monitor") ja teate, et mittevajalikud jooksvad protsessid koormavad süsteemi ja kõige "raskemad" neist sageli külmuvad, mistõttu tuleb need sunniviisiliselt lõpetada. .

Kuid kasutajatele meeldib multitegumtöö: ärge andke neile leiba – laske neil avada kümmekond akent ja hüpata edasi-tagasi. Tekib dilemma: peate tagama rakenduste samaaegse töö ja samal ajal vähendama süsteemi koormust, et see ei aeglustuks. Oletame, et riistvara ei suuda omanike vajadustega sammu pidada – peate probleemi lahendama tarkvara tasemel.

Soovime, et protsessor täidaks rohkem juhiseid ja töötleks rohkem andmeid ajaühiku kohta. See tähendab, et peame mahutama igasse ajalõiku rohkem käivitatud koodi. Mõelge koodi täitmise ühikule kui objektile – see on niit.

Keerulisele juhtumile on lihtsam läheneda, kui jagada see mitmeks lihtsaks. Nii et kui töötate mäluga: "raske" protsess jaguneb lõimedeks, mis võtavad vähem ressursse ja viivad tõenäolisemalt koodi kalkulaatorisse (kuidas täpselt - vt allpool).

Igal rakendusel on vähemalt üks protsess ja igal protsessil on vähemalt üks lõime, mida nimetatakse põhilõimeks ja millest vajadusel käivitatakse uued.

Lõimede ja protsesside erinevus

    Lõimed kasutavad protsessi jaoks eraldatud mälu ja protsessid nõuavad oma mäluruumi. Seetõttu luuakse ja valmivad lõimed kiiremini: süsteem ei pea neile iga kord uut aadressiruumi eraldama ja seejärel vabastama.

    Protsessid töötavad igaüks oma andmetega - nad saavad midagi vahetada ainult protsessidevahelise suhtluse mehhanismi kaudu. Lõimed pääsevad üksteise andmetele ja ressurssidele otse juurde: see, mida üks on muutnud, on kohe kõigile kättesaadav. Lõim saab juhtida protsessis osalevat "kaaslast", samas kui protsess juhib ainult oma "tütreid". Seetõttu on voogude vahetamine kiirem ja nendevaheline suhtlus lihtsam.

Mis on sellest järeldus? Kui teil on vaja võimalikult kiiresti töödelda suurt hulka andmeid, jagage need tükkideks, mida saab töödelda eraldi lõimedega, ja seejärel ühendage tulemus. See on parem kui ressursinäljaste protsesside kudemine.

Kuid miks selline populaarne rakendus nagu Firefox loob mitu protsessi? Kuna just brauseri jaoks on isoleeritud vahelehed töökindel ja paindlik. Kui ühe protsessiga on midagi valesti, ei ole vaja kogu programmi lõpetada – on võimalik salvestada vähemalt osa andmetest.

Mis on multithreading

Nii et jõuame põhipunktini. Mitme lõimega töötlemine on siis, kui rakendusprotsess jagatakse lõimedeks, mida protsessor töötleb paralleelselt – ühes ajaühikus.

Arvutuskoormus jaotatakse kahe või enama tuuma vahel, nii et liides ja muud programmikomponendid ei aeglustaks üksteise tööd.

Mitme lõimega rakendusi saab käivitada ühetuumalistes protsessorites, kuid siis käivitatakse lõime kordamööda: esimene töötas, selle olek salvestati - teisel lasti töötada, salvestati - naasis esimesse või käivitati kolmas, jne.

Hõivatud inimesed kurdavad, et neil on ainult kaks kätt. Protsessidel ja programmidel võib olla nii palju käsi kui vaja, et ülesanne võimalikult kiiresti lõpule viia.

Oodake signaali: sünkroonimine mitme keermega rakendustes

Kujutage ette, et mitu lõime üritavad sama andmeala korraga muuta. Kelle muudatused lõpuks vastu võetakse ja kelle muudatused tühistatakse? Et vältida segadust jagatud ressurssidega, peavad lõimed oma tegevusi kooskõlastama. Selleks vahetavad nad teavet signaalide abil. Iga lõim räägib teistele, mida ta teeb ja milliseid muutusi oodata. Seega sünkroonitakse kõigi lõimede andmed ressursside hetkeseisu kohta.

Põhilised sünkroonimistööriistad

Vastastikune välistamine (vastastikune välistamine, lühendatult - mutex) - "lipp", mis läheb lõimele, millel on praegu lubatud töötada jagatud ressurssidega. Kõrvaldab teiste lõimede juurdepääsu hõivatud mälualale. Rakenduses võib olla mitu mutexet ja neid saab protsesside vahel jagada. Siin on konks: mutex sunnib rakendust iga kord juurde pääsema operatsioonisüsteemi tuumale, mis on kallis.

Semafor - võimaldab piirata lõimede arvu, millel on antud hetkel juurdepääs ressursile. See vähendab protsessori koormust, kui käivitate koodi, kus esineb kitsaskohti. Probleem on selles, et optimaalne lõimede arv sõltub kasutaja masinast.

Sündmus - määratlete tingimuse, mille ilmnemisel viiakse juhtimine üle soovitud lõimele. Vood vahetavad sündmuste andmeid, et üksteise tegevust arendada ja loogiliselt jätkata. Üks sai andmed kätte, teine ​​kontrollis nende õigsust, kolmas salvestas kõvakettale. Sündmused erinevad nende tühistamise viisi poolest. Kui teil on vaja sündmusest teavitada mitut lõime, peate signaali peatamiseks käsitsi määrama tühistamise funktsiooni. Kui sihtlõime on ainult üks, saate luua automaatse lähtestamise sündmuse. See peatab signaali ise pärast voogu jõudmist. Paindliku voo juhtimise jaoks saab sündmusi järjekorda panna.

Kriitiline osa - keerulisem mehhanism, mis ühendab silmusloenduri ja semafori. Loendur võimaldab semafori algust soovitud ajaks edasi lükata. Eeliseks on see, et kernel aktiveeritakse ainult siis, kui sektsioon on hõivatud ja semafor tuleb sisse lülitada. Ülejäänud aja töötab lõim kasutajarežiimis. Paraku saab jaotist kasutada ainult ühes protsessis.

Kuidas Java-s mitmelõimestust rakendada

Lõim klass vastutab Java lõimedega töötamise eest. Ülesande täitmiseks uue lõime loomine tähendab klassi Thread eksemplari loomist ja selle seostamist soovitud koodiga. Seda saab teha kahel viisil:

    alamklass Niit;

    rakendage oma klassis Runnable liides ja seejärel edastage klassi eksemplarid Thread-konstruktorile.

Kuigi me ei puuduta ummikseisu teemat, kui lõimed blokeerivad üksteise tööd ja tarduvad, jätame selle järgmise artikli jaoks.

Java mitmelõimeline näide: pingpong mutexidega

Kui arvate, et midagi kohutavat on juhtumas, hingake välja. Kaalume sünkroonimisobjektidega töötamist peaaegu mängulisel viisil: mutex viskab kaks lõime, kuid tegelikult näete reaalset rakendust, kus korraga saab töödelda ainult üks lõim.

Kõigepealt loome klassi, mis pärib meile juba teada oleva lõime omadused, ja kirjutame kickBall-meetodi:

Avalik klass PingPongThread pikendab lõime (PingPongThread (stringi nimi) (this.setName (nimi); // alistab lõime nime) @Alista avaliku void run () (Ball ball = Ball.getBall (); while (ball.isInGame () ) (kickBall (pall);)) privaatne void kickBall (pallipall) (if (! ball.getSide (). võrdub (getName ())) (ball.kick (getName ());)))

Nüüd hoolitseme palli eest. Ta ei saa meiega olema lihtne, vaid meeldejääv: et ta teaks, kes teda lõi, milliselt küljelt ja mitu korda. Selleks kasutame mutexi: see kogub teavet iga lõime töö kohta - see võimaldab eraldatud lõimedel üksteisega suhelda. Pärast 15. tabamust võtame palli mängust välja, et mitte tõsiselt vigastada.

Avalik pall (privaatne int kicks = 0; privaatne staatiline palli eksemplar = uus pall (); privaatne stringi pool = ""; privaatne pall () () staatiline pall getBall () (tagasi eksemplar;) sünkroniseeritud tühilöök (Stringi mängijanimi) (löögid ++; pool = mängijanimi; System.out.println (löögid + "" + pool);) String getSide () (tagasipool;) tõeväärtus isInGame () (tagasi (löögid)< 15); } }

Ja nüüd on sündmuskohale sisenemas kaks mängija lõime. Nimetagem neid ilma pikema jututa Pingiks ja Pongiks:

Avaliku klassi PingPongGame (PingPongThread player1 = uus PingPongThread ("Ping"); PingPongThread mängija2 = uus PingPongThread ("Pong"); Pallipall; PingPongGame () (pall = Ball.getBall ();) kehtetu algusMäng () visked (mäng katkestatud1 .start (); player2.start ();))

"Täis staadion rahvast – aeg matšiga alustada." Koosoleku avamisest anname ametlikult teada - rakenduse põhiklassis:

Avaliku klassi PingPong (avalik static void main (String args) viskab InterruptedException (PingPongGame mäng = uus PingPongGame (); game.startGame ();)))

Nagu näete, pole siin midagi vihast. See on praegu vaid sissejuhatus multithreadingisse, kuid teate juba, kuidas see töötab, ja saate katsetada – piirake mängu kestust mitte löökide arvu, vaid näiteks ajaga. Multithreadingu teema juurde tuleme hiljem tagasi – vaatleme paketti java.util.concurrent, Akka teeki ja volatiilset mehhanismi. Räägime ka Pythonis mitmelõime rakendamisest.

Mitme lõimega programmeerimine ei erine põhimõtteliselt sündmustepõhiste graafiliste kasutajaliideste kirjutamisest või isegi lihtsate järjestikuste rakenduste kirjutamisest. Siin kehtivad kõik olulised reeglid, mis reguleerivad kapseldamist, murede eraldamist, lahtist sidumist jne. Kuid paljudel arendajatel on keeruline mitmelõimega programme kirjutada just seetõttu, et nad eiravad neid reegleid. Selle asemel püüavad nad praktikas rakendada palju vähem olulisi teadmisi lõimede ja sünkroonimisprimitiivide kohta, mis on ammutatud algajatele mõeldud mitmelõimelise programmeerimise tekstidest.

Mis need reeglid siis on

Teine programmeerija, kes seisab silmitsi probleemiga, arvab: "Oh, täpselt, me peame rakendama regulaaravaldisi." Ja nüüd on tal juba kaks probleemi – Jamie Zawinski.

Teine programmeerija, kes seisab silmitsi probleemiga, arvab: "Oh, eks ma kasutan siin vooge." Ja nüüd on tal kümme probleemi – Bill Schindler.

Liiga paljud programmeerijad, kes võtavad ette mitme lõimega koodi kirjutamise, langevad lõksu, nagu Goethe ballaadi kangelane. Nõia õpipoiss". Programmeerija õpib, kuidas luua hunnik lõime, mis põhimõtteliselt töötavad, kuid varem või hiljem väljuvad nad kontrolli alt ja programmeerija ei tea, mida teha.

Kuid erinevalt võlurist väljalangevast ei saa õnnetu programmeerija loota võimsa nõia saabumist, kes viibutab võlukeppi ja taastab korra. Selle asemel teeb programmeerija kõige inetumaid trikke, püüdes toime tulla pidevalt esilekerkivate probleemidega. Tulemus on alati sama: saadakse liiga keeruline, piiratud, habras ja ebausaldusväärne rakendus. Sellel on pidev ummikseisu oht ja muud halvale mitmelõimelisele koodile omased ohud. Ma ei räägi isegi seletamatutest krahhidest, kehvast sooritusest, puudulikest või valedest töötulemustest.

Võib-olla olete mõelnud: miks see juhtub? Levinud eksiarvamus on: "Mitme lõimega programmeerimine on väga raske." Kuid see pole nii. Kui mitme lõimega programm on ebausaldusväärne, ebaõnnestub see tavaliselt samadel põhjustel kui madala kvaliteediga ühe lõimega programm. Lihtsalt programmeerija ei järgi põhilisi, tuntud ja end tõestanud arendusmeetodeid. Mitme lõimega programmid tunduvad olevat keerulisemad, sest mida rohkem paralleelseid lõime valesti läheb, seda rohkem segadust need tekitavad – ja palju kiiremini kui üks lõime teeks.

Väärarusaam "mitme lõimega programmeerimise keerukusest" on muutunud laialt levinud nende arendajate tõttu, kes on ühelõimelise koodi kirjutamises professionaalselt arenenud, puutusid esmakordselt kokku mitmelõimega ega tulnud sellega toime. Kuid selle asemel, et oma eelarvamusi ja tööharjumusi ümber mõelda, fikseerivad nad kangekaelselt tõsiasja, et nad ei taha kuidagi tööd teha. Vabandades ebausaldusväärset tarkvara ja möödalastud tähtaegu, kordavad need inimesed sama asja: "mitmelõimeline programmeerimine on väga raske."

Pange tähele, et eespool räägin tüüpilistest programmidest, mis kasutavad mitut lõime. Tõepoolest, on keerulisi mitme keermega stsenaariume – nagu ka keerukaid ühe keermega stsenaariume. Kuid need pole levinud. Reeglina ei nõuta programmeerijalt praktikas midagi üleloomulikku. Liigutame andmeid, teisendame neid, teeme aeg-ajalt mingeid arvutusi ja lõpuks salvestame info andmebaasi või kuvame selle ekraanile.

Keskmise ühelõimelise programmi täiustamises ja mitmelõimeliseks muutmises pole midagi rasket. Vähemalt ei tohiks olla. Raskused tekivad kahel põhjusel:

  • programmeerijad ei oska rakendada lihtsaid, tuntud ja tõestatud arendusmeetodeid;
  • enamik mitme lõimega programmeerimise raamatutes esitatud teabest on tehniliselt õiged, kuid rakendusprobleemide lahendamiseks täiesti sobimatud.

Kõige olulisemad programmeerimiskontseptsioonid on universaalsed. Need on võrdselt rakendatavad nii ühe- kui ka mitmelõimeliste programmide puhul. Voogude keerisesse uppuvad programmeerijad lihtsalt ei saanud olulisi õppetunde, kui õppisid ühelõimelist koodi. Ma võin seda öelda, sest sellised arendajad teevad samu põhimõttelisi vigu mitme lõimega ja ühe lõimega programmides.

Võib-olla on kuuekümneaastase programmeerimisajaloo kõige olulisem õppetund: globaalne muutuv olek- kurjast... Tõeline kuri. Programmid, mis põhinevad globaalselt muutuvale olekule, on suhteliselt raskesti põhjendatavad ja üldiselt ebausaldusväärsed, kuna oleku muutmiseks on liiga palju võimalusi. Seda üldpõhimõtet kinnitavaid uuringuid on tehtud palju, disainimustreid on lugematul hulgal, mille põhieesmärk on üht või teist andmete peitmise viisi rakendamine. Programmide prognoositavamaks muutmiseks proovige muutlikud olekud nii palju kui võimalik kõrvaldada.

Ühe keermestatud järjestikuse programmi puhul on andmete riknemise tõenäosus otseselt proportsionaalne komponentide arvuga, mis võivad andmeid muuta.

Üldjuhul ei ole võimalik täielikult vabaneda globaalsest seisundist, kuid arendaja arsenalis on väga tõhusad tööriistad, mis võimaldavad teil rangelt kontrollida, millised programmi komponendid võivad olekut muuta. Lisaks õppisime, kuidas luua primitiivsete andmestruktuuride ümber piiravaid API kihte. Seetõttu on meil hea kontroll nende andmestruktuuride muutumise üle.

Globaalselt muutuva oleku probleemid ilmnesid järk-järgult 80ndate lõpus ja 90ndate alguses koos sündmustepõhise programmeerimise levikuga. Programmid ei alanud enam "algusest" ega järginud ühte etteaimatavat täitmisteed "lõpuni". Kaasaegsetel programmidel on algseisund, millest väljumisel toimuvad neis sündmused - ettearvamatus järjekorras, muutuvate ajavahemike järel. Kood jääb ühelõimeliseks, kuid muutub juba asünkroonseks. Andmete riknemise tõenäosus suureneb just seetõttu, et sündmuste toimumise järjekord on väga oluline. Sellised olukorrad on üsna tavalised: kui sündmus B toimub pärast sündmust A, siis kõik toimib hästi. Kuid kui sündmus A toimub pärast sündmust B ja sündmusel C on aega nende vahele sekkuda, võivad andmed olla tundmatuseni moonutatud.

Kui tegemist on paralleelsete voogudega, süveneb probleem veelgi, kuna mitmed meetodid võivad samaaegselt toimida globaalses olekus. On võimatu hinnata, kuidas globaalne seisund täpselt muutub. Me räägime juba mitte ainult sellest, et sündmused võivad toimuda ettearvamatus järjekorras, vaid ka sellest, et mitme täitmise lõime olekut saab värskendada. samaaegselt... Asünkroonse programmeerimisega saate vähemalt tagada, et teatud sündmus ei saaks toimuda enne, kui mõni muu sündmus on töötlemise lõpetanud. See tähendab, et on võimalik kindlalt öelda, milline on globaalne seisund konkreetse sündmuse töötlemise lõpus. Mitme lõimega koodis on reeglina võimatu öelda, millised sündmused toimuvad paralleelselt, seega on võimatu kirjeldada kindlalt globaalset seisundit igal ajahetkel.

Ulatusliku globaalselt muutuva olekuga mitmelõimeline programm on üks kõnekamaid näiteid Heisenbergi määramatuse põhimõttest, mida ma tean. Programmi olekut on võimatu kontrollida ilma selle käitumist muutmata.

Kui alustan järjekordset filippi globaalse muutuva oleku kohta (olemus on välja toodud mitmes eelnevas lõigus), pööritavad programmeerijad silmi ja kinnitavad mulle, et nad on seda kõike juba ammu teadnud. Aga kui sa seda tead, siis miks sa ei saa seda oma koodi järgi kindlaks teha? Programmid on täis globaalset muutuvat olekut ja programmeerijad imestavad, miks kood ei tööta.

Pole üllatav, et kõige olulisem töö mitme lõimega programmeerimises toimub projekteerimisetapis. On vaja selgelt määratleda, mida programm peaks tegema, välja töötama sõltumatud moodulid kõigi funktsioonide täitmiseks, kirjeldama üksikasjalikult, milliseid andmeid millise mooduli jaoks on vaja, ja määrama moodulitevahelise teabe vahetamise viisid ( Jah, ärge unustage valmistada kõigile projektis osalejatele toredaid T-särke. Esimene asi.- u. toim. originaalis). See protsess ei erine põhimõtteliselt ühe lõimega programmi kujundamisest. Edu võti, nagu ka ühe lõimega koodi puhul, on piirata moodulite vahelist suhtlust. Kui saate jagatud muutuvast olekust lahti, siis andmete jagamise probleeme lihtsalt ei teki.

Keegi võib väita, et mõnikord pole programmi nii delikaatseks ülesehituseks aega, mis võimaldab ilma globaalse riigita hakkama saada. Usun, et sellele on võimalik ja vajalik aega kulutada. Miski ei mõjuta mitmelõimega programme nii hävitavalt kui katse tulla toime globaalse muutuva olekuga. Mida rohkem üksikasju peate haldama, seda tõenäolisemalt teie programm jõuab haripunkti ja jookseb kokku.

Realistlikes rakendustes peab olema teatud jagatud olek, mis võib muutuda. Ja see on koht, kus enamikul programmeerijatel tekivad probleemid. Programmeerija näeb, et siin on vaja jagatud olekut, pöördub mitme keermega arsenali poole ja võtab sealt kõige lihtsama tööriista: universaalse luku (kriitiline sektsioon, mutex või kuidas nad seda nimetavad). Nad näivad uskuvat, et vastastikune väljajätmine lahendab kõik andmete jagamise probleemid.

Probleemide hulk, mis sellise ühe lukuga tekkida võib, on jahmatav. Arvestada tuleb võistlustingimustega, liiga ulatusliku blokeerimisega seotud väravaprobleemidega ja jaotamise õiglusega on vaid mõned näited. Kui teil on mitu lukku, eriti kui need on pesastatud, peate võtma meetmeid ka ummikseisu, dünaamilise ummikseisu, järjekordade blokeerimise ja muude samaaegsusega seotud ohtude vastu. Lisaks on ühekordse blokeerimisega kaasnevad probleemid.
Kui ma koodi kirjutan või üle vaatan, on mul peaaegu eksimatu raudreegel: kui luku tegid, siis tundub, et oled kuskil vea teinud.

Seda väidet saab kommenteerida kahel viisil:

  1. Kui vajate lukustamist, on teil tõenäoliselt globaalne muutuv olek, mida soovite kaitsta samaaegsete värskenduste eest. Globaalse muutuva oleku olemasolu on viga rakenduse kavandamise etapis. Vaata üle ja kujunda ümber.
  2. Lukkude õige kasutamine ei ole lihtne ja lukustamisega seotud vigade lokaliseerimine võib olla uskumatult keeruline. On väga tõenäoline, et kasutate lukku valesti. Kui ma näen lukku ja programm käitub ebatavaliselt, siis kontrollin esimese asjana lukust sõltuvat koodi. Ja ma leian sellest tavaliselt probleeme.

Mõlemad tõlgendused on õiged.

Mitme lõimega koodi kirjutamine on lihtne. Kuid sünkroniseerimisprimitiive on väga-väga raske õigesti kasutada. Võib-olla ei ole te pädev isegi ühte lukku õigesti kasutama. Lukud ja muud sünkroniseerimisprimitiivid on ju konstruktsioonid, mis on püstitatud kogu süsteemi tasandil. Inimesed, kes mõistavad paralleelprogrammeerimist palju paremini kui teie, kasutavad neid primitiive samaaegsete andmestruktuuride ja kõrgetasemeliste sünkroonimiskonstruktsioonide loomiseks. Ja sina ja mina, tavalised programmeerijad, võtame lihtsalt sellised konstruktsioonid ja kasutame neid oma koodis. Rakenduse programmeerija ei tohiks kasutada madala tasemega sünkroonimisprimitiive sagedamini, kui ta teeb otsekõnesid seadme draiveritele. See tähendab, et peaaegu mitte kunagi.

Andmete jagamise probleemide lahendamiseks lukkude kasutamine on nagu vedela hapnikuga tulekahju kustutamine. Sarnaselt tulekahjuga on selliseid probleeme lihtsam ennetada kui parandada. Kui vabanete jagatud olekust, ei pea te ka sünkroonimisprimitiive kuritarvitama.

Suurem osa sellest, mida te mitmekeermestamise kohta teate, on ebaoluline

Algajatele mõeldud mitme lõimega töötamise õpetustest saate teada, mis lõimed on. Seejärel hakkab autor kaaluma erinevaid võimalusi, kuidas need lõimed saaksid paralleelselt töötada – näiteks rääkida jagatud andmetele juurdepääsu juhtimisest lukkude ja semaforide abil ning peatuda sellel, mis sündmustega töötamisel juhtuda võib. Vaatleb tähelepanelikult tingimuste muutujaid, mälutõkkeid, kriitilisi sektsioone, mutexe, lenduvaid välju ja aatomioperatsioone. Arutatakse näiteid selle kohta, kuidas neid madala tasemega konstruktsioone kasutada igasuguste süsteemioperatsioonide tegemiseks. Olles selle materjali pooleks lugenud, otsustab programmeerija, et ta teab kõigist nendest primitiividest ja nende kasutamisest juba piisavalt. Lõppude lõpuks, kui ma tean, kuidas see asi süsteemi tasemel töötab, saan ma seda samamoodi rakendada rakenduse tasemel. Jah?

Kujutage ette, et räägiksite teismelisele, kuidas ise sisepõlemismootorit kokku panna. Siis, ilma igasuguse sõidukoolituseta, paned ta autorooli ja ütled: "Sõida!" Teismeline saab aru, kuidas auto töötab, kuid tal pole õrna aimugi, kuidas sellega punktist A punkti B jõuda.

Lõimede süsteemitasemel toimimise mõistmine ei aita tavaliselt rakenduse tasemel kuidagi. Ma ei väida, et programmeerijad ei pea kõiki neid madala taseme üksikasju õppima. Ärge lihtsalt lootke, et saate neid teadmisi ärirakenduse kavandamisel või arendamisel kohe rakendada.

Sissejuhatav lõimekirjandus (ja sellega seotud akadeemilised kursused) ei tohiks selliseid madalatasemelisi konstruktsioone uurida. Peate keskenduma kõige levinumate probleemide klasside lahendamisele ja näitama arendajatele, kuidas neid probleeme kõrgetasemeliste võimaluste abil lahendatakse. Põhimõtteliselt on enamik ärirakendusi äärmiselt lihtsad programmid. Nad loevad andmeid ühest või mitmest sisendseadmest, töötlevad neid andmeid keerukalt (näiteks küsivad protsessi käigus veel andmeid) ja seejärel väljastavad tulemused.

Need programmid sobivad sageli suurepäraselt pakkuja-tarbija mudeliga, mis nõuab ainult kolme lõime:

  • sisendvoog loeb andmed ja paneb need sisendjärjekorda;
  • töötaja lõim loeb kirjeid sisendjärjekorrast, töötleb neid ja paneb tulemused väljundjärjekorda;
  • väljundvoog loeb väljundjärjekorra kirjed ja salvestab need.

Need kolm lõime töötavad iseseisvalt, suhtlus nende vahel toimub järjekorra tasemel.

Kui tehniliselt võib neid järjekordi pidada jagatud oleku tsoonideks, siis praktikas on need vaid sidekanalid, milles toimib nende enda sisemine sünkroniseerimine. Järjekorrad toetavad töötamist paljude tootjate ja tarbijatega korraga, neisse saab paralleelselt üksusi lisada ja eemaldada.

Kuna sisend-, töötlus- ja väljundetapid on üksteisest isoleeritud, saab nende rakendamist hõlpsasti muuta, ilma et see mõjutaks ülejäänud programmi. Kuni andmete tüüp järjekorras ei muutu, saate üksikuid programmikomponente oma äranägemise järgi ümber kujundada. Lisaks, kuna järjekorras osaleb suvaline arv tarnijaid ja tarbijaid, ei ole teiste tootjate/tarbijate lisamine keeruline. Meil võib olla kümneid sisendvooge, mis kirjutavad teavet samasse järjekorda, või kümneid töölõime, mis võtavad teavet sisendjärjekorrast ja seedivad andmeid. Ühe arvuti raames selline mudel skaleerub hästi.

Kõige tähtsam on see, et kaasaegsed programmeerimiskeeled ja teegid muudavad tootja-tarbija rakenduste loomise väga lihtsaks. NET-ist leiate paralleelkogud ja TPL-i andmevoo teegi. Java-l on teenus Executor, samuti BlockingQueue ja muud klassid java.util.concurrent nimeruumist. C ++-l on Boosti keermestusteek ja Inteli lõimede ehitusplokkide teek. Microsofti Visual Studio 2013 tutvustab asünkroonseid agente. Sarnased teegid on saadaval ka Python, JavaScript, Ruby, PHP ja minu teada paljudes teistes keeltes. Saate luua tootja-tarbija rakenduse mis tahes nendest pakettidest, ilma et peaksite kunagi kasutama lukke, semafoore, tingimusmuutujaid või muid sünkroonimisprimitiive.

Nendes teekides kasutatakse vabalt mitmesuguseid sünkroniseerimisprimitiive. See sobib. Kõik need teegid on kirjutatud inimeste poolt, kes mõistavad multithreadingut võrreldamatult paremini kui keskmine programmeerija. Sellise teegiga töötamine on praktiliselt sama, mis käituskeeleteegi kasutamine. Seda võib võrrelda programmeerimisega kõrgetasemelises keeles, mitte assemblerkeeles.

Tarnija-tarbija mudel on vaid üks näide paljudest. Ülaltoodud teegid sisaldavad klasse, mida saab kasutada paljude tavaliste keermestamise kujundusmustrite rakendamiseks ilma madala tasemega üksikasjadesse laskumata. Võimalik on luua suuremahulisi mitme lõimega rakendusi, muretsemata lõimede koordineerimise ja sünkroonimise pärast.

Töö raamatukogudega

Seega ei erine mitme lõimega programmide loomine põhimõtteliselt ühe lõimega sünkroonsete programmide kirjutamisest. Kapseldamise ja andmete peitmise olulised põhimõtted on universaalsed ja nende tähtsus kasvab ainult siis, kui kaasatud on mitu samaaegset lõime. Kui jätate need olulised aspektid tähelepanuta, ei päästa teid isegi kõige põhjalikumad teadmised madala taseme keermestamise kohta.

Kaasaegsed arendajad peavad lahendama palju probleeme rakenduste programmeerimise tasemel, juhtub, et lihtsalt pole aega mõelda, mis süsteemi tasandil toimub. Mida keerulisemaks rakendused lähevad, seda keerulisemad üksikasjad tuleb API tasemete vahele peita. Oleme seda teinud juba üle tosina aasta. Võib väita, et süsteemi keerukuse kvalitatiivne varjamine programmeerija eest on peamine põhjus, miks programmeerija suudab kirjutada kaasaegseid rakendusi. Kas me ei varja süsteemi keerukust kasutajaliidese sõnumiahela rakendamise, madala taseme sideprotokollide loomise jne abil?

Sarnane on olukord ka mitmelõimega. Enamik mitme lõimega stsenaariume, millega keskmine ärirakenduste programmeerija võib kokku puutuda, on juba hästi teada ja raamatukogudes hästi rakendatud. Raamatukogu funktsioonid teevad suurepärast tööd paralleelsuse valdava keerukuse varjamisel. Peate õppima, kuidas neid teeke kasutada samal viisil, nagu kasutate kasutajaliidese elementide, sideprotokollide ja paljude muude lihtsalt toimivate tööriistade teeke. Madala tasemega multithreading jätke spetsialistide - rakenduste loomisel kasutatud raamatukogude autorite - hooleks.

NS See artikkel pole mõeldud paadunud Pythoni taltsutajatele, kelle jaoks selle maokera lahtiharutamine on lapsemäng, vaid pigem pealiskaudne ülevaade äsja sõltuvusse sattunud pythoni mitmelõimelisuse võimalustest.

Kahjuks pole Pythonis nii palju venekeelset materjali mitmelõimestamise teemal ja kadestusväärse regulaarsusega hakkasid minu juurde sattuma pythonerid, kes polnud midagi kuulnud näiteks GIL-ist. Selles artiklis püüan kirjeldada mitmelõimelise pythoni kõige põhilisemaid funktsioone, öelda teile, mis on GIL ja kuidas sellega (või ilma selleta) elada ja palju muud.


Python on võluv programmeerimiskeel. See ühendab suurepäraselt palju programmeerimisparadigmasid. Enamik ülesandeid, millega programmeerija kokku saab, on siin lahendatud lihtsalt, elegantselt ja lühidalt. Kuid kõigi nende probleemide puhul piisab sageli ühe lõimega lahendusest ning ühe lõimega programmid on tavaliselt etteaimatavad ja neid on lihtne siluda. Sama ei saa öelda mitme lõimega ja mitme töötlusega programmide kohta.

Mitme lõimega rakendused


Pythonil on moodul keermestamine , ja sellel on kõik, mida vajate mitme keermega programmeerimiseks: on erinevat tüüpi lukke, semafori ja sündmuste mehhanismi. Ühesõnaga - kõik, mis on vajalik enamiku mitmelõimeliste programmide jaoks. Pealegi on kõigi nende tööriistade kasutamine üsna lihtne. Vaatleme näidet programmist, mis käivitab 2 lõime. Üks niit kirjutab kümme "0", teine ​​- kümme "1" ja rangelt omakorda.

impordi keermestamine

def kirjanik

minu jaoks xrange'is (10):

printida x

Event_for_set.set ()

# algussündmust

e1 = keermestamine. Sündmus ()

e2 = keermestamine. Sündmus ()

# init lõime

0, e1, e2))

1, e2, e1))

# alusta lõime

t1.start ()

t2.start ()

t1.liituma ()

t2.liituma ()


Ei mingit maagiat ega voodoo koodi. Kood on selge ja järjekindel. Lisaks, nagu näete, oleme funktsioonist loonud voo. See on väikeste ülesannete jaoks väga mugav. See kood on ka üsna paindlik. Oletame, et meil on kolmas protsess, mis kirjutab "2", siis näeb kood välja selline:

impordi keermestamine

def kirjanik (x, event_for_wait, event_for_set):

minu jaoks xrange'is (10):

Event_for_wait.wait () # oota sündmust

Event_for_wait.clear () # puhas sündmus tuleviku jaoks

printida x

Event_for_set.set () # määrake sündmus naabri lõimele

# algussündmust

e1 = keermestamine. Sündmus ()

e2 = keermestamine. Sündmus ()

e3 = keermestamine. Sündmus ()

# init lõime

t1 = keermestamine. Lõim (sihtmärk = kirjutaja, args = ( 0, e1, e2))

t2 = keermestamine. Lõim (sihtmärk = kirjutaja, args = ( 1, e2, e3))

t3 = keermestamine. Lõim (sihtmärk = kirjutaja, args = ( 2, e3, e1))

# alusta lõime

t1.start ()

t2.start ()

t3.start ()

e1.set () # algatab esimese sündmuse

# ühenda lõimed põhilõimega

t1.liituma ()

t2.liituma ()

t3.liituma ()


Lisasime uue sündmuse, uue lõime ja muutsime veidi parameetreid, millega
vood algavad (loomulikult saab kirjutada ka üldisema lahenduse, kasutades näiteks MapReduce'i, kuid see ei kuulu selle artikli ulatusse).
Nagu näete, pole ikka veel maagiat. Kõik on lihtne ja arusaadav. Lähme edasi.

Globaalne tõlgi lukk


Lõimede kasutamisel on kaks levinumat põhjust: esiteks, et suurendada tänapäevaste protsessorite mitmetuumalise arhitektuuri kasutamise efektiivsust ja seega ka programmi jõudlust;
teiseks, kui meil on vaja jagada programmi loogika paralleelseteks, täielikult või osaliselt asünkroonseteks osadeks (näiteks selleks, et saaksime pingida mitut serverit korraga).

Esimesel juhul seisame silmitsi Pythoni (või õigemini selle peamise CPythoni teostuse) piiranguga nagu Global Interpreter Lock (või lühidalt GIL). GIL-i põhimõte seisneb selles, et protsessor saab korraga täita ainult ühte lõime. Seda tehakse selleks, et lõimede vahel ei oleks võitlust eraldi muutujate pärast. Käivitatav lõim saab juurdepääsu kogu keskkonnale. See Pythoni lõimede rakendamise funktsioon lihtsustab oluliselt tööd lõimedega ja annab lõime teatud ohutuse.

Kuid on ka üks peen punkt: võib tunduda, et mitme lõimega rakendus töötab täpselt sama kaua kui ühe lõimega rakendus, mis teeb sama, või protsessori iga lõime täitmisaja summa. Kuid siin ootab meid üks ebameeldiv mõju. Mõelge programmile:

avatud ("test1.txt", "w") kui fout:

i jaoks xrange'is (1000000):

print >> fout, 1


See programm kirjutab lihtsalt faili miljon rida "1" ja teeb seda minu arvutis ~ 0,35 sekundiga.

Kaaluge teist programmi:

keermestamisest import Thread

def-kirjutaja (failinimi, n):

avatud (failinimi, "w") nagu fout:

i jaoks xvahemikus (n):

print >> fout, 1

t1 = Lõim (sihtmärk = kirjutaja, args = ("test2.txt", 500000))

t2 = Lõim (sihtmärk = kirjutaja, args = ("test3.txt", 500000))

t1.start ()

t2.start ()

t1.liituma ()

t2.liituma ()


See programm loob 2 lõime. Igas lõimes kirjutab see eraldi faili pool miljonit rida "1". Tegelikult on töö maht sama, mis eelmises programmis. Kuid aja jooksul saadakse siin huvitav efekt. Programm võib töötada 0,7 sekundist kuni 7 sekundini. Miks see juhtub?

See on tingitud asjaolust, et kui lõime ei vaja protsessori ressurssi, vabastab see GIL-i ja sel hetkel võib ta proovida seda hankida, teist lõime ja ka põhilõimi. Samal ajal võib operatsioonisüsteem, teades, et tuumasid on palju, kõike süvendada, püüdes tuumade vahel niite laiali jagada.

UPD: praegu on Python 3.2-s GIL-i täiustatud juurutamine, milles see probleem on osaliselt lahendatud, eelkõige seetõttu, et iga lõim pärast kontrolli kaotamist ootab lühikest aega enne see suudab uuesti GIL-i jäädvustada (inglise keeles on hea esitlus)

"Nii et te ei saa Pythonis tõhusaid mitmelõimega programme kirjutada?" Küsite. Ei, loomulikult on väljapääs ja isegi mitu.

Multitöötlusrakendused


Eelmises lõigus kirjeldatud probleemi mõnes mõttes lahendamiseks on Pythonil moodul alamprotsess ... Programmi, mida tahame käivitada, saame kirjutada paralleellõime (tegelikult juba protsessina). Ja käivitage see mõnes teises programmis ühes või mitmes lõimes. See kiirendaks meie programmi tõesti, sest GIL-i käivitajas loodud lõimed ei võta üles, vaid ootavad ainult jooksva protsessi lõppu. Sellel meetodil on aga palju probleeme. Peamine probleem seisneb selles, et andmete ülekandmine protsesside vahel muutub keeruliseks. Peaksite objektid kuidagi serialiseerima, PIPE-i või muude tööriistade kaudu suhtlema, kuid see kõik käib paratamatult üle ja koodist on raske aru saada.

Teine lähenemine võib meid siin aidata. Pythonil on multitöötlusmoodul ... Funktsionaalsuse poolest sarnaneb see moodul keermestamine ... Näiteks saab protsesse samamoodi luua tavafunktsioonidest. Protsessidega töötamise meetodid on peaaegu samad, mis keermestusmooduli lõimede puhul. Kuid protsesside sünkroonimiseks ja andmevahetuseks on tavaks kasutada muid tööriistu. Jutt käib järjekordadest (Queue) ja torudest (Pipe). Kuid siin on ka niidistamises olnud lukkude, sündmuste ja semaforide analoogid.

Lisaks on multitöötlusmoodulil mehhanism jagatud mäluga töötamiseks. Selleks on moodulis muutuja (Väärtus) ja massiivi (Array) klassid, mida saab protsesside vahel “jagada”. Jagatud muutujatega töötamise mugavuse huvides saate kasutada halduriklasse. Need on paindlikumad ja hõlpsamini kasutatavad, kuid aeglasemad. Tuleb märkida, et on tore võimalus teha levinud tüüpe ctypes moodulist kasutades moodulit multiprocessing.sharedctypes.

Ka multitöötlusmoodulis on mehhanism protsessikogumite loomiseks. Seda mehhanismi on väga mugav kasutada Master-Worker mustri rakendamiseks või paralleelkaardi rakendamiseks (mis teatud mõttes on Master-Workeri erijuhtum).

Mitmetöötlusmooduliga töötamise peamistest probleemidest väärib märkimist selle mooduli suhteline platvormsõltuvus. Kuna töö protsessidega on erinevates operatsioonisüsteemides erinevalt korraldatud, seatakse koodile teatud piirangud. Näiteks Windowsil pole kahvlimehhanismi, seega tuleb protsesside eralduspunkt mähkida:

if __name__ == "__main__":


See disain on aga juba hea vorm.

Mida veel...


Pythonis paralleelsete rakenduste kirjutamiseks on ka teisi teeke ja lähenemisviise. Näiteks saate kasutada Hadoop + Python või erinevaid Pythoni MPI rakendusi (pyMPI, mpi4py). Võite isegi kasutada olemasolevate C ++ või Fortrani teekide ümbriseid. Siin võiks mainida selliseid raamistikke/teeke nagu Pyro, Twisted, Tornado ja paljud teised. Kuid see kõik jääb juba sellest artiklist välja.

Kui teile minu stiil meeldis, proovin järgmises artiklis teile öelda, kuidas PLY-s lihtsaid tõlke kirjutada ja milleks neid kasutada saab.

10. peatükk.

Mitme lõimega rakendused

Kaasaegsetes operatsioonisüsteemides on multitegumtöö iseenesestmõistetav [ Enne Apple OS X tulekut polnud Macintoshi arvutites moodsaid multitegumtööga operatsioonisüsteeme. Täieliku multitegumtöötlusega operatsioonisüsteemi on väga raske korralikult kujundada, mistõttu OS X pidi põhinema Unixil.]. Kasutaja eeldab, et kui tekstiredaktor ja meiliklient käivitatakse samaaegselt, siis need programmid konflikti ei lähe ning e-kirjade saamisel toimetaja tööd ei lakka. Mitme programmi samaaegsel käivitamisel lülitub operatsioonisüsteem kiiresti programmide vahel ümber, varustades neid omakorda protsessoriga (kui arvutisse pole muidugi installitud mitu protsessorit). Tulemusena, illusioon mitme programmi samaaegne käivitamine, sest isegi parim masinakirjutaja (ja kiireim Interneti-ühendus) ei suuda kaasaegse protsessoriga sammu pidada.

Mitme lõimega töötamist võib teatud mõttes vaadelda kui multitegumtöötluse järgmist taset: selle asemel, et vahetada erinevate vahel programmid operatsioonisüsteem lülitub sama programmi erinevate osade vahel. Näiteks võimaldab mitme lõimega meiliklient uute kirjade lugemise või koostamise ajal uusi meilisõnumeid vastu võtta. Tänapäeval peavad paljud kasutajad ka mitme lõimega töötlemist iseenesestmõistetavaks.

VB-l pole kunagi olnud tavalist mitmelõimelise tuge. Tõsi, üks selle sortidest ilmus VB5-s - koostööpõhine voogedastusmudel(korteri keermestamine). Nagu varsti näete, pakub koostöömudel programmeerijale mõningaid mitmelõimestamise eeliseid, kuid ei kasuta seda täielikult ära. Varem või hiljem tuleb treeningmasinalt päris masinale üle minna ja VB .NET sai esimeseks VB versiooniks, mis toetab tasuta mitmelõimega mudelit.

Kuid mitme lõimega töötlemine ei kuulu nende funktsioonide hulka, mida programmeerimiskeeltes hõlpsasti rakendatakse ja programmeerijad hõlpsasti omandavad. Miks?

Sest mitme lõimega rakendustes võivad tekkida väga keerulised vead, mis tekivad ja kaovad ettearvamatult (ja selliseid vigu on kõige raskem siluda).

Aus hoiatus: mitme lõimega töötlemine on programmeerimise üks raskemaid valdkondi. Väikseimgi tähelepanematus toob kaasa tabamatute vigade ilmnemise, mille parandamine nõuab astronoomilisi summasid. Sel põhjusel sisaldab see peatükk palju halb näited - me kirjutasime need teadlikult nii, et näidata levinud vigu. See on kõige turvalisem lähenemine mitmelõimelise programmeerimise õppimisele: peaksite suutma märgata võimalikke probleeme, kui esmapilgul tundub, et kõik töötab hästi, ja teadma, kuidas neid lahendada. Kui soovite kasutada mitme lõimega programmeerimistehnikaid, ei saa te ilma selleta hakkama.

See peatükk loob tugeva aluse edasiseks iseseisvaks tööks, kuid me ei suuda kirjeldada mitmelõimelist programmeerimist kõigis nüanssides – ainult Threadingu nimeruumi klasside trükitud dokumentatsioon on üle 100 lehekülje. Kui soovite omandada mitme lõimega programmeerimist kõrgemal tasemel, vaadake spetsiaalseid raamatuid.

Kuid hoolimata sellest, kui ohtlik on mitmelõimeline programmeerimine, on see mõne probleemi professionaalseks lahendamiseks hädavajalik. Kui teie programmid ei kasuta vajaduse korral mitmelõimega töötlust, on kasutajad väga pettunud ja eelistavad mõnda muud toodet. Näiteks ainult populaarse e-posti programmi Eudora neljandas versioonis ilmusid mitmelõimelised võimalused, ilma milleta on võimatu ette kujutada ühtegi kaasaegset programmi e-postiga töötamiseks. Selleks ajaks, kui Eudora tutvustas mitmelõimelise toe, olid paljud kasutajad (sealhulgas üks selle raamatu autoritest) üle läinud teistele toodetele.

Lõpuks, .NET-is pole ühe lõimega programme lihtsalt olemas. Kõik.NET-programmid on mitme lõimega, kuna prügikorjaja töötab madala prioriteediga taustprotsessina. Nagu allpool näidatud, võib .NET-i tõsise graafilise programmeerimise korral õige lõimestamine aidata vältida graafilise liidese blokeerimist, kui programm täidab pikki toiminguid.

Tutvustame mitme lõimega töötamist

Iga programm töötab konkreetselt kontekst, kirjeldades koodi ja andmete jaotust mälus. Konteksti salvestamisel salvestatakse tegelikult programmivoo olek, mis võimaldab seda tulevikus taastada ja programmi täitmist jätkata.

Konteksti salvestamisega kaasneb aja- ja mälukulu. Operatsioonisüsteem jätab programmilõime oleku meelde ja annab juhtimise üle teisele lõimele. Kui programm soovib peatatud lõime täitmist jätkata, tuleb salvestatud kontekst taastada, mis võtab veelgi kauem aega. Seetõttu tuleks mitme lõimega töötlemist kasutada ainult siis, kui kasu katab kõik kulud. Mõned tüüpilised näited on loetletud allpool.

  • Programmi funktsionaalsus on selgelt ja loomulikult jagatud mitmeks heterogeenseks toiminguks, nagu näites e-kirjade vastuvõtmise ja uute sõnumite ettevalmistamisega.
  • Programm teeb pikki ja keerulisi arvutusi ning te ei soovi, et graafiline liides oleks arvutuste ajaks blokeeritud.
  • Programm töötab mitme protsessoriga arvutis, mille operatsioonisüsteem toetab mitme protsessori kasutamist (seni kuni aktiivsete lõimede arv ei ületa protsessorite arvu, on paralleelkäivitus praktiliselt vaba lõimede vahetamisega kaasnevatest kuludest).

Enne mitmelõimega programmide mehaanika juurde asumist tuleb välja tuua üks asjaolu, mis mitmelõimelise programmeerimise alal algajates sageli segadust tekitab.

Programmivoos käivitatakse protseduur, mitte objekt.

Raske on öelda, mida mõeldakse väljendiga "objekt teostab", kuid üks autoritest peab sageli mitmelõimelise programmeerimise seminare ja seda küsimust esitatakse sagedamini kui teisi. Võib-olla arvab keegi, et programmilõime töö algab klassi meetodi New väljakutsega, mille järel lõim töötleb kõiki vastavale objektile edastatud teateid. Sellised esitused absoluutselt on valed. Üks objekt võib sisaldada mitut lõime, mis täidavad erinevaid (ja mõnikord isegi samu) meetodeid, samas kui objekti sõnumeid edastatakse ja võetakse vastu mitme erineva lõime kaudu (muide, see on üks põhjusi, mis muudab mitme lõimega programmeerimise keeruliseks: programmi silumiseks peate välja selgitama, milline lõim konkreetsel hetkel seda või teist protseduuri teostab!).

Kuna lõimed luuakse objektide meetoditest, luuakse objekt ise tavaliselt enne lõime. Pärast objekti edukat loomist loob programm lõime, edastades sellele objekti meetodi aadressi ja alles pärast seda annab käsu alustada lõime täitmist. Protseduur, mille jaoks lõim loodi, nagu kõik protseduurid, saab luua uusi objekte, teha toiminguid olemasolevate objektidega ning kutsuda teisi protseduure ja funktsioone, mis kuuluvad selle ulatusse.

Klasside levinud meetodeid saab käivitada ka programmilõimedes. Sel juhul pidage meeles ka teist olulist asjaolu: niit lõpeb protseduurist, mille jaoks see loodi, väljumisega. Programmi voo tavaline lõpetamine pole võimalik enne, kui protseduur on lõpetatud.

Lõimed võivad lõppeda mitte ainult loomulikult, vaid ka ebanormaalselt. See ei ole üldiselt soovitatav. Lisateabe saamiseks vaadake jaotist Voogude lõpetamine ja katkestamine.

Põhilised .NET-i tööriistad, mis on seotud programmilõimede kasutamisega, on koondatud Threadingu nimeruumi. Seetõttu peaks enamik mitme lõimega programme algama järgmise reaga:

Impordib System.Treading

Nimeruumi importimine muudab programmi tippimise lihtsamaks ja võimaldab IntelliSense'i tehnoloogiat.

Voogude otsene seos protseduuridega viitab sellele, et sellel pildil delegaadid(vt 6. peatükk). Täpsemalt sisaldab Threading nimeruum ThreadStart delegaati, mida tavaliselt kasutatakse programmilõimede käivitamisel. Selle delegaadi kasutamise süntaks näeb välja järgmine:

Avaliku delegaadi alamlõime algus ()

ThreadStarti delegaadiga kutsutud koodil ei tohi olla parameetreid ega tagastusväärtust, nii et funktsioonide (mis tagastavad väärtuse) ja parameetritega protseduuride jaoks ei saa lõime luua. Voost teabe edastamiseks peate otsima ka alternatiivseid vahendeid, kuna käivitatud meetodid ei tagasta väärtusi ega saa kasutada viitega edastamist. Näiteks kui ThreadMethod on WilluseThread klassis, saab ThreadMethod teavet edastada, muutes klassi WillUseThread eksemplaride omadusi.

Rakenduste domeenid

.NET-lõimed töötavad niinimetatud rakendusdomeenides, mis on dokumentatsioonis määratletud kui "liivakast, milles rakendus töötab". Rakenduse domeeni võib pidada Win32 protsesside kergeks versiooniks; üks Win32 protsess võib sisaldada mitut rakenduse domeeni. Peamine erinevus rakenduse domeenide ja protsesside vahel seisneb selles, et Win32 protsessil on oma aadressiruum (dokumentatsioonis võrreldakse rakenduste domeene ka füüsilise protsessi sees töötavate loogiliste protsessidega). NET-is haldab kogu mäluhaldust käitusaeg, nii et mitu rakendusdomeeni saab töötada ühe Win32 protsessiga. Üks selle skeemi eeliseid on rakenduste täiustatud skaleerimisvõimalused. Rakenduste domeenidega töötamise tööriistad on AppDomain klassis. Soovitame tutvuda selle klassi dokumentatsiooniga. Selle abiga saate teavet keskkonna kohta, milles teie programm töötab. Eelkõige kasutatakse klassi AppDomain .NET-i süsteemiklasside kajastamisel. Järgmine programm loetleb laaditud koostud.

Impordib System.Reflection

Moodul Moodul

Peamine alam ()

Dim theDomain kui AppDomain

theDomain = AppDomain.CurrentDomain

Hämarad koostud () As

Assemblies = theDomain.GetAssemblies

Dim anAssemblyxAs

Iga assamblee jaoks koostutes

Console.WriteLinetanAssembly.Full Name) Järgmine

Console.ReadLine ()

Lõpeta alam

Lõppmoodul

Voogude loomine

Alustame algelise näitega. Oletame, et soovite käivitada protseduuri eraldi lõimes, mis vähendab loenduri väärtust lõpmatus tsüklis. Protseduur on määratletud klassi osana:

Avalik klass WillUseThreads

Avalik lahutada loendurist ()

Hämar arv täisarvuna

Kas True count - = 1

Console.WriteLlne ("Olen teises lõimes ja loendur ="

& loe)

Loop

Lõpeta alam

Lõppklass

Kuna Do tsükli tingimus on alati tõene, võite arvata, et miski ei sega SubtractFromCounter protseduuri. Kuid mitme lõimega rakenduses pole see alati nii.

Järgmine väljavõte näitab Sub Main protseduuri, mis käivitab lõime, ja käsku Import:

Valik Strict On Imports System.Threading Module Modulel

Peamine alam ()

1 Dim myTest uute WillUseThreadsina ()

2 Dim bThreadStart kui uus ThreadStart (AddressOf_

myTest.SubtractFromCounter)

3 Dim bThread uue lõimena (bThreadStart)

4 "bThread.Start ()

Dim i täisarvuna

5 Tehke tõese ajal

Console.WriteLine ("Põhilõimes ja arv on" & i) i + = 1

Loop

Lõpeta alam

Lõppmoodul

Vaatame järjestikku kõige olulisemaid punkte. Esiteks töötab alati Sub Man n protseduur peavool(peamine niit). .NET-programmides töötab alati vähemalt kaks lõime: põhilõim ja prügikoristuslõim. 1. rida loob testklassi uue eksemplari. 2. real loome ThreadStart delegaadi ja edastame protseduuri SubtractFromCounter aadressi real 1 loodud testklassi eksemplarile (seda protseduuri kutsutakse välja ilma parameetriteta). HeaThreading nimeruumi importimisel saab pika nime ära jätta. Uus lõimeobjekt luuakse real 3. Märka ThreadStart delegaadi möödumist Thread klassi konstruktori kutsumisel. Mõned programmeerijad eelistavad ühendada need kaks rida üheks loogiliseks reaks:

Dim bThread uue lõimena (New ThreadStarttAddressOf _

myTest.SubtractFromCounter))

Lõpuks "käivitab" rida 4 lõime, kutsudes välja ThreadStart delegaadi jaoks loodud lõime eksemplari Start-meetodi. Seda meetodit kutsudes ütleme operatsioonisüsteemile, et lahutamise protseduur peaks töötama eraldi lõimes.

Sõna "algatab" eelmises lõigus on jutumärkides, sest see on üks paljudest mitmelõimelise programmeerimise veidrustest: Starti kutsumine ei käivita tegelikult lõime! See lihtsalt käsib operatsioonisüsteemil ajastada määratud lõime käivitumist, kuid programmi otsekäivitamine ei saa seda kontrollida. Te ei saa ise lõime täitma hakata, sest lõimede täitmist kontrollib alati operatsioonisüsteem. Hilisemas jaotises saate teada, kuidas kasutada prioriteeti, et operatsioonisüsteem teie lõime kiiremini käivitaks.

Joonisel fig. 10.1 näitab näidet, mis võib juhtuda pärast programmi käivitamist ja seejärel selle katkestamist klahvikombinatsiooni Ctrl + Break abil. Meie puhul algas uus lõime alles pärast seda, kui põhilõime loendur tõusis 341-ni!

Riis. 10.1. Lihtne mitme lõimega tarkvara käitusaeg

Kui programm töötab pikema aja jooksul, näeb tulemus välja umbes selline, nagu on näidatud joonisel fig. 10.2. Me näeme, et sinajooksva keerme valmimine peatatakse ja juhtimine läheb uuesti põhilõngale. Sel juhul on manifestatsioon ennetav mitmelõimeline läbi aja lõikamise. Selle hirmuäratava termini tähendust selgitatakse allpool.

Riis. 10.2. Lõimede vahel vahetamine lihtsas mitmelõimega programmis

Lõimede katkestamisel ja juhtimise üleandmisel teistele lõimedele kasutab operatsioonisüsteem ennetava mitmelõime põhimõtet läbi aja lõikamise. Ajakvantimine lahendab ka ühe levinud probleemi, mis mitmelõimega programmides varem tekkis – üks lõim võtab kogu CPU aja ja ei jää alla teiste lõimede juhtimisele (reeglina juhtub see intensiivsetes tsüklites nagu ülaltoodud). Eksklusiivse CPU kaaperdamise vältimiseks peaksid teie lõimed aeg-ajalt juhtimise teistele lõimedele üle andma. Kui programm osutub "teadvustamatuks", on veel üks, veidi vähem soovitav lahendus: operatsioonisüsteem ennetab alati töötavat lõime, olenemata selle prioriteetsuse tasemest, nii et protsessorile pääseb igale süsteemi lõimele.

Kuna kõigi .NET-i kasutavate Windowsi versioonide kvantimisskeemidel on igale lõimele eraldatud minimaalne ajalõik, ei ole .NET-i programmeerimises CPU eksklusiivsete haarangutega seotud probleemid nii tõsised. Teisest küljest, kui .NET-i raamistikku muude süsteemide jaoks kohandatakse, võib see muutuda.

Kui lisame enne Starti kutsumist oma programmi järgmise rea, saavad isegi madalaima prioriteediga lõimed osa CPU ajast:

bThread.Priority = ThreadPriority.Highest

Riis. 10.3. Kõrgeima prioriteediga lõim käivitub tavaliselt kiiremini

Riis. 10.4. Protsessor on ette nähtud ka madalama prioriteediga lõimede jaoks

Käsk määrab uuele lõimele maksimaalse prioriteedi ja vähendab põhilõime prioriteeti. Joonis fig. 10.3 on näha, et uus niit hakkab tööle kiiremini kui varem, kuid nagu joonisel fig. 10.4, saab ka põhilõime juhtimiselaiskus (ehkki väga lühikest aega ja alles pärast pikaajalist voolu tööd lahutamisega). Kui käivitate programmi oma arvutis, saate samasuguseid tulemusi nagu on näidatud joonisel fig. 10.3 ja 10.4, kuid meie süsteemide erinevuste tõttu täpset vastet ei tule.

ThreadProllority loenditüüp sisaldab väärtusi viie prioriteeditaseme jaoks:

ThreadPriority.Highest

ThreadPriority.BoveNormal

ThreadProlority.Normal

ThreadPriority.BelowNormal

ThreadPriority.Lowest

Liitumise meetod

Mõnikord tuleb mõni programmilõim peatada, kuni mõni teine ​​lõim lõpeb. Oletame, et soovite 1. lõime peatada, kuni lõime 2 arvutuse lõpetab. Selle jaoks voost 1 voo 2 jaoks kutsutakse liitumismeetodit. Teisisõnu, käsk

lõim2.Liitu ()

peatab praeguse lõime ja ootab lõime 2 lõpetamist. Lõim 1 läheb lukustatud olek.

Kui ühendate voo 1 vooga 2, kasutades liitumismeetodit, käivitab operatsioonisüsteem pärast voogu 2 automaatselt voogu 1. Pange tähele, et käivitusprotsess on mittedeterministlik: on võimatu täpselt öelda, kui kaua pärast lõime 2 lõppu hakkab tööle lõim 1. On veel üks Join versioon, mis tagastab tõeväärtuse:

lõim2. Liitu (täisarv)

See meetod kas ootab 2. lõime lõpetamist või deblokeerib lõime 1 blokeeringu pärast määratud ajaintervalli möödumist, mistõttu operatsioonisüsteemi planeerija eraldab lõimele uuesti CPU aega. Meetod tagastab väärtuse Tõene, kui lõim 2 lõpeb enne määratud ajalõpuintervalli aegumist, ja Väär muul juhul.

Pidage meeles põhireeglit: kas 2. lõim on lõppenud või aegunud, ei saa te kontrollida, millal lõim 1 aktiveeritakse.

Lõimede nimed, CurrentThread ja ThreadState

Atribuut Thread.CurrentThread tagastab viite parajasti käivitatavale lõimeobjektile.

Kuigi VB .NET-is on olemas suurepärane lõimeaken mitme lõimega rakenduste silumiseks, mida kirjeldatakse allpool, aitas meid sageli käsk

MsgBox (Thread.CurrentThread.Name)

Sageli selgus, et kood käivitati täiesti erinevas lõimes, millest see pidi käivitama.

Tuletame meelde, et mõiste "programmivoogude mittedeterministlik planeerimine" tähendab väga lihtsat asja: programmeerijal pole praktiliselt mingeid vahendeid planeerija töö mõjutamiseks. Sel põhjusel kasutavad programmid sageli atribuuti ThreadState, mis tagastab teabe lõime hetkeoleku kohta.

Voogude aken

Visual Studio .NET'i lõimede aken on mitme lõimega programmide silumisel hindamatu väärtusega. Selle aktiveerib katkestusrežiimis alammenüü Silumine> Windows käsk. Oletame, et määrasite bThreadi lõimele nime järgmise käsuga:

bThread.Name = "Lõime lahutamine"

Voogude akna ligikaudne vaade pärast programmi katkestamist klahvikombinatsiooniga Ctrl + Break (või muul viisil) on näidatud joonisel fig. 10.5.

Riis. 10.5. Voogude aken

Esimeses veerus olev nool tähistab atribuudi Thread.CurrentThread poolt tagastatud aktiivset lõime. ID-veerg sisaldab lõime numbrilisi ID-sid. Järgmises veerus on loetletud voogude nimed (kui need on määratud). Veerg Location näitab käivitatavat protseduuri (näiteks joonisel 10.5 klassi Console protseduur WriteLine). Ülejäänud veerud sisaldavad teavet prioriteetsete ja peatatud lõimede kohta (vt järgmist jaotist).

Lõime aken (mitte operatsioonisüsteem!) Võimaldab teil juhtida oma programmi lõime kontekstimenüüde abil. Näiteks saab aktiivse lõime peatada, tehes vastaval real paremklõpsu ja valides käsu Freeze (hiljem saab peatatud lõime jätkata). Lõimede peatamist kasutatakse sageli silumisel, et vältida rikkis lõime rakendust segamast. Lisaks võimaldab voogude aken aktiveerida teise (mitte peatatud) voo; selleks paremklõpsake vajalikul real ja valige kontekstimenüüst käsk Switch To Thread (või lihtsalt topeltklõps lõime real). Nagu allpool näidatud, on see võimalike ummikseisude diagnoosimisel väga kasulik.

Voo peatamine

Ajutiselt kasutamata voogusid saab Sleer meetodi abil passiivsesse olekusse viia. Blokeerituks loetakse ka passiivne voog. Muidugi, kui lõime passiivsesse olekusse pannakse, on ülejäänud lõimedel rohkem protsessori ressursse. Sleer meetodi standardne süntaks on järgmine: Thread.Sleep (intervall_in_milliconds)

Unerežiimi kõne tulemusena muutub aktiivne lõim passiivseks vähemalt teatud arvu millisekunditeks (samas pole garanteeritud aktiveerimine kohe pärast määratud intervalli möödumist). Pange tähele: meetodi kutsumisel ei edastata viidet konkreetsele lõimele - unerežiimi meetodit kutsutakse ainult aktiivse lõime jaoks.

Teine unerežiimi versioon paneb praeguse lõime ülejäänud eraldatud protsessori ajast loobuma:

Lõim.Unerežiim (0)

Järgmine valik paneb praeguse lõime piiramatuks ajaks passiivsesse olekusse (aktiveerimine toimub ainult siis, kui helistate käsule Katkestus):

Thread.Sleer (Timeout.Infinite)

Kuna passiivseid lõime (isegi piiramatu ajalõpuga) saab katkestada katkestamismeetodiga, mis viib erandi korral ThreadlnterruptExcepti käivitamiseni, on Slayeri kutse alati Try-Catch plokis, nagu järgmises lõigus:

Proovi

Thread.Sleep (200)

"Lõime passiivne olek on katkenud

Catch e As Exception

"Muud erandid

Lõpeta proovi

Iga .NET-programm töötab programmilõime peal, seega kasutatakse programmide peatamiseks ka Sleep-meetodit (kui Threadipg nimeruumi programm ei impordi, tuleb kasutada täiskvalifitseeritud nime Threading.Thread. Sleep).

Programmi lõimede lõpetamine või katkestamine

Lõim lõpetatakse automaatselt, kui ThreadStart delegaadi loomisel määratud meetod, kuid mõnikord peab meetod (ja seega lõim) teatud tegurite ilmnemisel lõpetama. Sellistel juhtudel kontrollivad vood tavaliselt tingimuslik muutuja, olenevalt millisest olekusttehakse otsus ojast varuväljapääsu kohta. Tavaliselt kaasatakse selle protseduuri juurde tsükkel Do-While:

Alamlõime meetod ()

«Programm peab andma vahendid küsitluseks

"tingimuslik muutuja.

"Näiteks tingimusliku muutuja saab stiilida atribuudina

Do While tingimusmuutuja = False and MoreWorkToDo

"Põhikood

Loop End Sub

Tingimusliku muutuja küsitlemiseks kulub veidi aega. Kui ootate lõime enneaegset lõpetamist, peaksite kasutama püsivat pollimist ainult tsükli tingimustes.

Kui tingimusmuutujat tuleb kontrollida kindlas kohas, kasutage käsku If-Then koos käsuga Exit Sub lõpmatus tsüklis.

Juurdepääs tingimuslikule muutujale peab olema sünkroonitud, et teiste lõimede eksponeerimine ei segaks selle tavapärast kasutamist. Seda olulist teemat käsitletakse jaotises "Tõrkeotsing: sünkroonimine".

Kahjuks passiivsete (või muul viisil blokeeritud) lõimede koodi ei käivitata, mistõttu tingimusliku muutuja pollimise võimalus neile ei sobi. Sel juhul kutsuge välja katkestusmeetod objektimuutujal, mis sisaldab viidet soovitud lõimele.

Katkestusmeetodit saab kutsuda ainult oote-, puhke- või liitumisolekus olevatel lõimedel. Kui kutsute katkestusse lõime jaoks, mis on ühes loetletud olekus, hakkab lõim mõne aja pärast uuesti tööle ja täitmiskeskkond algatab lõimes erandil ThreadlnterruptedExcepti. See juhtub isegi siis, kui lõim on määratud määramata ajaks passiivseks, kutsudes välja Thread.Sleepdimeout. Lõpmatu). Me ütleme "mõne aja pärast", sest lõime ajastamine on mittedeterministlik. Erandi ThreadlnterruptedExcepti püüab kinni jaotis Catch, mis sisaldab ooteolekust väljumiskoodi. Sektsioon Catch ei pea aga lõime katkestama katkestamiskõne korral – lõim käsitleb erandit oma äranägemise järgi.

NET-is saab katkestamismeetodit kutsuda isegi blokeerimata lõimede jaoks. Sel juhul katkeb niit lähimas blokeeringus.

Niitide peatamine ja tapmine

Keermestamise nimeruum sisaldab muid meetodeid, mis katkestavad tavalise lõimestamise:

  • Riputama;
  • Katkesta.

Raske on öelda, miks .NET sisaldas nende meetodite tuge – Suspend and Abort kutsumine põhjustab tõenäoliselt programmi ebastabiilsuse. Ükski meetoditest ei võimalda voo normaalset initsialiseerimist. Lisaks ei saa te peatamis- või katkestamiskõne korral ennustada, millisesse olekusse lõim objektid pärast peatamist või katkestamist jätab.

Aborti helistamine tekitab ThreadAbortExceptioni. Et aidata teil mõista, miks seda kummalist erandit programmides käsitleda ei tohiks, on siin väljavõte .NET SDK dokumentatsioonist:

“... Kui lõime Aborti kutsumisega hävitatakse, viskab käitusaeg ThreadAbortExceptioni. See on eriline erand, mida programm ei saa tabada. Kui see erand tehakse, käivitab käitusaeg enne lõime lõpetamist kõik Lõpuks plokid. Kuna lõpuks plokkides võib toimuda mis tahes toiming, helistage Liitu, et tagada voo hävitamine.

Moraal: katkestamine ja peatamine ei ole soovitatavad (ja kui te ikka ei saa ilma peatamiseta hakkama, jätkake peatatud lõime jätkamise meetodil). Lõime saate ohutult lõpetada ainult sünkroonitud tingimusmuutuja küsitlemise või ülalkirjeldatud katkestusmeetodi kutsumisega.

Taustalõime (deemonid)

Mõned taustal töötavad lõimed lõpetavad automaatselt töötamise, kui teised programmikomponendid peatuvad. Eelkõige töötab prügikoguja ühes taustalõimes. Taustalõime luuakse tavaliselt andmete vastuvõtmiseks, kuid seda tehakse ainult siis, kui teised lõimed töötavad koodi, mis saab vastuvõetud andmeid töödelda. Süntaks: voo nimi. IsBackGround = tõene

Kui rakenduses on järel ainult taustalõime, suletakse rakendus automaatselt.

Tõsisem näide: HTML-koodist andmete eraldamine

Soovitame kasutada vooge ainult siis, kui programmi funktsionaalsus on selgelt jagatud mitmeks toiminguks. Hea näide on 9. peatükist pärit HTML-i ekstraheerimisprogramm. Meie klass teeb kahte asja: hangib Amazonist andmeid ja töötleb neid. See on suurepärane näide olukorrast, kus mitmelõimeline programmeerimine on tõeliselt sobiv. Loome klassid mitme erineva raamatu jaoks ja seejärel analüüsime andmeid erinevates voogudes. Iga raamatu jaoks uue lõime loomine suurendab programmi efektiivsust, sest samal ajal kui üks lõim võtab vastu andmeid (mis võib nõuda Amazoni serveris ootamist), tegeleb teine ​​lõime juba vastuvõetud andmete töötlemisega.

Selle programmi mitme lõimega versioon töötab tõhusamalt kui ühe lõimega versioon ainult mitme protsessoriga arvutis või kui täiendavate andmete vastuvõtmist saab tõhusalt kombineerida nende analüüsiga.

Nagu eespool mainitud, saab lõimedes käivitada ainult parameetriteta protseduure, seega peate programmis tegema väikesed muudatused. Allpool on põhiprotseduur, mis on ümber kirjutatud parameetrite välistamiseks:

Avalik Sub FindRank ()

m_Rank = ScrapeAmazon ()

Console.WriteLine ("the rank of" & m_Name & "Is" & GetRank)

Lõpeta alam

Kuna me ei saa kasutada kombineeritud välja teabe salvestamiseks ja hankimiseks (mitme lõimega programmide kirjutamisest graafilise liidesega on juttu selle peatüki viimases osas), salvestab programm nelja raamatu andmed massiivi, mille määratlus algab järgmiselt:

Dim theBook (3.1) as String theBook (0.0) = "1893115992"

theBook (0.l) = "VB .NET programmeerimine" "Jne

Neli voogu luuakse samas tsüklis, milles luuakse AmazonRankeri objektid:

Kui i = 0 kuni 3

Proovi

theRanker = uus AmazonRanker (theBook (i.0). theBookd.1))

aThreadStart = Uus ThreadStar (AddressOf theRanker.FindRan ()

aThread = uus lõim (aThreadStart)

aThread.Name = raamat (i.l)

aThread.Start () Catch e As Exception

Console.WriteLine (e.Message)

Lõpeta proovi

Edasi

Allpool on programmi täielik tekst:

Valik Strict On Imports System.IO Impordib System.Net

Impordib System.Treading

Moodul Moodul

Peamine alam ()

Dim theBook (3.1) Stringina

theBook (0.0) = "1893115992"

theBook (0.l) = "VB .NET programmeerimine"

theBook (l.0) = "1893115291"

theBook (l.l) = "Andmebaasi programmeerimine VB .NET"

theBook (2,0) = "1893115623"

theBook (2.1) = "Programmeerija" sissejuhatus C #-sse. "

theBook (3.0) = "1893115593"

theBook (3.1) = "Tühjendage .Neti platvorm"

Dim i täisarvuna

Dim theRanker As = AmazonRanker

Dim aThreadStart kui Threading.ThreadStart

Dim aThread As Threading.Thread

Kui i = 0 kuni 3

Proovi

theRanker = uus AmazonRankerttheBook (i.0). raamat (i.1))

aThreadStart = Uus lõime algus (AddressOf theRanker. FindRank)

aThread = uus lõim (aThreadStart)

aThread.Name = raamat (i.l)

aThread.Start ()

Catch e As Exception

Console.WriteLlnete.Message)

Lõpeta Proovige järgmist

Console.ReadLine ()

Lõpeta alam

Lõppmoodul

Avalik klass AmazonRanker

Privaatne m_URL stringina

Privaatne m_Rank täisarvuna

Privaatne m_Name stringina

Avalik alam uus (ByVal ISBN stringina. ByVal theName stringina)

m_URL = "http://www.amazon.com/exec/obidos/ASIN/" ja ISBN

m_Name = theName End Sub

Avalik Sub FindRank () m_Rank = ScrapeAmazon ()

Console.Writeline ("auaste" & m_Name & "on"

& GetRank) End Sub

Public Readonly Property GetRank () Nagu string Get

Kui m_Rank<>0 Siis

Taga CStr (m_Rank) Muu

"Probleemid

Lõpeta Kui

End Get

Lõppomadus

Avalik kirjutuskaitstud atribuut GetName () Nagu string Get

Tagasta m_Name

End Get

Lõppomadus

Privaatne funktsioon ScrapeAmazon () Täisarvuna Proovi

Muutke URL uueks uriks (m_URL)

Hämardage päring kui WebRequest

theRequest = WebRequest.Create (theURL)

Hämardage Response kui WebResponse

theResponse = theRequest.GetResponse

Hämarda aReader uue StreamReaderina (theResponse.GetResponseStream ())

Dim the Data As String

theData = aReader.ReadToEnd

Tagastuse analüüs (andmed)

Catch E As Exception

Console.WriteLine (E.Message)

Console.WriteLine (E.StackTrace)

konsool. ReadLine ()

Lõpeta Proovi Lõpeta funktsioon

Privaatfunktsiooni analüüs (ByVal theData As String) täisarvuna

Dim Location As.Integer Location = theData.IndexOf (" Amazon.com

Müügiaste:") _

+ "Amazon.com müügiasetus:".Pikkus

Dim temp As String

Tehke kuni theData.Substring (Location.l) = "<" temp = temp

& theData.Substring (Location.l) Asukoht + = 1 Silmus

Return Clnt (temp)

Lõpetamisfunktsioon

Lõppklass

Mitme lõimega operatsioone kasutatakse tavaliselt .NET-i ja I/O nimeruumides, seega pakub .NET Frameworki teek nende jaoks spetsiaalseid asünkroonseid meetodeid. Lisateavet asünkroonsete meetodite kasutamise kohta mitme lõimega programmide kirjutamisel leiate klassi HTTPWebRequest meetoditest BeginGetResponse ja EndGetResponse.

Peamine oht (üldandmed)

Seni on peetud niitide jaoks ainsaks ohutuks kasutusvõimaluseks - meie vood ei muutnud üldandmeid. Kui lubate üldiste andmete muutmist, hakkavad potentsiaalsed vead hüppeliselt paljunema ja nendest vabanemine muutub programmi jaoks palju keerulisemaks. Teisest küljest, kui keelate jagatud andmete muutmise erinevate lõimede abil, ei erine mitme lõimega .NET-i programmeerimine VB6 piiratud võimalustest.

Pakume teile väikest programmi, mis demonstreerib tekkivaid probleeme ilma tarbetutesse detailidesse laskumata. See programm simuleerib maja, mille igas toas on termostaat. Kui temperatuur on 5 kraadi Fahrenheiti või rohkem (umbes 2,77 kraadi Celsiuse järgi) madalam kui sihtmärk, anname küttesüsteemile korralduse tõsta temperatuuri 5 kraadi võrra; muidu tõuseb temperatuur vaid 1 kraadi võrra. Kui praegune temperatuur on seatud temperatuurist suurem või sellega võrdne, siis muudatust ei tehta. Temperatuuri reguleerimine igas ruumis toimub eraldi vooluga 200-millisekundilise viivitusega. Põhitöö tehakse ära järgmise jupiga:

Kui mHouse.HouseTemp< mHouse.MAX_TEMP = 5 Then Try

Thread.Sleep (200)

Püüdke lips As ThreadlnterruptedException

«Passiivne ootamine on katkenud

Catch e As Exception

"Muud otsad, proovige erandeid

mHouse.HouseTemp + - 5 "Jne

Allpool on programmi täielik lähtekood. Tulemus on näidatud joonisel fig. 10.6: Maja temperatuur on jõudnud 105 kraadi Fahrenheiti (40,5 kraadi Celsiuse järgi)!

1 Valik Rangelt sees

2 Impordisüsteem. Keermestamine

3 mooduli moodul

4 alampeamist ()

5 Dim myHouse as New House (l0)

6 Konsool. ReadLine ()

7 Lõpeta alam

8 Lõppmoodul

9 Rahvaklassi maja

10 Public Const MAX_TEMP täisarvuna = 75

11 Privaatne mCurTemp täisarvuna = 55

12 privaatset mtuba () Toana

13 avalikku ala uut (ByVal numOfRooms täisarvuna)

14 Redim mRoomsit (NumOfRooms = 1)

15 Dim i täisarvuna

16 Dim aThreadStart kui Threading.ThreadStart

17 Dim aThread As Thread

18 Kui i = 0 To numOfRooms -1

19 Proovi

20 mRooms (i) = NewRoom (mina, mCurTemp, CStr (i) ja "throom")

21 aThreadStart – uus lõime algus (AddressOf_

mRooms (i) .CheckTempInRoom)

22 aThread = uus lõim (aThreadStart)

23 aThread.Start ()

24 Erandina püüda

25 Console.WriteLine (E.StackTrace)

26 Lõpeta proovimine

27 Järgmine

28 End Sub

29 Public Property HouseTemp () Täisarvuna

kolmkümmend . Hangi

31 Tagasi mCurTemp

32 End Get

33 Määra (ByVal väärtus täisarvuna)

34 mCurTemp = väärtus 35 lõppkomplekt

36 Lõppomand

37 Lõppklass

38 Avalik klassiruum

39 Privaatne mCurTemp täisarvuna

40 Privaatne mName stringina

41 Private mHouse as House

42 Public Sub New (ByVal theHouse as House,

ByVal temp täisarvuna, ByVal roomName stringina)

43 mMaja = maja

44 mCurTemp = temp

45 mName = ruumiNimi

46 End Sub

47 Public Sub CheckTempInRoom ()

48 Muuda temperatuuri ()

49 End Sub

50 privaatne alammuutustemperatuur ()

51 Proovige

52 Kui mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

53 Thread.Sleep (200)

54 mMaja.MajaTemp + - 5

55 Console.WriteLine ("Am in" & Me.mName & _

56 ".Praegune temperatuur on" & mHouse.HouseTemp)

57. Elself mHouse.HouseTemp< mHouse.MAX_TEMP Then

58 Thread.Sleep (200)

59 mHouse.HouseTemp + = 1

60 Console.WriteLine ("Am in" & Me.mName & _

61 ".Praegune temperatuur on" & mHouse.HouseTemp)

62 muu

63 Console.WriteLine ("Am in" & Me.mName & _

64 ".Praegune temperatuur on" & mHouse.HouseTemp)

65 "Ära tehke midagi, temperatuur on normaalne

66 Lõpp Kui

67 Catch tae As ThreadlnterruptedException

68 "Passiivne ootamine on katkenud

69 Catch e As Exception

70 "Muud erandid

71 Lõpeta proovimine

72 End Sub

73 Lõppklass

Riis. 10.6. Mitme lõimega seotud probleemid

Sub Main protseduur (read 4-7) loob kümne "toaga" "maja". Maja klass seab maksimaalseks temperatuuriks 75 kraadi Fahrenheiti (umbes 24 kraadi Celsiuse järgi). Read 13-28 määratlevad üsna keeruka majaehitaja. Read 18–27 on programmi mõistmiseks võtmetähtsusega. Rida 20 loob teise ruumiobjekti ning viide majaobjektile edastatakse konstruktorile, et ruumiobjekt saaks vajadusel sellele viidata. Read 21–23 alustavad kümmet voogu, et reguleerida temperatuuri igas ruumis. Ruumiklass on määratletud ridadel 38-73. Maja coxpa viideon salvestatud ruumiklassi konstruktoris mHouse muutujasse (rida 43). Temperatuuri kontrollimise ja reguleerimise kood (read 50-66) tundub lihtne ja loomulik, kuid nagu varsti näete, on see mulje petlik! Pange tähele, et see kood on mähitud Try-Catch plokki, kuna programm kasutab unerežiimi.

Vaevalt oleks keegi nõus elama temperatuuril 105 kraadi Fahrenheiti (40,5–24 kraadi Celsiuse järgi). Mis juhtus? Probleem on seotud järgmise reaga:

Kui mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

Ja juhtub järgmine: kõigepealt kontrollitakse temperatuuri voolu 1 abil. Ta näeb, et temperatuur on liiga madal, ja tõstab seda 5 kraadi võrra. Kahjuks katkeb voog 1 enne temperatuuri tõusu ja juhtimine viiakse üle voolule 2. Voog 2 kontrollib sama muutujat, mis pole veel muudetud vool 1. Seega valmistub ka vool 2 temperatuuri tõstma 5 kraadi võrra, kuid tal pole selleks aega ja läheb samuti ooteolekusse. Protsess jätkub kuni voo 1 aktiveerimiseni ja jätkab järgmise käsuga - temperatuuri tõstmine 5 kraadi võrra. Tõus kordub kõigi 10 oja aktiveerimisel ja majaelanikel läheb kehvasti.

Probleemi lahendus: sünkroonimine

Eelmises programmis tekib olukord, kus programmi väljund sõltub lõimede täitmise järjekorrast. Sellest vabanemiseks peate veenduma, et käsud nagu

Kui mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then...

on aktiivse lõime poolt täielikult töödeldud enne selle katkestamist. Seda omadust nimetatakse aatomi häbi - koodiplokk peab toimuma iga lõime poolt katkestusteta, aatomiühikuna. Aatomplokiks kombineeritud käskude rühma ei saa lõime planeerija katkestada enne, kui see on lõpetatud. Igal mitmelõimelisel programmeerimiskeelel on atomaalsuse tagamiseks oma viisid. VB .NET-is on lihtsaim viis SyncLocki käsu kasutamiseks sisestada kutsumisel objektimuutuja. Tehke eelmises näites ChangeTemperature protseduuris väikesed muudatused ja programm töötab hästi:

Privaatne alammuutustemperatuur () SyncLock (mHouse)

Proovi

Kui mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then

Thread.Sleep (200)

mHouse.HouseTemp + = 5

Console.WriteLine ("Am in" & Me.mName & _

".Praegune temperatuur on" & mHouse.HouseTemp)

Ise

mHouse.HouseTemp< mHouse. MAX_TEMP Then

Lõim.Uni (200) mMaja.MajaTemp + = 1

Console.WriteLine ("Am in" & Me.mName & _ ". Praegune temperatuur on" & mHouse.HomeTemp) Else

Console.WriteLineC "Am in" & Me.mName & _ ". Praegune temperatuur on" & mHouse.HouseTemp)

„Ära tee midagi, temperatuur on normaalne

Lõpeta, kui püüda siduda kui lõime katkestatud erand

"Passiivse ootamise katkestas Catch e As Exception

"Muud erandid

Lõpeta proovi

Lõpetage SyncLock

Lõpeta alam

SyncLocki plokikood käivitatakse atomaarselt. Juurdepääs sellele kõikidest teistest lõimedest suletakse, kuni esimene lõim vabastab lukust käsuga End SyncLock. Kui sünkroniseeritud plokis olev lõim läheb passiivsesse ooteolekusse, jääb lukk kuni lõime katkestamiseni või jätkamiseni.

Käsu SyncLock õige kasutamine hoiab teie programmilõime turvalisena. Kahjuks mõjutab SyncLocki liigne kasutamine jõudlust negatiivselt. Koodi sünkroonimine mitme lõimega programmis vähendab selle töö kiirust mitu korda. Sünkroonige ainult kõige vajalikum kood ja vabastage lukk niipea kui võimalik.

Põhikogu klassid ei ole mitme lõimega rakendustes ohutud, kuid .NET Framework sisaldab enamiku kogumisklasside lõimekindlaid versioone. Nendes klassides on potentsiaalselt ohtlike meetodite kood suletud SyncLocki plokkidesse. Kogumisklasside lõimekindlaid versioone tuleks kasutada mitme lõimega programmides, kui andmete terviklikkus on ohus.

Jääb veel mainida, et tingimuslikud muutujad on hõlpsasti rakendatavad käsu SyncLock abil. Selleks peate lihtsalt sünkroonima kirjutamise ühise Boolean atribuudiga, mis on lugemiseks ja kirjutamiseks saadaval, nagu seda tehakse järgmises fragmendis:

Public Class ConditionVariable

Privaatne jagatud kapp objektina = uus objekt ()

Privaatne jagatud mOK kui Boolean Shared

Atribuut TheConditionVariable () tõeväärtusena

Hangi

Tagasi mOK

End Get

Määra (ByVal väärtus tõeväärtusena) SyncLock (kapp)

mOK = väärtus

Lõpetage SyncLock

Lõppkomplekt

Lõppomadus

Lõppklass

SyncLocki käsu- ja monitoriklass

Käsu SyncLock kasutamine hõlmab mõningaid nüansse, mida ülaltoodud lihtsates näidetes ei näidatud. Seega mängib sünkroonimisobjekti valik väga olulist rolli. Proovige eelmist programmi käivitada käsuga SyncLock (Me) SyncLocki (mHouse) asemel. Temperatuur tõuseb taas üle läve!

Pidage meeles, et käsk SyncLock sünkroonib kasutades objekt, edastatakse parameetrina, mitte koodilõigu kaudu. Parameeter SyncLock toimib uksena, mille kaudu pääseb juurde sünkroonitud fragmendile teistest lõimedest. Käsk SyncLock (Me) avab tegelikult mitu erinevat "ust", mis on täpselt see, mida sa sünkroonimisega vältida püüdsid. Moraal:

Jagatud andmete kaitsmiseks mitme lõimega rakenduses peab käsk SyncLock sünkroonima ühe objekti korraga.

Kuna sünkroonimine on seotud konkreetse objektiga, on mõnes olukorras võimalik kogemata lukustada teisi fragmente. Oletame, et teil on kaks sünkroonitud meetodit, esimene ja teine, ning mõlemad meetodid on bigLocki objektiga sünkroonitud. Kui lõime 1 siseneb esimesena meetodisse ja hõivab bigLocki, ei saa ükski lõim teisena meetodit sisestada, kuna juurdepääs sellele on juba piiratud lõimega 1!

Käsu SyncLock funktsionaalsust võib pidada monitori klassi funktsionaalsuse alamhulgaks. Monitori klass on väga kohandatav ja seda saab kasutada mittetriviaalsete sünkroonimisülesannete lahendamiseks. Käsk SyncLock on jälgimisklassi sisestus- ja väljumismeetodite ligikaudne analoog:

Proovi

Monitor.Enter (Objekt) Lõpuks

Monitor.Exit (theObject)

Lõpeta proovi

Mõne standardtoimingu jaoks (muutuja suurendamine/vähendamine, kahe muutuja sisu vahetamine) pakub .NET Framework klassi Interlocked, mille meetodid teostavad neid toiminguid aatomitasandil. Interlocked klassi kasutades on need toimingud palju kiiremad kui käsu SyncLock kasutamine.

Blokeering

Sünkroonimise ajal seatakse lukk objektidele, mitte lõimedele, seega kasutamisel erinev blokeeritavad objektid erinev programmides esinevad koodijupid mõnikord üsna ebatriviaalsed vead. Kahjuks ei ole paljudel juhtudel ühe objekti sünkroonimine lihtsalt lubatud, kuna see viib lõimede liiga sageli blokeerimiseni.

Mõelge olukorrale blokeeriv(tupik) selle kõige lihtsamal kujul. Kujutage ette kahte programmeerijat õhtusöögilauas. Kahjuks on neil kahe peale ainult üks nuga ja üks kahvel. Eeldades, et vajate söömiseks nii nuga kui ka kahvlit, on võimalikud kaks olukorda:

  • Üks programmeerija jõuab haarata noa ja kahvli ning hakkab sööma. Kui tal on kõht täis, paneb ta kõrvale pandud õhtusöögi ja siis saab teine ​​programmeerija need võtta.
  • Üks programmeerija võtab noa ja teine ​​kahvli. Kumbki ei saa sööma hakata, kui teine ​​ei loobu oma seadmest.

Mitme lõimega programmis nimetatakse seda olukorda vastastikune blokeerimine. Need kaks meetodit on erinevatel objektidel sünkroonitud. Lõim A hõivab objekti 1 ja siseneb selle objektiga kaitstud programmiosasse. Kahjuks vajab see selle toimimiseks juurdepääsu koodile, mis on kaitstud teise sünkroonimislukuga, millel on erinev sünkroonimisobjekt. Kuid enne, kui on aega teise objektiga sünkroonitud fragmenti sisestada, siseneb voog B sellesse ja jäädvustab selle objekti. Nüüd ei saa lõim A siseneda teise fragmenti, niit B ei saa siseneda esimesse fragmenti ja mõlemad lõimed on määratud lõputult ootama. Ükski lõime ei saa jätkata, kuna vajalikku objekti ei vabastata kunagi.

Ummikseisude diagnoosimist raskendab asjaolu, et need võivad tekkida suhteliselt harvadel juhtudel. Kõik sõltub sellest, millises järjekorras planeerija neile protsessori aega eraldab. Võimalik, et enamikul juhtudel jäädvustatakse sünkroonimisobjektid mitte-ummikus järjekorras.

Järgnev on äsja kirjeldatud ummikseisu rakendamine. Pärast kõige olulisemate punktide lühikest arutelu näitame, kuidas tuvastada lõimeaknas ummikseisu:

1 Valik Rangelt sees

2 Impordisüsteem. Keermestamine

3 mooduli moodul

4 alampeamist ()

5 Dim Tom kui uus programmeerija ("Tom")

6 Dim Bob uue programmeerijana ("Bob")

7 Dim aThreadStart kui uus ThreadStart (AddressOf Tom.Eat)

8 Dim aThread uue lõimena (aThreadStart)

9 aThread.Name = "Tom"

10 Dim bThreadStart kui uus ThreadStartAddressOf Bob.Eat)

11 Dim bThread uue lõimena (bThreadStart)

12 bThread.Name = "Bob"

13 aThread.Start ()

14 bTread.Start ()

15 End Sub

16 Lõppmoodul

17 Avaliku klassi kahvel

18 Privaatne jagatud mForkAvaiTable kui Boolean = tõene

19 Private Shared Owner As String = "Keegi"

20 Private Readonly Property OwnsUtensil () Stringina

21 Hankige

22 Tagastatav niitja

23 End Get

24 Lõppomand

25 avalik alam GrabForktByVal a programmeerijana)

26 Console.Writel_ine (Thread.CurrentThread.Name & _

"proovib kahvlit haarata.")

27 Console.WriteLine (Me.OwnsUtensil & "on the fork."). ...

28 Monitor.Sisesta (mina) "SyncLock (aFork)"

29 Kui mForkAvailable Siis

30 a.HasFork = tõsi

31 omanik = a.MinuNimi

32 mKahvelSaadaval = Vale

33 Console.WriteLine (a.MyName & "just got the fork.waiting")

34 Proovige

Thread.Sleep (100) Catch e As Exception Console.WriteLine (e.StackTrace)

Lõpeta proovi

35 Lõpp Kui

36 Monitor.Exit (mina)

Lõpetage SyncLock

37 End Sub

38 Lõppklass

39 Avaliku klassi nuga

40 Privaatne jagatud mNugaSaadaval Booleanina = Tõene

41 Private Shared Owner As String = "Keegi"

42 Privaatne kirjutuskaitstud atribuut OwnsUtensi1 () Stringina

43 Hangi

44 Tagastatav niitja

45 End Get

46 Lõppomand

47 Avalik Sub GrabKnifetByVal a Programmeerijana)

48 Console.WriteLine (Thread.CurrentThread.Name & _

"proovib nuga haarata.")

49 Console.WriteLine (Me.OwnsUtensil & "on the nuga.")

50 Monitor.Sisestage (mina) "SyncLock (aKnife)"

51 Kui mKnifeAvailable Siis

52 mKnifeAvailable = vale

53 a.HasKnife = tõsi

54 Owner = a.MinuNimi

55 Console.WriteLine (a.MyName & "sain just noa kätte.ootab")

56 Proovige

Lõim.Unerežiim (100)

Catch e As Exception

Console.WriteLine (e.StackTrace)

Lõpeta proovi

57 Lõpp Kui

58 Monitor.Exit (mina)

59 End Sub

60 Lõppklass

61 Avaliku klassi programmeerija

62 Privaatne mName stringina

63 Privaatne jagatud mFork kui kahvel

64 Privaatne jagatud mKnife kui noa

65 Privaatne mHasKnife kui Boolean

66 Privaatne mHasFork kui Boolean

67 jagatud alam-uus ()

68 m Fork = uus kahvel ()

69 mKnife = uus nuga ()

70 End Sub

71 Avalik alam uus (ByVal theName stringina)

72 mName = theName

73 End Sub

74 Avalik kirjutuskaitstud atribuut MinuNimi () Stringina

75 Hangi

76 Tagasta mName

77 End Get

78 Lõppomand

79 Public Property HasKnife () Booleanina

80 Hankige

81 Tagastage mHasKnife

82 End Get

83 komplekt (ByVal väärtus tõeväärtusena)

84 mHasKnife = väärtus

85 Lõppkomplekt

86 Lõppomand

87 Public Property HasFork () kui Boolean

88 Hangi

89 Tagasta mHasFork

90 End Get

91 komplekt (ByVal väärtus tõeväärtusena)

92 mHasFork = väärtus

93 Lõppkomplekt

94 Lõppomand

95 avalikku söögikohta ()

96 Do Until Me.HasKnife And Me.HasFork

97 Console.Writeline (Thread.CurrentThread.Name & "on lõimes.")

98 Kui Rnd ()< 0.5 Then

99 mFork.GrabFork (mina)

100 muu

101 mKnife.GrabKnife (mina)

102 Lõpp Kui

103 Loop

104 MsgBox (Me.MyName & "saab süüa!")

105 mNuga = uus nuga ()

106 m Fork = uus kahvel ()

107 End Sub

108 Lõppklass

Põhiprotseduur Main (read 4-16) loob kaks Programmeerija klassi eksemplari ja seejärel käivitab kaks lõime, et käivitada Programmer klassi kriitilise söömise meetod (read 95-108), mida kirjeldatakse allpool. Protseduur Main määrab lõimede nimed ja seadistab need; ilmselt kõik toimuv on arusaadav ja ilma kommentaarideta.

Klassi Fork kood tundub huvitavam (read 17-38) (sarnane Knife klass on määratletud ridadel 39-60). Ridadel 18 ja 19 on määratud ühisväljade väärtused, mille järgi saate teada, kas pistik on hetkel saadaval ja kui mitte, siis kes seda kasutab. ReadOnly atribuut OwnUtensi1 (read 20-24) on mõeldud teabe lihtsaimaks edastamiseks. Klassis Fork on kesksel kohal meetod GrabFork “haara kahvlist”, mis on määratletud ridadel 25–27.

  1. Read 26 ja 27 prindivad lihtsalt silumisinfot konsooli. Meetodi põhikoodis (read 28-36) sünkroonitakse juurdepääs kahvlile objektigavöö Mina. Kuna meie programm kasutab ainult ühte kahvlit, tagab Me sync, et kaks lõime ei saaks seda korraga haarata. Käsk Slee "p (34. realt algavas plokis) simuleerib viivitust kahvli/noa haaramise ja söömise alustamise vahel. Pange tähele, et Sleep-käsk ei ava objekte ja ainult kiirendab ummikseisu!
    Kõige huvitavam on aga Programmer klassi kood (read 61-108). Read 67–70 määratlevad üldise konstruktori tagamaks, et programmis on ainult üks kahvel ja nuga. Omandikood (read 74–94) on lihtne ega vaja kommentaare. Kõige olulisem asi juhtub söömismeetodis, mida teostavad kaks eraldi lõime. Protsess jätkub tsüklina, kuni mõni vool haarab kahvli koos noaga. Ridadel 98–102 haarab objekt juhuslikult kahvli/noa, kasutades Rnd-kutset, mis põhjustab ummikseisu. Juhtub järgmine:
    Lõim, mis käivitab Toti söömismeetodi, käivitatakse ja siseneb tsüklisse. Ta haarab noa ja läheb ooteseisundisse.
  2. Bob's Eat meetodit käivitav lõim käivitatakse ja siseneb tsüklisse. See ei saa haarata nuga, kuid see haarab kahvli ja läheb ooterežiimi.
  3. Lõim, mis käivitab Toti söömismeetodi, käivitatakse ja siseneb tsüklisse. Ta üritab kahvlit haarata, kuid Bob on juba hargist kinni haaranud; niit läheb ooteolekusse.
  4. Bob's Eat meetodit käivitav lõim käivitatakse ja siseneb tsüklisse. Ta püüab nuga haarata, kuid noa on juba tabanud objekt Thoth; niit läheb ooteolekusse.

Kõik see jätkub lõputult - seisame silmitsi tüüpilise ummikseisuga (proovige programm käivitada ja näete, et keegi ei saa niimoodi süüa).
Samuti näete lõimede aknas ummikseisu. Käivitage programm ja katkestage see klahvidega Ctrl + Break. Kaasake vaateakna muutuja Mina ja avage voogude aken. Tulemus näeb välja umbes nagu joonisel fig. 10.7. Jooniselt on näha, et Bobi niit on haaranud noa, kuid sellel pole kahvlit. Paremklõpsake aknas Threads real Tot ja valige kontekstimenüüst käsk Switch to Thread. Vaateaknast on näha, et Thothi ojal on kahvel, kuid mitte nuga. See pole muidugi sada protsenti tõend, aga selline käitumine tekitab vähemalt kahtluse, et midagi oli valesti.
Kui ühe objektiga sünkroonimise võimalus (nagu majas temperatuuri tõstmise programmis) pole võimalik, saab vastastikuste lukkude vältimiseks nummerdada sünkroonimisobjekte ja neid alati konstantses järjekorras jäädvustada. Jätkame söögikoha programmeerija analoogiaga: kui niit võtab alati kõigepealt noa ja seejärel kahvli, ei teki blokeerimisega probleeme. Esimene vool, mis noa haarab, saab normaalselt süüa. Programmi voogude keelde tõlgituna tähendab see, et objekti 2 püüdmine on võimalik ainult siis, kui objekt 1 on esmalt hõivatud.

Riis. 10.7. Lõngaakna ummikseisude analüüs

Seega, kui eemaldame liinil 98 kõne Rnd-le ja asendame selle fragmendiga

mFork.GrabFork (mina)

mKnife.GrabKnife (mina)

ummik kaob!

Tehke andmete loomisel koostööd

Mitme lõimega rakendustes on sageli olukord, kus lõimed mitte ainult ei tööta jagatud andmetega, vaid ootavad ka nende ilmumist (st lõim 1 peab looma andmed, enne kui lõim 2 saab neid kasutada). Kuna andmeid jagatakse, tuleb juurdepääs neile sünkroonida. Samuti on vaja ette näha vahendid ootavate lõimede teavitamiseks valmisandmete ilmumisest.

Seda olukorda nimetatakse tavaliselt tarnija/tarbija probleem. Lõim püüab juurde pääseda andmetele, mida veel ei eksisteeri, seega peab see juhtima teisele lõimele, mis loob vajalikud andmed. Probleem lahendatakse järgmise koodiga:

  • Lõim 1 (tarbija) ärkab, sisestab sünkroonitud meetodi, otsib andmeid, ei leia neid ja läheb ooteolekusse. Esialgufüüsiliselt peab ta blokeeringu eemaldama, et mitte segada toitekeerme tööd.
  • Lõim 2 (pakkuja) siseneb sünkroniseeritud meetodisse, mille vabastab lõime 1, loob 1. voo andmed ja teavitab voogu 1 andmete olemasolust. Seejärel vabastab see luku, et lõim 1 saaks uusi andmeid töödelda.

Ärge proovige seda probleemi lahendada, kutsudes pidevalt esile lõime 1 ja kontrollides tingimusmuutuja seisukorda, mille väärtus on> 2. lõimega määratud. See otsus mõjutab tõsiselt teie programmi jõudlust, kuna enamikul juhtudel kutsub lõime 1 välja mitte. põhjus; ja lõim 2 ootab nii sageli, et andmete loomise aeg saab otsa.

Pakkuja/tarbija suhted on väga levinud, seetõttu luuakse selliste olukordade jaoks spetsiaalsed primitiivid mitme lõimega programmeerimisklassi teekides. NETis nimetatakse neid primitiive Wait ja Pulse-PulseAl 1 ning need kuuluvad monitori klassi. Joonis 10.8 illustreerib olukorda, mida hakkame programmeerima. Programm korraldab kolm lõime järjekorda: ootejärjekord, blokeerimisjärjekord ja täitmisjärjekord. Lõimide ajakava ei eralda protsessori aega ootejärjekorras olevatele lõimedele. Lõimele aja eraldamiseks peab see liikuma täitmisjärjekorda. Selle tulemusena on rakenduse töö korraldatud palju tõhusamalt kui tavapärase tingimusliku muutuja küsitlemisega.

Pseudokoodis on andmetarbija idioom sõnastatud järgmiselt:

"Sisenemine järgmist tüüpi sünkroniseeritud plokki

Kuigi andmeid pole

Minge ootejärjekorda

Loop

Kui andmeid on, siis töötle neid.

Lahku sünkroonitud blokist

Kohe pärast käsu Wait täitmist lõim peatatakse, lukk vabastatakse ja lõim siseneb ootejärjekorda. Kui lukk vabastatakse, lubatakse täitmisjärjekorra lõimel töötada. Aja jooksul loob üks või mitu blokeeritud lõime ootejärjekorras oleva lõime tööks vajalikud andmed. Kuna andmete valideerimine toimub tsüklina, siis üleminek andmete kasutamisele (pärast tsüklit) toimub alles siis, kui andmed on töötlemiseks valmis.

Pseudokoodis näeb andmepakkuja idioom välja järgmine:

"Sünkroniseeritud vaateploki sisestamine

Kuigi andmeid EI OLE vaja

Minge ootejärjekorda

Else Toota andmeid

Kui andmed on valmis, helistage Pulse-PulseAll.

ühe või mitme lõime teisaldamiseks blokeerimisjärjekorrast täitmisjärjekorda. Väljuge sünkroonitud plokist (ja naaske käitamisjärjekorda)

Oletame, et meie programm simuleerib perekonda, kus üks vanem teenib raha ja laps, kes seda raha kulutab. Kui raha on otsasselgub, et laps peab ootama uue summa saabumist. Selle mudeli tarkvararakendus näeb välja järgmine:

1 Valik Rangelt sees

2 Impordisüsteem. Keermestamine

3 mooduli moodul

4 alampeamist ()

5 Dim the Family kui uus perekond ()

6 pere.Alusta elu ()

7 Lõpeta alam

8 Lõpeta fjodul

9

10 avaliku klassi perekond

11 Privaatne mRaha täisarvuna

12 privaatne mnädal täisarvuna = 1

13 Public Sub StartltsLife ()

14 Dim aThreadStart kui uus ThreadStarUAddressOf Me.Produce)

15 Dim bThreadStart As New ThreadStarUAddressOf Me.Consume)

16 Dim aThread uue lõimena (aThreadStart)

17 Dim bThread uue lõimena (bThreadStart)

18 aThread.Name = "Toota"

19 aThread.Start ()

20 bThread.Name = "Tarbi"

21 bniit. Alusta ()

22 End Sub

23 Public Property TheWeek () Täisarvuna

24 Hankige

25 Tagasi mnädalal

26 End Get

27 Set (ByVal väärtus täisarvuna)

28 mnädal – väärtus

29 Lõppkomplekt

30 Lõppvara

31 Avalik omand MeieRaha () Täisarvuna

32 Hankige

33 Tagasta mRaha

34 End Get

35 Set (ByVal väärtus täisarvuna)

36 miljonit raha = väärtus

37 Lõppkomplekt

38 Lõppomand

39 Avalik alamprodukt ()

40 Thread.Sleep (500)

41 Tee

42 Monitor.Enter (mina)

43 Tee Kuigi mina.Meie raha> 0

44 Monitor.Oota (mina)

45 Silmus

46 Mina.Meie raha = 1000

47 Monitor.PulseAll (mina)

48 Monitor.Exit (mina)

49 Silmus

50 End Sub

51 Avalik alamtarbija ()

52 MsgBox ("Olen tarbimise lõimes")

53 Tee

54 Monitor.Enter (mina)

55 Tee Kuigi mina.Meie raha = 0

56 Monitor.Oota (mina)

57 Loop

58 Console.WriteLine ("Kallis vanem, ma kulutasin just kogu teie" & _

raha nädalas "& The Week"

59 Nädal + = 1

60 Kui nädal = 21 * 52, siis System.Environment.Exit (0)

61 Mina.Meie raha = 0

62 Monitor.PulseAll (mina)

63 Monitor.Exit (mina)

64 Loop

65 End Sub

66 Lõppklass

StartltsLife meetod (read 13–22) valmistub voogude Toota ja Tarbi käivitamiseks. Kõige olulisem asi toimub voogudes Toota (read 39-50) ja Consume (read 51-65). Sub Produce protseduur kontrollib raha olemasolu ja kui raha on, läheb see ootejärjekorda. Vastasel juhul genereerib vanem raha (rida 46) ja teavitab olukorra muutumisest ootejärjekorras olevaid objekte. Pange tähele, et Pulse-Pulse All väljakutse jõustub ainult siis, kui lukk vabastatakse käsuga Monitor.Exit. Seevastu Subsume protseduur kontrollib raha olemasolu ja kui raha pole, annab sellest lapseootel lapsevanemale teada. Rida 60 lihtsalt lõpetab programmi pärast 21 tingimuslikku aastat; helistamissüsteem. Environment.Exit (0) on .NET-i analoog käsule End (toetatud on ka käsk End, kuid erinevalt System. Environment. Exit-st ei tagasta see operatsioonisüsteemile väljumiskoodi).

Teie programmi teised osad peavad vabastama ootejärjekorda pandud lõimed. Sel põhjusel eelistame kasutada PulseAll'i kui Pulse. Kuna Pulse 1 kutsumisel pole ette teada, milline lõim aktiveerub, siis kui niite on järjekorras suhteliselt vähe, saab sama hästi välja kutsuda ka PulseAll.

Mitme lõimega töötlemine graafikaprogrammides

Meie arutelu graafiliste kasutajaliidese rakenduste mitmelõimestamise kohta algab näitega, mis selgitab, milleks GUI rakenduste mitmelõimeline kasutamine on mõeldud. Looge vorm kahe nupuga Start (btnStart) ja Cancel (btnCancel), nagu on näidatud joonisel fig. 10.9. Klõpsates nuppu Start, genereeritakse klass, mis sisaldab 10 miljonist tähemärgist koosnevat juhuslikku stringi ja meetodit tähe "E" esinemiste loendamiseks selles pikas stringis. Pange tähele klassi StringBuilder kasutamist pikkade stringide tõhusamaks loomiseks.

Samm 1

1. lõim märkab, et selle kohta pole andmeid. See helistab Wait, vabastab luku ja läheb ootejärjekorda.



2. samm

Kui lukk vabastatakse, lahkub niit 2 või 3 ploki järjekorrast ja siseneb sünkroniseeritud plokki, omandades luku

3. samm

Oletame, et lõime 3 siseneb sünkroniseeritud plokki, loob andmed ja kutsub esile Pulse-Pulse All.

Kohe pärast plokist väljumist ja luku vabastamist viiakse lõim 1 täitmisjärjekorda. Kui lõim 3 kutsub Pluse'i, siseneb täitmisjärjekorda ainult ükslõime, kui Pluse All kutsutakse, lähevad kõik lõimed täitmisjärjekorda.



Riis. 10.8. Pakkuja/tarbija probleem

Riis. 10.9. Mitme lõimega töötlemine lihtsas GUI rakenduses

Impordib System.Text

Avaliku klassi juhuslikud tegelased

Privaatne m_Data StringBuilderina

Privaatne mjength, m_count täisarvuna

Avalik alam uus (ByVal n täisarvuna)

m_Pikkus = n -1

m_Data = Uus StringBuilder (m_length) MakeString ()

Lõpeta alam

Privaatne alam MakeString ()

Dim i täisarvuna

Dim myRnd uuena juhuslikult ()

Kui i = 0 kuni m_pikkus

"Genereerige juhuslik arv vahemikus 65 kuni 90,

"teisenda see suurtähtedeks

"ja lisage StringBuilderi objektile

m_Data.Append (Chr (myRnd.Next (65.90)))

Edasi

Lõpeta alam

Avalik alam algusarv ()

GetEes ()

Lõpeta alam

Privaatne alam-GetEes ()

Dim i täisarvuna

Kui i = 0 kuni m_pikkus

Kui m_Data.Chars (i) = CChar ("E") Siis

m_count + = 1

Lõpeta, kui järgmine

m_CountDone = Tõene

Lõpeta alam

Avalik kirjutuskaitstud

Atribuut GetCount () Integer Get

Kui mitte (m_CountDone) Siis

Tagasta m_count

Lõpeta Kui

End Hankige lõppvara

Avalik kirjutuskaitstud

Atribuut IsDone () Nagu Boolean Get

Tagasi

m_Loendamine Valmis

End Get

Lõppomadus

Lõppklass

Vormi kahe nupuga on seotud väga lihtne kood. Protseduur btn-Start_Click loob ülaltoodud RandomCharacters klassi, mis kapseldab 10 miljoni tähemärgiga stringi:

Privaatne alam btnStart_Click (ByVal saatja kui System.Object.

ByVal e As System.EventArgs) Käepidemed btnStart.Click

Dim RC uute juhuslike tegelastena (10000000)

RC.StartCount ()

MsgBox ("E-de arv on" & RC.GetCount)

Lõpeta alam

Nupp Tühista kuvab teatekasti:

Privaatne alam btnCancel_Click (ByVal saatja kui System.Object._

ByVal e As System.EventArgs) Käsitseb btnCancel.Click

MsgBox ("Loendamine katkestati!")

Lõpeta alam

Programmi käivitamisel ja nupule Start vajutamisel selgub, et nupp Tühista ei reageeri kasutaja sisendile, kuna pidev tsükkel takistab nupul vastuvõetud sündmuse käsitlemist. See on kaasaegsetes programmides vastuvõetamatu!

Võimalikke lahendusi on kaks. Esimene võimalus, mis on hästi tuntud eelmistest VB versioonidest, loobub mitme lõimega ühendamisest: DoEventsi kõne on tsüklisse kaasatud. NET-is näeb see käsk välja järgmine:

Application.DoEvents ()

Meie näites pole see kindlasti soovitav – kes see ikka tahab kümne miljoni DoEventsi kõnega programmi aeglustada! Kui eraldate tsükli hoopis eraldi lõimele, lülitub operatsioonisüsteem lõimede vahel ümber ja nupp Tühista jääb tööle. Eraldi lõimega teostus on näidatud allpool. Et selgelt näidata, et nupp Tühista töötab, lõpetame sellel klõpsamisel programmi lihtsalt.

Järgmine samm: Kuva loenduri nupp

Oletame, et otsustasite näidata oma loomingulist kujutlusvõimet ja anda vormile joonisel fig. 10.9. Pange tähele: nupp Näita loendust pole veel saadaval.

Riis. 10.10. Lukustatud nupu vorm

Eeldatakse, et eraldi lõim loeb ja avab kättesaamatu nupu. Seda saab loomulikult teha; pealegi tekib selline ülesanne üsna sageli. Kahjuks ei saa te toimida kõige ilmsemal viisil – linkige teisese lõime GUI lõimega, hoides linki konstruktoris nupule ShowCount või kasutades isegi tavalist delegaati. Teisisõnu, mitte kunagiära kasuta allolevat valikut (põhiline ekslik read on paksus kirjas).

Avaliku klassi juhuslikud tegelased

Privaatne m_0ata StringBuilderina

Privaatne m_CountDone kui Boolean

Privaatne mjength. m_count täisarvuna

Privaatne m_Button Nagu Windows.Forms.Button

Avalik alam uus (ByVa1 n täisarvuna, _

ByVal b Nagu Windows.Forms.Button)

m_pikkus = n–1

m_Data = Uus StringBuilder (mJength)

m_Button = b MakeString ()

Lõpeta alam

Privaatne alam MakeString ()

Dim I As Integer

Dim myRnd uuena juhuslikult ()

I = 0 kuni m_pikkus

m_Data.Append (Chr (myRnd.Next (65.90)))

Edasi

Lõpeta alam

Avalik alam algusarv ()

GetEes ()

Lõpeta alam

Privaatne alam-GetEes ()

Dim I As Integer

I = 0 To mjength

Kui m_Data.Chars (I) = CChar ("E") Siis

m_count + = 1

Lõpeta, kui järgmine

m_CountDone = Tõene

m_Button.Enabled = Tõene

Lõpeta alam

Avalik kirjutuskaitstud

Atribuut GetCount () täisarvuna

Hangi

Kui mitte (m_CountDone) Siis

Viska uus erand ("Loendus pole veel tehtud") Muu

Tagasta m_count

Lõpeta Kui

End Get

Lõppomadus

Kirjutuskaitstud avalik atribuut IsDone () Tõeväärtusena

Hangi

Tagasi m_CountDone

End Get

Lõppomadus

Lõppklass

On tõenäoline, et see kood mõnel juhul töötab. Sellegipoolest:

  • Teisese lõime ja GUI loova lõime koostoimet ei saa korraldada ilmselge tähendab.
  • Mitte kunagiärge muutke teiste programmivoogude graafikaprogrammide elemente. Kõik muudatused peaksid toimuma ainult GUI loonud lõimes.

Kui rikute neid reegleid, siis meie me garanteerime et teie mitme lõimega graafikaprogrammides ilmnevad peened ja peened vead.

Samuti ei suuda see sündmusi kasutades korraldada objektide interaktsiooni. 06-sündmuse töötaja töötab samal lõimel, mida nimetati RaiseEventiks, nii et sündmused ei aita teid.

Siiski nõuab terve mõistus, et graafilistel rakendustel peab olema vahend mõne teise lõime elementide muutmiseks. NET Frameworkis on lõimekindel viis GUI-rakenduste meetodite kutsumiseks teisest lõimest. Selleks kasutatakse System.Windowsi nimeruumi spetsiaalset tüüpi Method Invokeri delegaati. Vormid. Järgmine väljavõte näitab GetEesi meetodi uut versiooni (muetud read paksus kirjas):

Privaatsed alam-GetEes ()

Dim I As Integer

I = 0 kuni m_pikkus

Kui m_Data.Chars (I) = CChar ("E") Siis

m_count + = 1

Lõpeta, kui järgmine

m_CountDone = Tõeline Proovi

Dim mylnvoker as New Methodlnvoker (AddressOf UpDateButton)

myInvoker.Invoke () Catch e As ThreadlnterruptedException

"Ebaõnnestumine

Lõpeta proovi

Lõpeta alam

Avalik alam värskendamise nupp ()

m_Button.Enabled = Tõene

Lõpeta alam

Lõimedevahelised kõned nupule tehakse mitte otse, vaid läbi Method Invokeri. .NET Framework garanteerib, et see suvand on lõime ohutu.

Miks on mitmelõimelise programmeerimisega nii palju probleeme?

Nüüd, kui olete mitme lõimega töötlemisest ja sellega seotud võimalikest probleemidest veidi aru saanud, otsustasime, et oleks asjakohane vastata selle peatüki lõpus olevale alajaotise pealkirjas olevale küsimusele.

Üks põhjusi on see, et multithreading on mittelineaarne protsess ja me oleme harjunud lineaarse programmeerimismudeliga. Alguses on raske harjuda juba mõttega, et programmi täitmine võib juhuslikult katkeda ja juhtimine läheb üle teisele koodile.

Siiski on veel üks, põhimõttelisem põhjus: tänapäeval programmeerivad programmeerijad liiga harva assembleris või vaatavad vähemalt kompilaatori lahtivõetud väljundit. Vastasel juhul oleks neil palju lihtsam harjuda mõttega, et kõrgetasemelise keele (näiteks VB .NET) ühele käsule võivad vastata kümned montaažijuhised. Lõime saab katkestada pärast mõnda neist juhistest ja seega ka kõrgetasemelise käsu keskel.

Kuid see pole veel kõik: kaasaegsed kompilaatorid optimeerivad programmide jõudlust ja arvuti riistvara võib segada mäluhaldust. Selle tulemusena võib kompilaator või riistvara muuta programmi lähtekoodis määratud käskude järjekorda ilma teie teadmata [ Paljud kompilaatorid optimeerivad massiivide tsüklilist kopeerimist, näiteks i = 0 kuni n: b (i) = a (i): ncxt. Kompilaator (või isegi spetsiaalne mäluhaldur) saab lihtsalt luua massiivi ja seejärel täita selle ühe kopeerimistoiminguga, selle asemel et üksikuid elemente mitu korda kopeerida!].

Loodetavasti aitavad need selgitused sul paremini mõista, miks mitmelõimeline programmeerimine nii palju probleeme tekitab – või vähemalt vähem üllatust sinu mitmelõimeliste programmide kummalise käitumise üle!

Näide lihtsa mitme keermega rakenduse loomisest.

Sündis paljude küsimuste tõttu Delphis mitmelõimeliste rakenduste loomise kohta.

Selle näite eesmärk on näidata, kuidas mitme lõimega rakendust õigesti ehitada, võttes pikaajalise töö eraldi lõime. Ja kuidas sellises rakenduses tagada põhilõime suhtlemine töötajaga andmete vormilt (visuaalsed komponendid) voogu edastamiseks ja vastupidi.

Näide ei pretendeeri täielikkusele, see demonstreerib ainult lõimedevahelise suhtluse lihtsamaid viise. Võimaldab kasutajal "kiiresti pimestada" (kes teaks, kui väga ma seda vihkan) korralikult töötavat mitmelõimega rakendust.
Kõik seal on üksikasjalikult kommenteeritud (minu arvates), kuid kui teil on küsimusi, küsige.
Kuid veel kord hoiatan teid: Vood ei ole lihtsad... Kui teil pole õrna aimugi, kuidas see kõik töötab, siis on suur oht, et sageli töötab teie jaoks kõik hästi ja mõnikord käitub programm rohkem kui kummaliselt. Valesti kirjutatud mitme lõimega programmi käitumine sõltub suuresti paljudest teguritest, mida mõnikord ei saa silumise käigus taasesitada.

Nii et näide. Mugavuse huvides olen paigutanud nii koodi kui ka lisanud arhiivi koos mooduli ja vormi koodiga

üksus ExThreadForm;

kasutab
Windows, sõnumid, SysUtils, variandid, klassid, graafika, juhtnupud, vormid,
Dialoogid, StdCtrl;

// konstandid, mida kasutatakse andmete edastamisel voost vormi kasutades
// aknateadete saatmine
konst
WM_USER_SendMessageMetod = WM_USER + 10;
WM_USER_PostMessageMetod = WM_USER + 11;

tüüp
// lõime klassi kirjeldus, tTreadi järeltulija
tMyThread = klass (tThread)
privaatne
SyncDataN: täisarv;
SyncDataS: string;
protseduur SyncMetod1;
kaitstud
protseduur Execute; alistama;
avalik
Param1: string;
Param2: täisarv;
Param3: Boolean;
Peatatud: Boolean;
LastRandom: täisarv;
Iteratsiooni nr: täisarv;
Tulemusloend: tStringList;

Constructor Create (aParam1: String);
hävitaja Hävita; alistama;
lõpp;

// voogu kasutava vormi klassikirjeldus
Tvorm1 = klass (Tvorm)
Silt1: Tsilt;
Memo1: TMemo;
btnStart: TButton;
btnStop: TButton;
Edit1: TEdit;
Edit2: TEdit;
Märkeruut1: TCheckbox;
Silt2: Tsilt;
Silt3: TLabel;
Silt4: TLabel;
protseduur btnStartClick (Saatja: TObject);
protseduur btnStopClick (Saatja: TObject);
privaatne
(eraavaldused)
MyThread: tMyThread;
protseduur EventMyThreadOnTerminate (Saatja: tObject);
protseduur EventOnSendMessageMetod (var Msg: TMessage); teade WM_USER_SendMessageMetod;
protseduur EventOnPostMessageMetod (var Msg: TMessage); sõnum WM_USER_PostMessageMetod;

Avalik
(Avalikud deklaratsioonid)
lõpp;

var
Vorm1: Tvorm1;

{
Peatatud – näitab andmete edastamist vormist voogu.
Täiendav sünkroonimine pole vajalik, kuna see on lihtne
ühesõnaline tüüp ja seda kirjutab ainult üks lõime.
}

protseduur TForm1.btnStartClick (Saatja: TObject);
alustada
Juhuslik (); // järjestuse juhuslikkuse tagamine Random () abil - sellel pole vooluga midagi pistmist

// Looge vooobjekti eksemplar, edastades sellele sisendparameetri
{
TÄHELEPANU!
Voo konstruktor on kirjutatud nii, et voog luuakse
peatatakse, kui see võimaldab:
1. Kontrollige selle käivitamise hetke. See on peaaegu alati mugavam, sest
võimaldab teil seadistada voo isegi enne alustamist, edastada see sisend
parameetrid jne.
2. Sest link loodud objektile salvestatakse vormiväljale, siis
pärast niidi enesehävitamist (vt allpool), mis siis, kui niit töötab
võib igal ajal ilmneda, muutub see link kehtetuks.
}
MyThread: = tMyThread.Create (Form1.Edit1.Text);

// Kuna aga lõim loodi peatatud, siis mis tahes vigade korral
// selle initsialiseerimisel (enne käivitamist) peame selle ise hävitama
// mille jaoks me kasutame try / välja arvatud blokk
proovige

// Lõime lõpetamise töötleja määramine, milles me saame
// voo töö tulemused ja selle link "üle kirjutada".
MyThread.OnTerminate: = EventMyThreadOnTerminate;

// Kuna tulemused kogutakse OnTerminate, st. enne enesehävitamist
// oja siis võtame selle hävitamise mure maha
MyThread.FreeOnTerminate: = Tõene;

// Näide sisendparameetrite edastamisest läbi vooobjekti väljade, punktis
// eksemplari, kui see veel ei tööta.
// Mina isiklikult eelistan seda teha overriden parameetrite kaudu
// konstruktor (tMyThread.Create)
MyThread.Param2: = StrToInt (vorm1.Edit2.Text);

MyThread.Stopped: = Vale; // omamoodi parameeter ka, aga muutuv sisse
// lõime tööaeg
välja arvatud
// kuna lõim pole veel alanud ja ei saa ennast hävitada, siis hävitame selle "käsitsi"
FreeAndNil (MyThread);
// ja las siis erandiga käituda nagu tavaliselt
tõsta;
lõpp;

// Kuna lõimeobjekt on edukalt loodud ja konfigureeritud, on aeg see käivitada
MyThread.Resume;

ShowMessage ("Stream algas");
lõpp;

protseduur TForm1.btnStopClick (Saatja: TObject);
alustada
// Kui lõime eksemplar on endiselt olemas, paluge sellel peatada
// Ja täpselt "küsi". Põhimõtteliselt saame ka "sundida", aga läheb
// äärmiselt hädaolukord, mis nõuab kõigest selget arusaamist
// voogesituse köök. Seetõttu seda siin ei käsitleta.
kui Määratud (MyThread), siis
MyThread.Stopped: = Tõene
muidu
ShowMessage ("Lõim ei tööta!");
lõpp;

protseduur TForm1.EventOnSendMessageMetod (var Msg: TMessage);
alustada
// meetod sünkroonse sõnumi töötlemiseks
// WParamis objekti tMyThread aadress, LParamis lõime LastRandom hetke väärtus
tMyThreadiga (Msg.WParam) alustage
Vorm1.Silt3.Caption: = Vorming ("% d% d% d",);
lõpp;
lõpp;

protseduur TForm1.EventOnPostMessageMetod (var Msg: TMessage);
alustada
// meetod asünkroonse sõnumi käsitlemiseks
// WParamis IterationNo praegune väärtus, LParamis voo LastRandom praegune väärtus
Vorm1.Silt4.Caption: = Vorming ("% d% d",);
lõpp;

protseduur TForm1.EventMyThreadOnTerminate (Saatja: tObject);
alustada
// TÄHTIS!
// Sündmuse OnTerminate käsitlemise meetodit kutsutakse alati välja peamise kontekstis
// lõim – selle tagab tThreadi teostus. Seetõttu saate selles vabalt
// kasutada mis tahes objektide omadusi ja meetodeid

// Igaks juhuks veendu, et objekti eksemplar on ikka olemas
kui pole määratud (MyThread), siis välju; // kui seda pole, siis pole midagi teha

// saada lõimeobjekti eksemplari lõime töö tulemused
Form1.Memo1.Lines.Add (Format ("Voo lõppes tulemusega% d"));
Form1.Memo1.Lines.AddStrings ((Saatja kui tMyThread) .ResultList);

// Hävitage viide vooobjekti eksemplarile.
// Kuna meie lõim on ennasthävitav (FreeOnTerminate: = Tõene)
// siis pärast OnTerminate'i käitleja lõpetamist on vooobjekti eksemplar
// hävitatud (tasuta) ja kõik viited sellele muutuvad kehtetuks.
// Et mitte kogemata sellisele lingile sattuda, moosime MyThreadi
// Märgin veel kord - me ei hävita objekti, vaid kirjutame ainult lingi üle. Objekt
// hävitab ennast!
MyThread: = null;
lõpp;

konstruktor tMyThread.Create (aParam1: String);
alustada
// Peatatud voo eksemplari loomine (vaadake instantimisel kommentaari)
päritud Loo (True);

// Looge sisemised objektid (vajadusel)
ResultList: = tStringList.Create;

// Hankige algandmed.

// Kopeerige parameetri kaudu edastatud sisendandmed
Param1: = aParam1;

// Näide sisendandmete vastuvõtmisest VCL-i komponentidelt vooobjekti konstruktoris
// See on antud juhul vastuvõetav, kuna konstruktorit kutsutakse kontekstis
// põhilõng. Seetõttu pääseb VCL-i komponentidele juurde siit.
// Aga see mulle ei meeldi, sest minu arvates on halb, kui niit midagi teab
// mingi vormi kohta seal. Aga mida sa ei saa demonstratsiooniks teha.
Param3: = Vorm1.Checkbox1.Checked;
lõpp;

hävitaja tMyThread.Destroy;
alustada
// sisemiste objektide hävitamine
FreeAndNil (ResultList);
// hävitab baasi tTread
päritud;
lõpp;

protseduur tMyThread.Execute;
var
t: kardinal;
s: string;
alustada
Iteratsiooni nr: = 0; // tulemuste loendur (tsükli number)

// Minu näites on niidi keha silmus, mis lõpeb
// või välise lõpetamistaotluse kaudu, mis on läbinud muutuja parameetri Peatatud,
// kas lihtsalt 5 silmust tehes
// Mulle on seda meeldivam kirjutada läbi "igavese" tsükli.

Kuigi True alustada

Inc (iteratsiooni nr); // järgmise tsükli number

LastRandom: = Juhuslik (1000); // võtmenumber – parameetrite voost vormi ülekandmise demonstreerimiseks

T: = juhuslik (5) +1; // aeg, milleks me magama jääme, kui me ei ole valmis

// Rumal töö (olenevalt sisendparameetrist)
kui mitte Param3, siis
Inc (Param2)
muidu
detsember (Param2);

// Moodustage vahetulemus
s: = vorming ("% s% 5d% s% d% d",
);

// Lisage tulemuste loendisse vahetulemus
ResultList.Add (s);

//// Näited vahetulemuse vormile edastamisest

//// Sünkroniseeritud meetodi läbimine – klassikaline viis
//// Puudused:
//// - sünkroonitav meetod on tavaliselt vooklassi meetod (juurdepääs
//// vooobjekti väljadele), kuid vormiväljadele juurdepääsuks peab see olema
//// "teadma" sellest ja selle väljadest (objektidest), millega tavaliselt väga hästi ei saa
//// vaatenurk programmi korraldusele.
//// - praegune lõim peatatakse kuni täitmise lõpuni
//// sünkroniseeritud meetod.

//// Eelised:
//// - standardne ja mitmekülgne
//// - sünkroniseeritud meetodil saate kasutada
//// kõik vooobjekti väljad.
// esmalt tuleb vajadusel ülekantud andmed sisse salvestada
// objektiobjekti eriväljad.
SyncDataN: = Iteratsiooni nr;
SyncDataS: = "Sünkrooni" + s;
// ja seejärel sünkroonitud meetodi kutse
Sünkrooni (SyncMetod1);

//// Saatmine sünkroonse sõnumite saatmisega (SendMessage)
//// sel juhul saab andmeid edastada nii sõnumi parameetrite kaudu (LastRandom),
//// ja läbi objekti väljade, edastades teate parameetris eksemplari aadressi
//// vooobjektist – täisarv (ise).
//// Puudused:
//// - niit peab tundma vormiakna käepidet
//// - nagu sünkroonimise puhul, peatatakse praegune lõim kuni
//// sõnumi töötlemise lõpetamine põhilõime poolt
//// - nõuab iga kõne jaoks märkimisväärsel hulgal protsessori aega
//// (lõimede vahetamiseks), seetõttu on väga sagedane helistamine ebasoovitav
//// Eelised:
//// - nagu ka sünkroonimise puhul, saate sõnumi töötlemisel kasutada
//// vooobjekti kõik väljad (muidugi, kui selle aadress edastati)


//// alusta lõime.
SendMessage (Form1.Handle, WM_USER_SendMessageMetod, Integer (Self), LastRandom);

//// Edastamine asünkroonse sõnumite saatmise kaudu (PostMessage)
//// Kuna antud juhul on selleks ajaks, kui põhilõime sõnumi kätte saab,
//// saatmisvoog võib olla juba lõppenud, edastades eksemplari aadressi
//// vooobjekt on kehtetu!
//// Puudused:
//// - niit peab tundma vormiakna käepidet;
//// - asünkroonsuse tõttu on andmeedastus võimalik ainult parameetrite kaudu
//// sõnumid, mis raskendab oluliselt andmete edastamist, millel on suurus
//// rohkem kui kaks masinsõna. Seda on mugav kasutada täisarvu vms edastamiseks.
//// Eelised:
//// - erinevalt eelmistest meetoditest, praegune lõime EI
//// peatatud, kuid jätkab kohe täitmist
//// - erinevalt sünkroniseeritud kõnest, sõnumite töötleja
//// on vormimeetod, mis peab teadma vooobjekti,
//// või ei tea voost üldse midagi, kui ainult andmeid edastatakse
//// sõnumi parameetrite kaudu. See tähendab, et niit ei pruugi vormist midagi teada.
//// üldiselt - ainult tema Handle, mida saab enne parameetrina edasi anda
//// alusta lõime.
PostMessage (Form1.Handle, WM_USER_PostMessageMetod, IterationNo, LastRandom);

//// Kontrollige võimalikku lõpetamist

// Kontrollige lõpetamist parameetri järgi
kui Peatatud, siis Break;

// Kontrollige aeg-ajalt valmimist
kui IteratsiooniNo> = 10, siis Break;

Uni (t * 1000); // Jääb t sekundiks magama
lõpp;
lõpp;

protseduur tMyThread.SyncMetod1;
alustada
// seda meetodit kutsutakse sünkroonimismeetodi kaudu.
// See tähendab, et hoolimata asjaolust, et see on tMyThreadi lõime meetod,
// see töötab rakenduse põhilõime kontekstis.
// Seetõttu saab ta teha kõike, hästi või peaaegu kõike :)
// Aga pidage meeles, siin ei tasu pikalt "jabada".

// Läbitud parameetrid saame välja võtta eriväljadest, kus need on
// salvestatud enne helistamist.
Vorm1.Silt1.Caption: = SyncDataS;

// kas teistelt vooobjekti väljadelt, näiteks kajastades selle hetkeseisu
Vorm1.Silt2.Caption: = Vorming ("% d% d",);
lõpp;

Üldiselt eelnes näitele minu järgmine arutluskäik sellel teemal ...

Esiteks:
Delphi mitmelõimelise programmeerimise KÕIGE OLULISEM reegel on:
Mittepeamise lõime kontekstis ei pääse te ligi vormide omadustele ja meetoditele ning tõepoolest kõigile komponentidele, mis "kasvavad" tWinControlist.

See tähendab (mõnevõrra lihtsustatult), et ei TThreadilt päritud Execute-meetodil ega ka muudes Execute-ist kutsutud meetodite/protseduuride/funktsioonide puhul, see on keelatud otse juurdepääs visuaalsete komponentide kõikidele omadustele ja meetoditele.

Kuidas seda õigesti teha.
Ühtseid retsepte pole. Täpsemalt, valikuid on nii palju ja erinevaid, et vastavalt konkreetsele juhtumile tuleb valida. Seetõttu viitavad nad artiklile. Olles seda lugenud ja sellest aru saanud, saab programmeerija aru ja kuidas seda konkreetsel juhul kõige paremini teha.

Lühidalt teie sõrmedel:

Enamasti saab mitme lõimega rakendus kas siis, kui on vaja teha mingit pikaajalist tööd või kui on võimalik korraga teha mitut asja, mis protsessorit tugevalt ei koorma.

Esimesel juhul viib põhilõime sees töö teostamine kasutajaliidese "aeglustumiseni" – töö tegemise ajal sõnumisilmust ei täideta. Selle tulemusena ei reageeri programm kasutaja toimingutele ja vormi ei joonistata näiteks pärast kasutaja teisaldamist.

Teisel juhul, kui töö hõlmab aktiivset vahetust välismaailmaga, siis sunnitud "seisaku" ajal. Andmete vastuvõtmist / saatmist oodates saate paralleelselt teha midagi muud, näiteks uuesti andmeid saata / vastu võtta.

On ka teisi juhtumeid, kuid harvem. Siiski pole see oluline. Nüüd ei räägita sellest.

Nüüd, kuidas see kõik kirjas on. Loomulikult vaadeldakse teatud kõige sagedasemat, mõnevõrra üldistatud juhtumit. Niisiis.

Eraldi lõimes tehtud tööl on üldiselt neli olemit (ma ei tea, kuidas seda täpsemalt nimetada):
1. Algandmed
2. Tegelikult töö ise (see võib sõltuda algandmetest)
3. Vaheandmed (näiteks teave töö hetkeseisu kohta)
4. Väljundandmed (tulemus)

Kõige sagedamini kasutatakse suurema osa andmete lugemiseks ja kuvamiseks visuaalseid komponente. Kuid nagu eespool mainitud, ei pääse te voost visuaalsetele komponentidele otse juurde. Kuidas olla?
Delphi arendajad soovitavad kasutada TThread klassi meetodit Synchronize. Siin ma ei kirjelda, kuidas seda kasutada - selle jaoks on ülalmainitud artikkel. Lubage mul lihtsalt öelda, et selle rakendamine, isegi õige, ei ole alati õigustatud. Probleeme on kaks:

Esiteks käivitatakse sünkroonimise kaudu kutsutud meetodi põhiosa alati põhilõime kontekstis ja seetõttu ei käivitata selle täitmise ajal akna sõnumisilmust. Seetõttu tuleb see kiiresti käivitada, vastasel juhul tekivad kõik samad probleemid, mis ühe lõimega teostuse puhul. Ideaalis tuleks sünkroonimise kaudu kutsutavat meetodit üldiselt kasutada ainult visuaalsete objektide omadustele ja meetoditele juurdepääsuks.

Teiseks on meetodi täitmine sünkroonimise kaudu "kallis" nauding, kuna lõimede vahel on vaja kahte lülitit.

Pealegi on mõlemad probleemid omavahel seotud ja tekitavad vastuolu: ühelt poolt tuleb esimese lahendamiseks "lihvida" sünkroonimise kaudu kutsutavad meetodid ja teisest küljest tuleb neid siis sagedamini kutsuda, kaotades hinnalise väärtuse. protsessori ressursid.

Seetõttu, nagu alati, on vaja läheneda mõistlikult ja erinevatel juhtudel kasutada erinevaid voo interaktsiooni viise välismaailmaga:

Esialgsed andmed
Kõik andmed, mis voogu edastatakse, ja selle töö ajal ei muutu, tuleb edastada juba enne selle käivitamist, s.t. voo loomisel. Nende kasutamiseks lõime kehas peate tegema neist kohaliku koopia (tavaliselt TThreadi järeltulija väljadel).
Kui on algandmeid, mis võivad lõime töötamise ajal muutuda, tuleb sellistele andmetele juurde pääseda kas sünkroonitud meetodite kaudu (meetodid, mida kutsutakse sünkroonimise kaudu) või lõimeobjekti väljade kaudu (TThreadi järeltulija). Viimane nõuab teatavat ettevaatust.

Vahe- ja väljundandmed
Siin on jällegi mitu võimalust (minu eelistuse järjekorras):
- Sõnumite asünkroonse saatmise meetod rakenduse põhiaknasse.
Tavaliselt kasutatakse seda protsessi edenemise kohta teadete saatmiseks rakenduse põhiaknasse koos väikese andmehulga (nt valmimisprotsent) edastamisega.
- Meetod sõnumite sünkroonseks saatmiseks rakenduse põhiaknasse.
Tavaliselt kasutatakse seda samadel eesmärkidel nagu asünkroonset saatmist, kuid võimaldab edastada suurema hulga andmeid ilma eraldi koopiat loomata.
- Võimaluse korral sünkroniseeritud meetodid, kombineerides võimalikult suure hulga andmete edastamise üheks meetodiks.
Saab kasutada ka andmete toomiseks vormilt.
- Läbi vooobjekti väljade, pakkudes üksteist välistavat juurdepääsu.
Lisateavet leiate artiklist.

Eh. Lühikest aega see ei õnnestunud