Multithreaded programma's met voorbeelden. Multi-threaded toepassingen voor. Threadbeheer in .NET

Clay Breshears

Invoering

De multithreading-implementatiemethoden van Intel omvatten vier hoofdfasen: analyse, ontwerp en implementatie, foutopsporing en prestatieafstemming. Dit is de aanpak die wordt gebruikt om een ​​multi-threaded applicatie te maken op basis van sequentiële code. Het werken met software tijdens de implementatie van de eerste, derde en vierde fase komt vrij breed aan bod, terwijl informatie over de implementatie van de tweede stap duidelijk niet voldoende is.

Er zijn veel boeken gepubliceerd over parallelle algoritmen en parallel computergebruik. Deze publicaties gaan echter vooral over het doorgeven van berichten, gedistribueerde geheugensystemen of theoretische parallelle computermodellen die soms niet toepasbaar zijn op real-life multi-core platforms. Als je klaar bent om serieus aan de slag te gaan met multithreaded programmeren, heb je waarschijnlijk kennis nodig over het ontwikkelen van algoritmen voor deze modellen. Uiteraard is het gebruik van deze modellen vrij beperkt, waardoor veel softwareontwikkelaars ze in de praktijk wellicht nog moeten implementeren.

Zonder overdrijving kunnen we zeggen dat de ontwikkeling van multi-threaded applicaties in de eerste plaats een creatieve activiteit is, en pas dan een wetenschappelijke activiteit. In dit artikel leert u acht eenvoudige regels waarmee u uw basis van parallelle programmeerpraktijken kunt uitbreiden en de efficiëntie van het implementeren van thread computing in uw toepassingen kunt verbeteren.

Regel 1. Markeer de bewerkingen die onafhankelijk van elkaar in de programmacode worden uitgevoerd

Parallelle verwerking is alleen van toepassing op sequentiële codebewerkingen die onafhankelijk van elkaar worden uitgevoerd. Een goed voorbeeld van hoe onafhankelijk van elkaar handelen tot één reëel resultaat leidt, is de bouw van een huis. Er zijn werknemers uit vele specialismen bij betrokken: timmerlieden, elektriciens, stukadoors, loodgieters, dakdekkers, schilders, metselaars, tuinarchitecten, enz. Natuurlijk kunnen sommigen van hen niet beginnen met werken voordat anderen klaar zijn met hun werk (dakdekkers beginnen bijvoorbeeld pas met werken als de muren zijn gebouwd, en schilders zullen die muren niet schilderen tenzij ze zijn gepleisterd). Maar over het algemeen kunnen we zeggen dat alle mensen die betrokken zijn bij de bouw onafhankelijk van elkaar handelen.

Laten we een ander voorbeeld bekijken: de werkcyclus van een dvd-verhuurwinkel, die bestellingen ontvangt voor bepaalde films. De bestellingen worden verdeeld onder de stationsmedewerkers, die in het magazijn naar deze films zoeken. Als een van de arbeiders een schijf uit het magazijn pakt waarop een film met Audrey Hepburn is opgenomen, zal dit uiteraard op geen enkele manier invloed hebben op een andere arbeider die op zoek is naar de volgende actiefilm met Arnold Schwarzenegger, en zeker niet op zijn collega die op zoek naar schijven met het nieuwe seizoen van Friends. In ons voorbeeld gaan we ervan uit dat alle problemen met niet-voorraad zijn opgelost voordat bestellingen op de verhuurlocatie aankomen, en dat het verpakken en verzenden van een bestelling geen invloed heeft op de verwerking van anderen.

In uw werk zult u waarschijnlijk berekeningen tegenkomen die alleen in een bepaalde volgorde kunnen worden uitgevoerd, en niet parallel, omdat de verschillende iteraties of stappen van de lus van elkaar afhankelijk zijn en in een strikte volgorde moeten worden uitgevoerd. Laten we een levend voorbeeld uit de natuur nemen. Stel je een zwanger hert voor. Omdat de draagtijd gemiddeld acht maanden duurt, zal het reekalf, hoe je het ook bekijkt, niet binnen een maand verschijnen, zelfs niet als acht herten tegelijkertijd zwanger worden. Acht rendieren tegelijk zouden echter uitstekend werk leveren als je ze allemaal aan de slee van de Kerstman vastmaakt.

Regel 2: Pas gelijktijdigheid toe op een laag granulariteitsniveau

Er zijn twee benaderingen voor het parallelle partitioneren van sequentiële programmacode: bottom-up en top-down. Ten eerste worden in de codeanalysefase codesegmenten (zogenaamde ‘hotspots’) geïdentificeerd, die een aanzienlijk deel van de uitvoeringstijd van het programma in beslag nemen. Het parallel scheiden van deze codesegmenten (indien mogelijk) levert de grootste prestatiewinst op.

De bottom-up benadering implementeert multi-threaded verwerking van codehotspots. Als parallelle partitionering van de gevonden punten niet mogelijk is, moet u de call-stack van de toepassing onderzoeken om andere segmenten te bepalen die beschikbaar zijn voor parallelle partitionering en die al een lange tijd actief zijn. Stel dat u werkt aan een toepassing die afbeeldingen comprimeert. Compressie kan worden geïmplementeerd met behulp van verschillende onafhankelijke parallelle threads die individuele beeldsegmenten verwerken. Maar zelfs als het je lukt om multi-threading hotspots te implementeren, mag je de analyse van de call-stack niet verwaarlozen, waardoor je segmenten kunt vinden die beschikbaar zijn voor parallelle verdeling, die zich op een hoger niveau van de programmacode bevinden. Op deze manier kunt u de granulariteit van parallelle verwerking vergroten.

Bij de top-downbenadering wordt het werk van de programmacode geanalyseerd en worden de afzonderlijke segmenten ervan geïdentificeerd, waarvan de uitvoering leidt tot de voltooiing van de hele taak. Als belangrijke codesegmenten niet duidelijk onafhankelijk zijn, analyseer dan hun samenstellende delen om te zoeken naar onafhankelijke berekeningen. Door uw code te analyseren, kunt u de codemodules identificeren die de meeste CPU-tijd nodig hebben om uit te voeren. Laten we eens kijken naar de implementatie van threading in een applicatie die is ontworpen voor videocodering. Parallelle verwerking kan worden geïmplementeerd op het laagste niveau - voor onafhankelijke pixels van één frame, of op een hoger niveau - voor groepen frames die onafhankelijk van andere groepen kunnen worden verwerkt. Als de applicatie wordt gemaakt om meerdere videobestanden tegelijkertijd te verwerken, kan parallelle verdeling op dit niveau nog eenvoudiger zijn en bevinden de details zich op het laagste niveau.

De granulariteit van parallelle berekeningen verwijst naar de hoeveelheid berekeningen die moet worden uitgevoerd vóór synchronisatie tussen threads. Met andere woorden: hoe minder vaak synchronisatie plaatsvindt, hoe lager het detailniveau. Threaded computing met een hoge granulariteit kan ervoor zorgen dat de systeemoverhead die gepaard gaat met het organiseren van threads de hoeveelheid nuttige berekeningen die door die threads worden uitgevoerd, overschrijdt. Het vergroten van het aantal threads met behoud van dezelfde hoeveelheid berekeningen compliceert het verwerkingsproces. Multithreading met lage granulariteit veroorzaakt minder systeemlatentie en heeft een groter potentieel voor schaalbaarheid, wat kan worden bereikt door extra threads te introduceren. Om parallelle verwerking met een lage granulariteit te implementeren, wordt aanbevolen een top-downbenadering te gebruiken en threads op een hoog niveau van de call-stack te organiseren.

Regel 3: Bouw schaalbaarheid in uw code in, zodat de prestaties ervan toenemen naarmate het aantal kernen toeneemt.

Nog niet zo lang geleden verschenen naast dual-coreprocessors ook quad-coreprocessors op de markt. Bovendien heeft Intel al de creatie aangekondigd van een processor met 80 cores die in staat is om een ​​biljoen floating point-bewerkingen per seconde uit te voeren. Omdat het aantal kernen in processors in de loop van de tijd alleen maar zal toenemen, moet uw code voldoende schaalbaarheidspotentieel hebben. Schaalbaarheid is een parameter waarmee men het vermogen van een applicatie kan beoordelen om adequaat te reageren op veranderingen zoals een toename van systeembronnen (aantal cores, geheugengrootte, busfrequentie, enz.) of een toename van het datavolume. Gezien het feit dat het aantal cores in toekomstige processors zal toenemen, schrijf schaalbare code die de prestaties zal verbeteren dankzij de toegenomen systeembronnen.

Om een ​​van de wetten van C. Northecote Parkinson te parafraseren kunnen we zeggen dat “gegevensverwerking alle beschikbare systeembronnen in beslag neemt.” Dit betekent dat naarmate de computerbronnen (zoals het aantal kernen) toenemen, ze waarschijnlijk allemaal zullen worden gebruikt voor gegevensverwerking. Laten we terugkeren naar de hierboven besproken videocompressietoepassing. Het is onwaarschijnlijk dat het verschijnen van extra processorkernen de grootte van verwerkte frames zal beïnvloeden - in plaats daarvan zal het aantal threads dat het frame verwerkt toenemen, wat zal leiden tot een afname van het aantal pixels per thread. Als gevolg hiervan zal, als gevolg van de organisatie van extra threads, de hoeveelheid overhead toenemen en zal de mate van parallellisme afnemen. Een ander, waarschijnlijker scenario zou een toename van de grootte of het aantal videobestanden zijn dat gecodeerd zou moeten worden. In dit geval kunt u door extra threads te organiseren die grotere (of extra) videobestanden verwerken, de volledige hoeveelheid werk direct verdelen in de fase waarin de toename plaatsvond. Een applicatie met dergelijke mogelijkheden heeft op zijn beurt een groot potentieel voor schaalbaarheid.

Het ontwerpen en implementeren van parallelle verwerking met behulp van data-decompositie biedt een grotere schaalbaarheid vergeleken met het gebruik van functionele decompositie. Het aantal onafhankelijke functies in de programmacode is meestal beperkt en verandert niet tijdens de uitvoering van de applicatie. Omdat aan elke onafhankelijke functie een afzonderlijke thread (en dienovereenkomstig een processorkern) wordt toegewezen, zullen extra georganiseerde threads bij een toename van het aantal kernen geen prestatieverbetering veroorzaken. Parallelle partitiemodellen met data-decompositie zullen dus een groter potentieel bieden voor de schaalbaarheid van applicaties vanwege het feit dat met een toename van het aantal processorkernen het volume van de verwerkte data zal toenemen.

Zelfs als de programmacode de verwerking van onafhankelijke functies in threads organiseert, is het waarschijnlijk dat er extra threads kunnen worden gebruikt die worden gelanceerd wanneer de invoerbelasting toeneemt. Laten we terugkeren naar het hierboven besproken voorbeeld van het bouwen van een huis. Het unieke doel van de constructie is om een ​​beperkt aantal onafhankelijke taken uit te voeren. Als u echter de opdracht krijgt om twee keer zoveel verdiepingen te bouwen, zult u waarschijnlijk voor bepaalde specialismen (schilders, dakdekkers, loodgieters, enz.) extra werknemers willen inhuren. Daarom moet u toepassingen ontwikkelen die zich kunnen aanpassen aan de gegevensontleding die optreedt als gevolg van de toegenomen werkdruk. Als uw code functionele decompositie implementeert, overweeg dan om extra threads te organiseren naarmate het aantal processorkernen toeneemt.

Regel 4: Gebruik thread-safe bibliotheken

Als u mogelijk een bibliotheek nodig heeft om gegevens op hotspots in uw code te verwerken, overweeg dan om vooraf gebouwde functies te gebruiken in plaats van uw eigen code. Kortom, probeer niet het wiel opnieuw uit te vinden door codesegmenten te ontwikkelen waarvan de functionaliteit al wordt geboden in geoptimaliseerde bibliotheekprocedures. Veel bibliotheken, waaronder de Intel® Math Kernel Library (Intel® MKL) en Intel® Integrated Performance Primitives (Intel® IPP), bevatten al multi-threaded functies die zijn geoptimaliseerd voor multi-core processors.

Het is vermeldenswaard dat wanneer u procedures uit multi-threaded bibliotheken gebruikt, u ervoor moet zorgen dat het aanroepen van een bepaalde bibliotheek de normale werking van de threads niet beïnvloedt. Dat wil zeggen dat als procedureaanroepen vanuit twee verschillende threads worden gedaan, elke aanroep de juiste resultaten moet opleveren. Als procedures toegang krijgen tot gedeelde bibliotheekvariabelen en deze bijwerken, kan er een ‘datarace’ ontstaan, die een nadelig effect zal hebben op de betrouwbaarheid van de berekeningsresultaten. Om correct met threads te kunnen werken, wordt de bibliotheekprocedure als nieuw toegevoegd (dat wil zeggen dat er niets anders wordt bijgewerkt dan lokale variabelen) of wordt deze gesynchroniseerd om de toegang tot gedeelde bronnen te beschermen. Conclusie: voordat u een bibliotheek van derden in uw programmacode gebruikt, moet u de bijbehorende documentatie lezen om er zeker van te zijn dat deze correct werkt met threads.

Regel 5: Gebruik een geschikt draadmodel

Laten we zeggen dat de functies van bibliotheken met meerdere threads duidelijk niet voldoende zijn om alle relevante codesegmenten parallel te splitsen, en dat je moet nadenken over het organiseren van threads. Haast je niet om je eigen (omslachtige) threadstructuur te creëren als de OpenMP-bibliotheek al alle functionaliteit bevat die je nodig hebt.

Het nadeel van expliciete multithreading is het onvermogen om threads nauwkeurig te controleren.

Als je alleen een parallelle scheiding van resource-intensieve lussen nodig hebt, of als de extra flexibiliteit die expliciete threads bieden voor jou van secundair belang is, dan heeft het in dit geval geen zin om extra werk te doen. Hoe complexer de implementatie van multithreading, hoe groter de kans op fouten in de code en hoe moeilijker de daaropvolgende wijziging ervan.

De OpenMP-bibliotheek is gericht op decompositie van gegevens en is vooral geschikt voor threaded verwerking van lussen die met grote hoeveelheden informatie werken. Ondanks het feit dat voor sommige toepassingen alleen decompositie van gegevens van toepassing is, is het noodzakelijk om rekening te houden met aanvullende vereisten (bijvoorbeeld een werkgever of klant), volgens welke het gebruik van OpenMP onaanvaardbaar is en het nog steeds nodig is om multithreading te implementeren met behulp van expliciete methoden . In dit geval kan OpenMP worden gebruikt voor pre-threading om potentiële prestatieverbeteringen, schaalbaarheid en de geschatte inspanning die nodig is om de code vervolgens te partitioneren met behulp van expliciete threading te schatten.

Regel 6. Het resultaat van de programmacode mag niet afhankelijk zijn van de uitvoeringsvolgorde van parallelle threads

Voor sequentiële code is het voldoende om eenvoudigweg een expressie te definiëren die na elke andere expressie wordt uitgevoerd. Bij code met meerdere threads is de volgorde van uitvoering van threads niet gedefinieerd en hangt deze af van de instructies van de planner van het besturingssysteem. Strikt genomen is het bijna onmogelijk om de volgorde van de threads te voorspellen die zullen worden gelanceerd om welke bewerking dan ook uit te voeren, of om te bepalen welke thread op een volgend moment door de planner zal worden gelanceerd. Voorspelling wordt voornamelijk gebruikt om de latentie van applicaties te verminderen, vooral wanneer deze wordt uitgevoerd op een platform met een processor die minder cores dan threads heeft. Als een thread wordt geblokkeerd omdat deze toegang moet krijgen tot een gebied dat niet naar de cache is geschreven of omdat deze een I/O-verzoek moet voltooien, zal de planner deze opschorten en een thread starten die klaar is om te worden uitgevoerd.

Een direct gevolg van de onzekerheid bij het plannen van threads zijn datarace-situaties. Ervan uitgaande dat één thread de waarde van een gedeelde variabele zal veranderen voordat een andere thread die waarde leest, kan het verkeerd zijn. Met een beetje geluk blijft de volgorde van uitvoering van threads voor een specifiek platform hetzelfde voor alle uitvoeringen van de applicatie. Kleine veranderingen in de toestand van het systeem (bijvoorbeeld de locatie van gegevens op de harde schijf, geheugensnelheid of zelfs een afwijking van de nominale AC-frequentie van de voeding) kunnen echter een andere volgorde van threaduitvoering veroorzaken. Voor programmacode die alleen correct werkt met een bepaalde reeks threads, zijn er dus waarschijnlijk problemen die verband houden met datarace-situaties en impasses.

Vanuit prestatieoogpunt verdient het de voorkeur om de volgorde waarin threads worden uitgevoerd niet te beperken. Een strikte volgorde van uitvoering van threads is alleen toegestaan ​​in gevallen van uiterste noodzaak, bepaald door een vooraf bepaald criterium. Als dergelijke omstandigheden zich voordoen, worden threads gelanceerd in de volgorde die is gespecificeerd door de meegeleverde synchronisatiemechanismen. Stel je bijvoorbeeld voor dat twee vrienden een krant lezen die op tafel ligt. Ten eerste kunnen ze met verschillende snelheden lezen, en ten tweede kunnen ze verschillende artikelen lezen. En hier maakt het niet uit wie de krant als eerste leest - hij zal in ieder geval op zijn vriend moeten wachten voordat hij de pagina omslaat. Tegelijkertijd zijn er geen beperkingen op het tijdstip of de volgorde van het lezen van artikelen - vrienden lezen op elke snelheid en synchronisatie tussen hen vindt onmiddellijk plaats bij het omslaan van de pagina.

Regel 7: Gebruik lokale streamopslag. Wijs indien nodig sloten toe aan individuele datagebieden

Synchronisatie verhoogt onvermijdelijk de belasting van het systeem, wat op geen enkele manier het proces van het verkrijgen van de resultaten van parallelle berekeningen versnelt, maar de juistheid ervan garandeert. Ja, synchronisatie is noodzakelijk, maar er mag geen misbruik van worden gemaakt. Om de synchronisatie te minimaliseren, wordt gebruik gemaakt van lokale threadopslag of toegewezen geheugengebieden (bijvoorbeeld array-elementen gemarkeerd met de identificatiegegevens van de overeenkomstige threads).

De noodzaak om tijdelijke variabelen tussen verschillende threads te delen komt vrij zelden voor. Dergelijke variabelen moeten lokaal aan elke thread worden gedeclareerd of toegewezen. Variabelen waarvan de waarden tussenresultaten zijn van de uitvoering van threads, moeten ook lokaal worden gedeclareerd voor de overeenkomstige threads. Synchronisatie zal nodig zijn om deze tussenresultaten samen te vatten in een gemeenschappelijk geheugengebied. Om de mogelijke belasting van het systeem tot een minimum te beperken, verdient het de voorkeur dit algemene gebied zo zelden mogelijk bij te werken. Expliciete multithreading-methoden bieden thread-lokale opslag-API's die de lokale gegevensintegriteit garanderen vanaf het begin van het ene multithreaded codesegment tot het volgende (of van de ene thread-functieaanroep tot de volgende uitvoering van diezelfde functie).

Als lokale opslag van threads niet mogelijk is, wordt de toegang tot gedeelde bronnen gesynchroniseerd met behulp van verschillende objecten, zoals vergrendelingen. Het is belangrijk om vergrendelingen correct toe te wijzen aan specifieke datablokken, wat het gemakkelijkst te doen is als het aantal vergrendelingen gelijk is aan het aantal datablokken. Eén enkel vergrendelingsmechanisme dat de toegang tot meerdere geheugenregio's synchroniseert, wordt alleen gebruikt als al deze regio's zich in hetzelfde kritieke gedeelte van de programmacode bevinden.

Wat moet u doen als u de toegang tot een grote hoeveelheid gegevens wilt synchroniseren, bijvoorbeeld een array die uit 10.000 elementen bestaat? Het organiseren van één enkele vergrendeling voor de gehele array zal waarschijnlijk een knelpunt in de toepassing creëren. Moeten we echt de vergrendeling voor elk element afzonderlijk organiseren? Zelfs als 32 of 64 parallelle threads toegang hebben tot de gegevens, zul je toegangsconflicten tot een vrij groot geheugengebied moeten voorkomen, en de kans dat dergelijke conflicten optreden is 1%. Gelukkig bestaat er een soort gulden middenweg, de zogenaamde ‘modulo locks’. Als N modulo-sloten worden gebruikt, synchroniseert elk slot de toegang tot het N-de deel van het totale gegevensgebied. Als er bijvoorbeeld twee van dergelijke vergrendelingen zijn georganiseerd, zal één ervan de toegang tot even array-elementen verhinderen, en de tweede de toegang tot oneven elementen. In dit geval bepalen threads, die toegang krijgen tot het vereiste element, de pariteit ervan en stellen ze de juiste vergrendeling in. Het aantal modulo locks wordt geselecteerd rekening houdend met het aantal threads en de waarschijnlijkheid van gelijktijdige toegang door meerdere threads tot hetzelfde geheugengebied.

Houd er rekening mee dat gelijktijdig gebruik van meerdere vergrendelingsmechanismen niet is toegestaan ​​om de toegang tot één geheugengebied te synchroniseren. Laten we de wet van Segal niet vergeten: “Iemand die één horloge heeft, weet precies hoe laat het is. Een man die een paar horloges heeft, is nergens zeker van.” Stel dat de toegang tot een variabele wordt geregeld door twee verschillende sloten. In dit geval kan het eerste slot worden gebruikt door één codesegment en het tweede door een ander segment. Vervolgens komen de threads die deze segmenten uitvoeren in een racesituatie terecht voor de gedeelde gegevens waartoe ze tegelijkertijd toegang hebben.

Regel 8. Wijzig indien nodig het software-algoritme om multithreading te implementeren

Het criterium voor het beoordelen van de prestaties van applicaties, zowel sequentieel als parallel, is de uitvoeringstijd. De asymptotische volgorde is geschikt als schatting van het algoritme. Met behulp van deze theoretische indicator is het bijna altijd mogelijk om de prestaties van een applicatie te evalueren. Dat wil zeggen dat, als alle overige omstandigheden gelijk blijven, een applicatie met een groeisnelheid van O(n log n) (snel sorteren) sneller zal werken dan een applicatie met een groeisnelheid van O(n2) (selectief sorteren), hoewel de resultaten van deze toepassingen zijn hetzelfde.

Hoe beter de asymptotische uitvoeringsvolgorde, hoe sneller de parallelle applicatie wordt uitgevoerd. Zelfs het meest productieve sequentiële algoritme kan echter niet altijd in parallelle threads worden verdeeld. Als een hotspot van een programma te moeilijk te splitsen is en er geen manier is om multithreading op een hoger niveau van de call-stack van de hotspot te implementeren, moet je eerst overwegen een ander sequentieel algoritme te gebruiken dat gemakkelijker te splitsen is dan het oorspronkelijke. Natuurlijk zijn er andere manieren om programmacode voor te bereiden voor threadverwerking.

Om de laatste bewering te illustreren, beschouwen we de vermenigvuldiging van twee vierkante matrices. Het algoritme van Strassen heeft een van de beste asymptotische uitvoeringsorders: O(n2.81), wat veel beter is dan de O(n3)-volgorde van het gewone drievoudige geneste lusalgoritme. Volgens het algoritme van Strassen wordt elke matrix verdeeld in vier submatrices, waarna zeven recursieve oproepen worden gedaan om n/2 × n/2 submatrices te vermenigvuldigen. Om recursieve oproepen te parallelliseren, kunt u een nieuwe thread maken die opeenvolgend zeven onafhankelijke submatrixvermenigvuldigingen uitvoert totdat ze een bepaalde grootte bereiken. In dit geval zal het aantal threads exponentieel toenemen, en zal de granulariteit van de berekeningen die door elke nieuw gevormde thread worden uitgevoerd toenemen naarmate de grootte van de submatrices afneemt. Laten we een andere optie overwegen: het organiseren van een pool van zeven threads die tegelijkertijd werken en één vermenigvuldiging van submatrices uitvoeren. Wanneer de threadpool klaar is met draaien, wordt de Strassen-methode recursief aangeroepen om de submatrices te vermenigvuldigen (zoals in de sequentiële versie van de code). Als het systeem waarop een dergelijk programma draait meer dan acht processorkernen heeft, zullen sommige daarvan inactief zijn.

Het mis veel gemakkelijker te parallelliseren met behulp van een drievoudige geneste lus. In dit geval wordt gegevensontleding gebruikt, waarbij de matrices worden verdeeld in rijen, kolommen of submatrices, en elke thread bepaalde berekeningen uitvoert. De implementatie van een dergelijk algoritme wordt uitgevoerd met behulp van OpenMP-pragma's die op een bepaald niveau van de lus zijn ingevoegd, of door expliciet threads te organiseren die matrixverdeling uitvoeren. Om dit eenvoudigere sequentiële algoritme te implementeren, zullen er veel minder wijzigingen aan de programmacode nodig zijn vergeleken met de implementatie van het multi-threaded Strassen-algoritme.

Dus nu ken je acht eenvoudige regels voor het effectief omzetten van sequentiële programmacode naar parallel. Door deze regels te volgen, creëert u veel sneller multi-threaded oplossingen met een grotere betrouwbaarheid, optimale prestaties en minder knelpunten.

Ga naar om terug te keren naar de webpagina met zelfstudies over Multithreaded Programming

Welk onderwerp roept de meeste vragen en moeilijkheden op voor beginners? Toen ik docent en Java-programmeur Alexander Pryakhin hierover vroeg, antwoordde hij meteen: “Multithreading.” Dank aan hem voor het idee en de hulp bij het voorbereiden van dit artikel!

We zullen kijken naar de innerlijke wereld van de applicatie en zijn processen, we zullen begrijpen wat de essentie van multithreading is, wanneer het nuttig is en hoe we het kunnen implementeren - met Java als voorbeeld. Als u een andere OOP-taal leert, hoeft u zich geen zorgen te maken: de basisprincipes zijn hetzelfde.

Over streams en hun bronnen

Om multithreading te begrijpen, moeten we eerst begrijpen wat een proces is. Een proces is een stukje virtueel geheugen en bronnen die het besturingssysteem toewijst om een ​​programma uit te voeren. Als u meerdere exemplaren van één toepassing opent, wijst het systeem voor elke toepassing een proces toe. In moderne browsers kan voor elk tabblad een afzonderlijk proces verantwoordelijk zijn.

U bent waarschijnlijk Windows "Taakbeheer" tegengekomen (in Linux is dit de "Systeemmonitor") en u weet dat onnodige lopende processen het systeem belasten, en dat de zwaarste daarvan vaak vastlopen, zodat ze met geweld moeten worden beëindigd.

Maar gebruikers houden van multitasken: geef ze geen brood, maar laat ze een tiental vensters openen en heen en weer springen. Er is een dilemma: u moet zorgen voor de gelijktijdige werking van applicaties en tegelijkertijd de belasting van het systeem verminderen, zodat het niet vertraagt. Laten we zeggen dat de hardware de behoeften van de eigenaren niet kan bijhouden - het probleem moet op softwareniveau worden opgelost.

We willen dat de processor meer opdrachten kan uitvoeren en meer gegevens per tijdseenheid kan verwerken. Dat wil zeggen dat we meer uitgevoerde code in elk tijdsdeel moeten passen. Beschouw een eenheid voor code-uitvoering als een object: dit is een thread.

Het is gemakkelijker om een ​​complexe taak aan te pakken als je deze in meerdere eenvoudige taken opsplitst. Hetzelfde geldt bij het werken met geheugen: een ‘zwaar’ proces wordt opgedeeld in threads die minder bronnen in beslag nemen en de code sneller aan de computer leveren (zie hieronder hoe precies).

Elke applicatie heeft minstens één proces, en elk proces heeft minstens één thread, die de master wordt genoemd en van waaruit indien nodig nieuwe worden gelanceerd.

Verschil tussen threads en processen

    Threads gebruiken geheugen dat is toegewezen aan een proces, en processen vereisen een aparte ruimte in het geheugen. Daarom worden threads sneller aangemaakt en beëindigd: het systeem hoeft er niet elke keer nieuwe adresruimte aan toe te wijzen en deze vervolgens vrij te geven.

    Verwerkt elk werk met zijn eigen gegevens - ze kunnen alleen iets uitwisselen via het mechanisme van interactie tussen processen. Threads hebben rechtstreeks toegang tot elkaars gegevens en bronnen: wat men verandert, is onmiddellijk voor iedereen beschikbaar. Een draad kan zijn ‘broeders’ in het proces controleren, terwijl een proces uitsluitend zijn ‘dochters’ controleert. Daardoor gaat het schakelen tussen streams sneller en is de communicatie daartussen eenvoudiger georganiseerd.

Wat is de conclusie? Als u een grote hoeveelheid gegevens zo snel mogelijk moet verwerken, verdeel deze dan in stukjes die door afzonderlijke threads kunnen worden verwerkt, en voeg vervolgens het resultaat samen. Dit is beter dan het creëren van processen die veel hulpbronnen vergen.

Maar waarom kiest zo'n populaire applicatie als Firefox ervoor om meerdere processen te creëren? Omdat het voor de browser is dat geïsoleerde bediening van tabbladen betrouwbaar en flexibel is. Als er iets mis is met één proces, is het niet nodig om het hele programma te beëindigen; het is mogelijk om ten minste een deel van de gegevens op te slaan.

Wat is multithreading

Nu komen we bij het belangrijkste. Bij multithreading wordt het applicatieproces opgedeeld in threads die parallel – in één tijdseenheid – door de processor worden verwerkt.

De rekenlast wordt verdeeld over twee of meer kernen, zodat de interface en andere programmacomponenten elkaar niet vertragen.

Multi-threaded applicaties kunnen ook worden uitgevoerd op single-core processors, maar dan worden de threads om de beurt uitgevoerd: de eerste werkte, de staat ervan werd opgeslagen - de tweede mocht werken, de threads werden opgeslagen - ze keerden terug naar de eerste of lanceerde de derde, enz.

Drukke mensen klagen dat ze maar twee handen hebben. Processen en programma's kunnen zoveel handen hebben als nodig is om een ​​taak zo snel mogelijk te voltooien.

Wacht op het signaal: synchronisatie in multi-threaded applicaties

Stel je voor dat meerdere threads tegelijkertijd hetzelfde gegevensgebied proberen te wijzigen. Wiens veranderingen zullen uiteindelijk worden geaccepteerd en wiens veranderingen zullen worden teruggedraaid? Om verwarring te voorkomen bij het werken met gedeelde bronnen, moeten threads hun acties coördineren. Om dit te doen, wisselen ze informatie uit met behulp van signalen. Elke thread vertelt de anderen wat er nu wordt gedaan en welke veranderingen ze kunnen verwachten. Op deze manier worden gegevens uit alle threads over de huidige staat van bronnen gesynchroniseerd.

Basissynchronisatietools

Wederzijdse uitsluiting (wederzijdse uitsluiting, afgekort als mutex) - een "vlag" die wordt doorgegeven aan de thread die momenteel het recht heeft om met gedeelde bronnen te werken. Voorkomt dat andere threads toegang krijgen tot het bezette geheugengebied. Er kunnen meerdere mutexen in een applicatie zijn, en deze kunnen tussen processen worden gedeeld. Er zit een addertje onder het gras: mutex dwingt de applicatie elke keer toegang te krijgen tot de kernel van het besturingssysteem, wat duur is.

Semafoor - hiermee kunt u het aantal threads beperken dat op een bepaald moment toegang heeft tot een bron. Dit vermindert de CPU-belasting bij het uitvoeren van code met knelpunten. Het probleem is dat het optimale aantal threads afhangt van de machine van de gebruiker.

Evenement - u definieert een voorwaarde bij het optreden waarvan de controle wordt overgedragen naar de gewenste thread. Threads wisselen gegevens uit over gebeurtenissen om elkaars acties te ontwikkelen en logisch voort te zetten. De één ontving de gegevens, de ander controleerde de juistheid ervan, de derde sloeg ze op de harde schijf op. Gebeurtenissen variëren in de manier waarop ze worden gesignaleerd. Als u meerdere threads over een gebeurtenis moet informeren, moet u de annuleringsfunctie handmatig instellen om het signaal te stoppen. Als er slechts één doelthread is, kunt u een evenement maken met automatische reset. Het stopt het signaal zelf nadat het de stroom heeft bereikt. Voor flexibele stroomcontrole kunnen gebeurtenissen in de wachtrij worden geplaatst.

Kritisch gedeelte - een complexer mechanisme dat een lusteller en een seinpaal combineert. Met de teller kunt u de start van de semafoor voor de gewenste tijd uitstellen. Het voordeel is dat de kernel alleen wordt gebruikt als de sectie bezet is en de semafoor moet worden ingeschakeld. De rest van de tijd draait de thread in de gebruikersmodus. Helaas kan de sectie slechts binnen één proces worden gebruikt.

Hoe multithreading in Java te implementeren

De klasse Thread is verantwoordelijk voor het werken met threads in Java. Als u een nieuwe thread wilt maken om een ​​taak uit te voeren, betekent dit dat u een exemplaar van de Thread-klasse maakt en deze aan de gewenste code koppelt. Dit kan op twee manieren:

    subklasse Draad;

    implementeer de Runnable-interface in uw klasse en geef vervolgens instanties van de klasse door aan de Thread-constructor.

Hoewel we het onderwerp van impassesituaties niet zullen bespreken, wanneer threads elkaars werk blokkeren en vastlopen, laten we dit over aan het volgende artikel. Laten we nu verder gaan met oefenen.

Voorbeeld van multithreading in Java: pingpong met mutexen

Als je denkt dat er iets vreselijks gaat gebeuren, adem dan uit. We zullen overwegen om met synchronisatieobjecten bijna in spelvorm te werken: twee threads worden overgedragen door mutex. Maar in wezen zul je een echte toepassing zien waarbij op een bepaald moment slechts één thread openbare gegevens kan verwerken.

Laten we eerst een klasse maken die de eigenschappen van Thread overneemt die al bij ons bekend zijn, en een "kickBall" -methode schrijven:

Openbare klasse PingPongThread breidt Thread( PingPongThread(String name)( this.setName(name); // overschrijf de threadnaam) @Override public void run() ( Ball ball = Ball.getBall(); while(ball.isInGame() ) ( kickBall(ball); ) ) private void kickBall(Ball ball) ( if(!ball.getSide().equals(getName()))( ball.kick(getName()); ) ) )

Laten we nu voor de bal zorgen. Die van ons zal niet eenvoudig zijn, maar gedenkwaardig: zodat hij kan zien wie hem heeft geraakt, van welke kant en hoe vaak. Om dit te doen, gebruiken we een mutex: deze verzamelt informatie over het werk van elke thread - hierdoor kunnen geïsoleerde threads met elkaar communiceren. Na de 15e treffer nemen wij de bal uit het spel om hem niet ernstig te verwonden.

Publieke klasse Ball ( private int kicks = 0; private statische Ball instance = new Ball(); private String side = ""; private Ball()() static Ball getBall())( return instance; ) gesynchroniseerde ongeldige kick(String spelernaam ) ( kicks++; side = spelersnaam; System.out.println(kicks + " " + side); ) String getSide())( return side; ) boolean isInGame())( return (kicks< 15); } }

En nu verschijnen er twee spelersthreads. Laten we ze zonder verder oponthoud Ping en Pong noemen:

Openbare klasse PingPongGame ( PingPongThread speler1 = nieuwe PingPongThread("Ping"); PingPongThread speler2 = nieuwe PingPongThread("Pong"); Ball ball; PingPongGame())( ball = Ball.getBall(); ) void startGame() gooit InterruptedException ( speler1 .start(); speler2.start();

“Het stadion zit vol met mensen, het is tijd om aan de wedstrijd te beginnen.” Laten we de opening van de bijeenkomst officieel aankondigen - in de hoofdklasse van de applicatie:

Openbare klasse PingPong ( public static void main(String args) gooit InterruptedException ( PingPongGame game = new PingPongGame(); game.startGame(); ) )

Zoals je kunt zien, is hier niets verbijsterends aan de hand. Dit is slechts een inleiding tot multithreading, maar je hebt al een idee van hoe het werkt en je kunt experimenteren - door de duur van het spel niet te beperken door het aantal hits, maar bijvoorbeeld door de tijd. We komen later terug op het onderwerp multithreading - we zullen kijken naar het java.util.concurrent-pakket, de Akka-bibliotheek en het vluchtige mechanisme. Laten we het ook hebben over het implementeren van multithreading in Python.

Andrej Kolesov

Wanneer we beginnen na te denken over de principes van het maken van multi-threaded applicaties voor het Microsoft .NET Framework, zullen we onmiddellijk een voorbehoud maken: hoewel alle voorbeelden worden gegeven in Visual Basic .NET, is de methodologie voor het maken van dergelijke programma's over het algemeen voor iedereen hetzelfde. programmeertalen die .NET ondersteunen, inclusief C#. VB werd gekozen om de technologie voor het maken van multi-threaded applicaties te demonstreren, voornamelijk omdat eerdere versies van deze tool een dergelijke functie niet boden.

Let op: Visual Basic .NET kan DIT ook!

Zoals u weet heeft Visual Basic (tot en met versie 6.0) nooit eerder de creatie van multi-threaded softwarecomponenten (EXE, ActiveX DLL en OCX) toegestaan. Hier moet u onthouden dat de COM-architectuur drie verschillende threadingmodellen omvat: single threaded (Single Thread), gedeeld (Single Threaded Apartment, STA) en gratis (Multi-Threaded Apartment). Met VB 6.0 kunt u programma's van de eerste twee typen maken. De STA-optie biedt een pseudo-multi-threaded modus - verschillende threads werken feitelijk parallel, maar de programmacode van elk van hen is beschermd tegen toegang van buitenaf (threads kunnen met name geen gedeelde bronnen gebruiken).

Visual Basic .NET kan nu native multithreading implementeren. Om precies te zijn: in .NET wordt deze modus ondersteund op het niveau van gemeenschappelijke klassenbibliotheken, de klassenbibliotheek en de Common Language Runtime. Als gevolg hiervan heeft VB.NET toegang tot deze mogelijkheden, samen met andere .NET-programmeertalen.

Hoewel de gemeenschap van VB-ontwikkelaars op een gegeven moment ontevredenheid uitte over veel toekomstige innovaties van deze taal, reageerde ze met grote instemming op het nieuws dat het met de nieuwe versie van de tool mogelijk zou zijn om programma's met meerdere threads te maken (zie "Wachten op Visual Studio .NET", "BYTE /Rusland" nr. 1/2001). Veel experts hebben echter een terughoudender oordeel over deze innovatie uitgesproken. Hier is bijvoorbeeld de mening van Dan Appleman, een beroemde ontwikkelaar en auteur van talloze boeken voor VB-programmeurs: “Multithreading in VB.NET maakt me meer bang dan welke andere innovatie dan ook, en, zoals met veel nieuwe .NET-technologieën, is het is eerder te wijten aan menselijke dan aan technologische factoren... Ik ben bang voor multithreading in VB.NET omdat VB-programmeurs over het algemeen geen ervaring hebben met het ontwerpen en debuggen van multithreaded applicaties."

Net als andere programmeertools op laag niveau (bijvoorbeeld dezelfde Win API's) biedt gratis multithreading enerzijds grotere mogelijkheden voor het creëren van hoogwaardige schaalbare oplossingen, en stelt het anderzijds hogere eisen aan de kwalificaties van ontwikkelaars. Bovendien wordt het probleem hier verergerd door het feit dat het zoeken naar fouten in een multi-threaded applicatie erg moeilijk is, omdat ze meestal willekeurig verschijnen, als resultaat van een specifieke kruising van parallelle computerprocessen (het is vaak simpelweg onmogelijk om te reproduceren dergelijke situatie opnieuw). Dat is de reden waarom methoden van traditioneel debuggen van programma's in de vorm van het opnieuw uitvoeren ervan in dit geval meestal niet helpen. En de enige manier om multithreading veilig te gebruiken is een hoogwaardig applicatieontwerp dat voldoet aan alle klassieke principes van “correct programmeren”.

Het probleem met VB-programmeurs is dat, hoewel velen van hen zeer ervaren professionals zijn en zich terdege bewust zijn van de valkuilen van multithreading, het gebruik van VB6 hun waakzaamheid zou kunnen afstompen. Als we VB de schuld geven van zijn beperkingen, vergeten we soms dat veel van de beperkingen werden bepaald door de verbeterde beveiligingsfuncties van deze tool, die ontwikkelaarsfouten voorkomen of elimineren. VB6 maakt bijvoorbeeld automatisch voor elke thread een aparte kopie van alle globale variabelen, waardoor mogelijke conflicten daartussen worden voorkomen. In VB.NET worden dergelijke problemen volledig op de schouders van de programmeur gelegd. We moeten ook niet vergeten dat het gebruik van een multi-threaded model in plaats van een single-threaded model niet altijd leidt tot betere programmaprestaties en zelfs tot een afname ervan (zelfs op systemen met meerdere processors!).

Alles wat hierboven is gezegd, moet echter niet worden beschouwd als een advies om niet met multithreading te knoeien. Je hoeft alleen maar een goed idee te hebben van wanneer dergelijke modi moeten worden gebruikt, en te begrijpen dat een krachtigere ontwikkeltool altijd hogere eisen stelt aan de kwalificaties van de programmeur.

Parallelle verwerking in VB6

Natuurlijk was het mogelijk om pseudo-parallelle gegevensverwerking te organiseren met behulp van VB6, maar deze mogelijkheden waren zeer beperkt. Een aantal jaren geleden moest ik bijvoorbeeld een procedure schrijven die de uitvoering van een programma gedurende een bepaald aantal seconden pauzeert (de bijbehorende SLEEP-instructie was in kant-en-klare vorm aanwezig in Microsoft Basic/DOS). Het is niet moeilijk om het zelf te implementeren in de vorm van de volgende eenvoudige subroutine:

U kunt de functionaliteit ervan eenvoudig verifiëren door bijvoorbeeld de volgende code te gebruiken voor het verwerken van een knopklik op het formulier:

Om dit probleem in VB6 op te lossen, moet u binnen de Do...Loop van de SleepVB-procedure de aanroep van de DoEvents-functie verwijderen, die de controle overdraagt ​​aan het besturingssysteem en het aantal open formulieren in deze VB-toepassing retourneert. Maar houd er rekening mee dat het weergeven van een venster met het bericht "Hallo weer!" op zijn beurt de uitvoering van de hele applicatie blokkeert, inclusief de SleepVB-procedure.

Door globale variabelen als vlaggen te gebruiken, kunt u er ook voor zorgen dat de lopende SleepVB-procedure abnormaal eindigt. Het is op zijn beurt het eenvoudigste voorbeeld van een computerproces dat de processorbronnen volledig in beslag neemt. Maar als je wat nuttige berekeningen gaat uitvoeren (en niet in een lege lus rondloopt), moet je er rekening mee houden dat het aanroepen van de DoEvent-functie behoorlijk wat tijd kost, dus dit moet met vrij grote tussenpozen gebeuren. .

Om de beperkingen van de parallelle computerondersteuning van VB6 te zien, vervangt u de aanroep van de DoEvents-functie door een labeluitvoer:

Label1.Caption = Timer

In dit geval zal niet alleen de Command2-knop niet werken, maar zelfs binnen 5 seconden zal de inhoud van het label niet veranderen.

Voeg voor een ander experiment een wait call toe aan de code voor Command2 (je kunt dit doen omdat de SleepVB-procedure reentrant is):

Private Sub Command2_Click() Bel SleepVB(5) MsgBox "Hallo weer!" Einde sub

Start vervolgens de applicatie en klik op Command1 en na 2-3 seconden - Command2. Het bericht "Nog een hallo" verschijnt als eerste, hoewel het bijbehorende proces later is gestart. De reden hiervoor is dat de DoEvents-functie alleen controleert op gebeurtenissen met visuele elementen, en niet op de aanwezigheid van andere verwerkingsthreads. Bovendien draait de VB-applicatie feitelijk op één enkele thread, zodat de controle terugkeert naar de gebeurtenisprocedure die het laatst is geactiveerd.

Threadbeheer in .NET

Multithreaded .NET-toepassingen worden gebouwd met behulp van een groep .NET Framework-basisklassen die worden beschreven door de System.Threading-naamruimte. In dit geval behoort de sleutelrol tot de klasse Thread, met behulp waarvan bijna alle threadbeheerbewerkingen worden uitgevoerd. Vanaf dit punt geldt alles wat gezegd wordt over het werken met threads voor alle programmeertools in .NET, inclusief C#.

Laten we voor de eerste kennismaking met het maken van parallelle threads een Windows-applicatie maken met een formulier waarop we de knoppen ButtonStart en ButtonAbort plaatsen en de volgende code schrijven:

Ik zou onmiddellijk uw aandacht willen vestigen op drie punten. Ten eerste worden de trefwoorden Imports gebruikt om te verwijzen naar de verkorte namen van de klassen die hier door de naamruimte worden beschreven. Ik heb specifiek een ander gebruiksscenario voor Imports opgenomen om een ​​steno-equivalent van een lange naamruimtenaam (VB = Microsoft.VisualBasic) te beschrijven die op programmatekst kan worden toegepast. In dit geval kunt u direct zien tot welke naamruimte het Timer-object behoort.

Ten tweede heb ik de logische haakjes #Region gebruikt om de code die ik heb geschreven visueel te scheiden van de code die de formulierontwerper automatisch heeft gegenereerd (de laatste wordt hier niet weergegeven).

Ten derde zijn beschrijvingen van de invoerparameters van gebeurtenisprocedures speciaal verwijderd (dit zal in de toekomst soms worden gedaan) om niet te worden afgeleid door dingen die in dit geval niet belangrijk zijn.

Start de applicatie en klik op de ButtonStart-knop. Het wachtproces is begonnen in een cyclus gedurende een bepaald tijdsinterval, en in dit geval (in tegenstelling tot het voorbeeld met VB6) - in een onafhankelijke thread. Dit is eenvoudig te verifiëren: alle visuele elementen van het formulier zijn toegankelijk. Door bijvoorbeeld op de ButtonAbort-knop te klikken, kunt u het proces abnormaal beëindigen met behulp van de Abort-methode (maar het sluiten van het formulier met de Systeemknop sluiten zal de procedure niet onderbreken!). Om de dynamiek van het proces te visualiseren, kunt u een label op het formulier plaatsen en een uitvoer van de huidige tijd toevoegen aan de slaaplus van de SleepVBNET-procedure:

Label1.Text = _ "Huidige tijd = " & VB.TimeOfDay

De SleepVBNET-procedure (die in dit geval al een methode van een nieuw object is) blijft uitvoeren, zelfs als u een berichtvenster aan de ButtonStart-code toevoegt om een ​​berichtvenster weer te geven dat het begin van de berekeningen aangeeft nadat de thread is gestart (Afbeelding 1) .

Een complexere optie is een stream als klasse

Om verdere experimenten met threads uit te voeren, gaan we een nieuwe VB-applicatie van het type Console maken, bestaande uit een reguliere codemodule met een Main-procedure (die begint met uitvoeren wanneer de applicatie start) en een WorkerThreadClass-klassemodule:

Laten we de gemaakte applicatie starten. Er verschijnt een consolevenster waarin een doorlopende reeks tekens zichtbaar is, die het model van het lopende computerproces demonstreert (WorkerThread). Vervolgens verschijnt het berichtenvenster dat wordt uitgegeven door het oproepproces (Hoofd), en ten slotte zien we de afbeelding in Fig. 2 (als u niet tevreden bent met de uitvoeringssnelheid van het gesimuleerde proces, verwijder dan enkele rekenkundige bewerkingen met de variabele “a” in de WorkerThread-procedure of voeg deze toe).

Let op: het berichtvenster "Eerste thread gestart" werd met een merkbare vertraging weergegeven nadat het WorkerThread-proces was gestart (in het geval van het formulier dat in de vorige paragraaf is beschreven, zou een dergelijk bericht vrijwel onmiddellijk zijn verschenen nadat op de ButtonStart-knop was gedrukt).

Hoogstwaarschijnlijk gebeurt dit omdat bij het werken met een formulier gebeurtenisprocedures een hogere prioriteit hebben dan het proces dat wordt gestart. In het geval van een consoleapplicatie hebben alle procedures dezelfde prioriteit. We zullen de kwestie van prioriteiten later bespreken, maar voor nu zullen we de hoogste prioriteit instellen voor de oproepende thread (hoofd):

Thread.CurrentThread.Priority = _ ThreadPriority.Highest Thread1.Start()

Nu verschijnt het venster vrijwel onmiddellijk. Zoals u kunt zien, zijn er twee manieren om instanties van een Thread-object te maken. In eerste instantie gebruikten we de eerste - we maakten een nieuw object (thread) Thread1 en werkten ermee. De tweede optie is om het Thread-object voor de momenteel actieve thread te verkrijgen met behulp van de statische CurrentThread-methode. Dit is hoe de hoofdprocedure zichzelf een hogere prioriteit heeft gegeven, maar dit had dit bijvoorbeeld voor elke andere thread kunnen doen:

Thread1.Priority = ThreadPriority.Laagste Thread1.Start()

Om de mogelijkheden van het beheren van een lopend proces te tonen, voegt u de volgende coderegels toe aan het einde van de hoofdprocedure:

Voer nu de applicatie uit terwijl u enkele muisbewerkingen uitvoert (hopelijk heeft u het juiste latentieniveau in de WorkerThread ingesteld, zodat deze niet te snel, maar ook niet te langzaam is).

Eerst wordt "Proces 1" gestart in het consolevenster en verschijnt het bericht "Eerste thread actief". "Proces 1" wordt uitgevoerd en u klikt snel op OK in het berichtvenster.

Vervolgens - “Proces 1” gaat verder, maar na twee seconden verschijnt het bericht “Thread opgeschort”. "Proces 1" bevroor. Klik op "OK" in het berichtvenster: "Proces 1" vervolgde de uitvoering en werd met succes voltooid.

In dit fragment hebben we de slaapmethode gebruikt om het huidige proces op te schorten. Opmerking: Slaap is een statische methode en kan alleen worden toegepast op het huidige proces, niet op enig exemplaar van het Thread-object. Met de taalsyntaxis kunt u Thread1.Sleep of Thread.Sleep schrijven, maar in dit geval wordt het object CurrentThread nog steeds gebruikt.

Een ander interessant gebruiksscenario voor Sleep is de waarde Timeout.Infinite. In dit geval wordt de thread voor onbepaalde tijd opgeschort totdat de status wordt onderbroken door een andere thread met behulp van de Thread.Interrupt-methode.

Om een ​​externe thread van een andere thread te onderbreken zonder deze te stoppen, moet je de methodeaanroep Thread.Suspend gebruiken. Vervolgens kunt u de uitvoering ervan voortzetten met behulp van de Thread.Resume-methode, wat we in de bovenstaande code hebben gedaan.

Iets over threadsynchronisatie

Threadsynchronisatie is een van de grootste uitdagingen bij het schrijven van multi-threaded applicaties, en de System.Threading-ruimte beschikt over een groot aantal tools om dit op te lossen. Maar nu zullen we alleen kennis maken met de Thread.Join-methode, waarmee u het einde van de uitvoering van de thread kunt volgen. Om te zien hoe het werkt, vervangt u de laatste regels van de hoofdprocedure door deze code:

Procesprioriteitbeheer

De verdeling van processortijdplakken tussen threads wordt uitgevoerd met behulp van prioriteiten, die worden opgegeven als de eigenschap Thread.Priority. Threads die tijdens runtime zijn gemaakt, kunnen worden ingesteld op vijf waarden: Highest, AboveNormal, Normal (standaard), BelowNormal en Lowest. Om te zien hoe prioriteiten de uitvoeringssnelheid van threads beïnvloeden, schrijven we de volgende code voor de hoofdprocedure:

Sub Main() " beschrijving van het eerste proces Dim Thread1 As Thread Dim oWorker1 As New WorkerThreadClass() Thread1 = New Thread(AddressOf _ oWorker1.WorkerThread) " Thread1.Priority = _ " ThreadPriority.BelowNormal " we geven de initiële gegevens door: oWorker1 .Start = 1 oWorker1.Finish = 10 oWorker1.ThreadName = "Tel 1" oWorker1.SymThread = "."

" beschrijving van het tweede proces Dim Thread2 As Thread Dim oWorker2 As New WorkerThreadClass() Thread2 = New Thread(AddressOf _ oWorker2.WorkerThread) " waarbij de initiële gegevens worden doorgegeven: oWorker2.Start = 11 oWorker2.Finish = 20 oWorker2.ThreadName = "Count 2" oWorker2 .SymThread = "*" " " voer de race uit Thread.CurrentThread.Priority = _ ThreadPriority.Highest Thread1.Start() Thread2.Start() " Wachten op de voltooiing van de processen Thread1.Join() Thread2.Join () MsgBox("Beide processen zijn voltooid ") End Sub

Laten we, voordat we met de eerste thread beginnen, de prioriteit ervan een niveau lager instellen:

Thread1.Priority = _ ThreadPriority.BelowNormal

Het beeld veranderde dramatisch: de tweede stroom nam bijna alle tijd in beslag vanaf de eerste (Fig. 4).

Let ook op het gebruik van de Join-methode. Met zijn hulp voeren we een vrij gebruikelijke variant van threadsynchronisatie uit, waarbij het hoofdprogramma wacht op de voltooiing van verschillende parallelle computerprocessen.

Conclusie

We hebben alleen de basisbeginselen van het ontwikkelen van multi-threaded .NET-applicaties besproken. Een van de meest complexe en praktisch relevante kwesties is threadsynchronisatie. Naast het gebruik van het Thread-object dat in dit artikel wordt beschreven (het heeft veel methoden en eigenschappen die we hier niet hebben besproken), spelen de klassen Monitor en Mutex, evenals de instructies lock (C#) en SyncLock (VB.NET), een zeer belangrijke rol bij draadbeheer.

Een meer gedetailleerde beschrijving van deze technologie wordt gegeven in afzonderlijke hoofdstukken van de boeken, waaruit ik een paar citaten zou willen geven (waarmee ik het volledig eens ben) als een zeer korte samenvatting van het onderwerp "Multithreading in .NET".

"Als je een beginner bent, zul je misschien verbaasd zijn om te ontdekken dat de overhead die gepaard gaat met het maken en verzenden van threads ervoor kan zorgen dat een single-threaded applicatie sneller draait... Probeer dus altijd zowel single-threaded als multi-threaded prototypes te testen van het programma.”

"Je moet multithreading zorgvuldig ontwerpen en de toegang tot gedeelde objecten en variabelen strak controleren."

"Multithreading mag niet als de standaardaanpak worden beschouwd."

"Ik vroeg een publiek van ervaren VB-programmeurs of ze gratis multi-threading wilden krijgen in een toekomstige versie van VB. Bijna iedereen stak zijn hand op. Vervolgens vroeg ik wie wist waar ze aan begonnen. Slechts een paar mensen staken hun hand op deze keer." en er was een veelbetekenende glimlach op hun gezichten."

"Als je je niet laat intimideren door de uitdagingen van het ontwerpen van multithreaded applicaties, kan multithreading, mits correct gebruikt, de applicatieprestaties aanzienlijk verbeteren."

Ik zou hieraan willen toevoegen dat de technologie voor het maken van multi-threaded .NET-applicaties (zoals veel andere .NET-technologieën) in het algemeen vrijwel onafhankelijk is van de gebruikte taal. Daarom raad ik ontwikkelaars aan om verschillende boeken en artikelen te bestuderen, ongeacht de programmeertaal die ze kiezen om een ​​bepaalde technologie te demonstreren.

Literatuur:

  1. Dan Appelman. Overgang naar VB.NET: strategieën, concepten, code/Trans. uit het Engels
  2. - Sint-Petersburg: "Peter", 2002, - 464 pp.: ill.

Tom Boogschutter. C#-basisprincipes. Nieuwste technologieën/Trans. uit het Engels - M.: Uitgeverij en handelshuis "Russische editie", 2001. - 448 p.: ill.

Multitasken en multithreaden

Laten we beginnen met deze eenvoudige verklaring: 32-bits Windows-besturingssystemen ondersteunen multitasking (multi-process) en multi-threaded gegevensverwerkingsmodi. Over hoe goed ze het doen valt te discussiëren, maar dat is een andere vraag.

Multitasking is een werkwijze waarbij een computer meerdere taken tegelijkertijd en parallel kan uitvoeren. Het is duidelijk dat als een computer één processor heeft, we het hebben over pseudo-parallelisme, wanneer het besturingssysteem volgens sommige regels snel kan schakelen tussen verschillende taken.

Een taak is een programma of onderdeel van een programma (applicatie) dat een logische actie uitvoert en een eenheid is waarvoor het besturingssysteem bronnen toewijst.

Wat is een stroom? Een thread is een autonoom computerproces, maar wordt niet toegewezen op besturingssysteemniveau, maar binnen een taak. Het fundamentele verschil tussen een thread en een “taakproces” is dat alle threads van een taak worden uitgevoerd in een enkele adresruimte, dat wil zeggen dat ze kunnen werken met gedeelde geheugenbronnen. Dit is precies waar hun voordelen (parallelle gegevensverwerking) en nadelen (bedreiging voor de betrouwbaarheid van het programma) liggen. Hierbij moet in gedachten worden gehouden dat in het geval van multitasking het besturingssysteem primair verantwoordelijk is voor het beschermen van applicaties, en wanneer multithreading wordt gebruikt, de ontwikkelaar zelf verantwoordelijk is.

Merk op dat het gebruik van de multitasking-modus in systemen met één processor u in staat stelt de algehele prestaties van het multitasking-systeem als geheel te verbeteren (hoewel niet altijd, aangezien naarmate het aantal schakelingen toeneemt, het aandeel van de bronnen dat door het besturingssysteem wordt ingenomen, toeneemt). Maar de tijd die nodig is om een ​​specifieke taak te voltooien, neemt altijd toe, ook al is het maar in geringe mate, vanwege het extra werk van het besturingssysteem.

Als de processor zwaar wordt belast met taken (met minimale I/O-downtime, bijvoorbeeld bij het oplossen van puur wiskundige problemen), worden echte algehele prestatieverbeteringen alleen bereikt bij gebruik van multiprocessorsystemen. Dergelijke systemen maken verschillende modellen van parallellisatie mogelijk - op taakniveau (elke taak kan slechts één processor in beslag nemen, terwijl threads alleen pseudo-parallel worden uitgevoerd) of op threadniveau (wanneer één taak meerdere processors met zijn threads kan bezetten).

Hier kunnen we ons ook herinneren dat bij het gebruik van krachtige gedeelde computersystemen, waarvan de grondlegger eind jaren zestig de IBM System/360-familie was, een van de meest urgente taken de selectie was van de optimale optie voor het beheren van multitasking - ook in de dynamische modus. , rekening houdend met verschillende parameters. Kortom, het beheren van multitasking is een functie van het besturingssysteem. Maar de effectiviteit van het implementeren van een of andere optie houdt rechtstreeks verband met de kenmerken van de computerarchitectuur als geheel, en vooral van de processor. Hetzelfde krachtige IBM System/360 werkte bijvoorbeeld perfect in gedeelde systemen voor zakelijke taken, maar was tegelijkertijd volkomen ongeschikt voor het oplossen van ‘real-time’ klassenproblemen. Op dit gebied waren aanzienlijk goedkopere en eenvoudigere minicomputers zoals de DEC PDP 11/20 destijds duidelijk toonaangevend.

Een voorbeeld van het bouwen van een eenvoudige multi-threaded applicatie.

Geboren over de reden voor het grote aantal vragen over het bouwen van multi-threaded applicaties in Delphi.

Het doel van dit voorbeeld is om te laten zien hoe je op de juiste manier een multi-threaded applicatie bouwt, waarbij langdurig werk naar een aparte thread wordt verplaatst. En hoe je in een dergelijke toepassing de interactie tussen de hoofdthread en de werkthread kunt garanderen om gegevens van het formulier (visuele componenten) naar de thread en terug over te dragen.

Het voorbeeld beweert niet volledig te zijn; het demonstreert alleen de eenvoudigste manieren van interactie tussen threads. De gebruiker in staat stellen om “snel een correct werkende multi-threaded applicatie te creëren” (wie weet hoeveel ik hier niet van houd).
Alles wordt gedetailleerd becommentarieerd (naar mijn mening), maar als je vragen hebt, stel ze dan.
Maar ik waarschuw je nogmaals: Stromen zijn niet eenvoudig. Als je geen idee hebt hoe het allemaal werkt, dan is er een groot gevaar dat vaak alles goed voor je zal werken, en soms zal het programma zich meer dan vreemd gedragen. Het gedrag van een verkeerd geschreven multi-threaded programma hangt in grote mate af van een groot aantal factoren die soms onmogelijk te reproduceren zijn tijdens het debuggen.

Een voorbeeld dus. Voor het gemak heb ik de code toegevoegd en een archief met de module- en formuliercode bijgevoegd

eenheid ExThreadForm;

gebruikt
Windows, berichten, SysUtils, varianten, klassen, afbeeldingen, besturingselementen, formulieren,
Dialogen, StdCtrls;

// constanten die worden gebruikt bij het overbrengen van gegevens van een stream naar een formulier met behulp van
// vensterberichten verzenden
const
WM_USER_SendMessageMetod = WM_USER+10;
WM_USER_PostMessageMetod = WM_USER+11;

type
// beschrijving van de threadklasse, een afstammeling van tThread
tMijnThread = klasse(tThread)
privé
SyncDataN: geheel getal;
SyncDataS:String;
procedure SyncMetod1;
beschermd
procedure Uitvoeren; overschrijven;
publiek
Param1:String;
Param2:Geheel getal;
Param3:Boolean;
Gestopt: Booleaans;
Laatste willekeurige: geheel getal;
Iteratienr: geheel getal;
ResultaatLijst:tStringLijst;

Constructor Create(aParam1:String);
vernietiger Vernietigen; overschrijven;
einde;

// beschrijving van de klasse van het formulier dat de stream gebruikt
TForm1 = klasse(TForm)
Label1: T-label;
Memo1: TMemo;
btnStart: TKnop;
btnStop: T-knop;
Bewerken1: TBewerken;
Bewerken2: TBewerken;
CheckBox1: TCheckBox;
Label2: T-label;
Label3: TLabel;
Label4: T-label;
procedure btnStartClick(Afzender: TObject);
procedure btnStopClick(Afzender: TObject);
privé
(Privéaangiften)
MijnThread:tMijnThread;
procedure EventMyThreadOnTerminate(Afzender:tObject);
procedure EventOnSendMessageMetod (var Msg: TMessage); bericht WM_USER_SendMessageMetod;
procedure EventOnPostMessageMetod(var Msg: TMessage); bericht WM_USER_PostMessageMetod;

Openbaar
(Openbare verklaringen)
einde;

var
Formulier1: TForm1;

{
Gestopt - demonstreert de overdracht van gegevens van een formulier naar een thread.
Vereist geen extra synchronisatie omdat het eenvoudig is
type met één woord, en is geschreven door slechts één thread.
}

procedure TForm1.btnStartClick(Afzender: TObject);
beginnen
Willekeurig(); // zorgen voor willekeur in de reeks met behulp van Random() - heeft niets te maken met de stroom

// Maak een instantie van een threadobject en geef er een invoerparameter aan
{
AANDACHT!
De threadconstructor is zo geschreven dat er een thread ontstaat
opgeschort omdat het het volgende mogelijk maakt:
1. Bepaal het moment van lancering. Dit is bijna altijd handiger, omdat...
Hiermee kunt u de stream configureren zelfs voordat deze wordt gestart, door deze invoer door te geven
parameters, enz.
2. Omdat Er wordt dan een link naar het aangemaakte object in het formulierveld opgeslagen
na de zelfvernietiging van de draad (zie hieronder) die optreedt wanneer de draad loopt
op elk moment kan optreden, wordt deze link ongeldig.
}
MijnThread:= tMijnThread.Create(Form1.Edit1.Text);

// Omdat de thread is gemaakt, worden eventuele fouten echter opgeschort
// tijdens de initialisatie (vóór de lancering) moeten we het zelf vernietigen
// waarom proberen / behalve blok gebruiken
poging

// Een thread-voltooiingshandler toewijzen waarin we zullen ontvangen
// resultaten van de thread, en “overschrijf” de link ernaartoe
MyThread.OnTerminate:= GebeurtenisMyThreadOnTerminate;

// Omdat we de resultaten in OnTerminate zullen gebruiken, d.w.z. tot zelfvernietiging
// streamen, dan zullen we onszelf bevrijden van de zorgen om het te vernietigen
MyThread.FreeOnTerminate:= Waar;

// Een voorbeeld van het doorgeven van invoerparameters door de velden van een streamobject, op het punt
// een instantie maken wanneer deze nog niet actief is.
// Persoonlijk geef ik er de voorkeur aan om dit te doen via de parameters van het overschreven
// constructor (tMyThread.Create)
MyThread.Param2:= StrToInt(Form1.Edit2.Text);

MyThread.Stopped:= Onwaar; // ook een soort parameter, maar verandert afhankelijk van
// looptijd van de thread
behalve
// aangezien de thread nog niet is gestart en zichzelf niet kan vernietigen, laten we deze “handmatig” vernietigen
FreeAndNil(MijnThread);
// en laat de uitzondering vervolgens zoals gewoonlijk worden verwerkt
salarisverhoging;
einde;

// Omdat het threadobject met succes is gemaakt en geconfigureerd, is het tijd om het uit te voeren
MyThread.Cv;

ShowMessage("Stream gestart");
einde;

procedure TForm1.btnStopClick(Afzender: TObject);
beginnen
// Als de threadinstantie nog steeds bestaat, vraag hem dan om te stoppen
// Bovendien zullen we het gewoon “vragen”. In principe kunnen we het ook ‘forceren’, maar dat zal wel zo zijn
// uitsluitend een noodoptie, waarvoor een duidelijk begrip van dit alles vereist is
// stroomkeuken. Daarom wordt het hier niet beschouwd.
indien toegewezen (MyThread) dan
MyThread.Stopped:= Waar
anders
ShowMessage("De thread is niet actief!");
einde;

procedure TForm1.EventOnSendMessageMetod(var Msg: TMessage);
beginnen
// methode voor het verwerken van een synchroon bericht
// in WParam het adres van het tMyThread-object, in LParam de huidige LastRandom-waarde van de thread
met tMyThread(Msg.WParam) begin
Form1.Label3.Caption:= Formaat("%d %d %d",);
einde;
einde;

procedure TForm1.EventOnPostMessageMetod(var Msg: TMessage);
beginnen
// methode voor het verwerken van een asynchroon bericht
// in WParam de huidige waarde van IterationNo, in LParam de huidige waarde van de LastRandom van de thread
Form1.Label4.Caption:= Formaat("%d %d",);
einde;

procedure TForm1.EventMyThreadOnTerminate(Sender:tObject);
beginnen
// BELANGRIJK!
// De gebeurtenisafhandelingsmethode OnTerminate wordt altijd aangeroepen in de context van de main
// thread - dit wordt gegarandeerd door de tThread-implementatie. Daarom kunt u vrijelijk
// gebruik alle eigenschappen en methoden van alle objecten

// Zorg ervoor dat de objectinstantie nog steeds bestaat
indien niet toegewezen (MyThread), dan afsluiten; // als het er niet is, is er niets aan te doen

// de resultaten verkrijgen van het werk van een thread van een exemplaar van een threadobject
Form1.Memo1.Lines.Add(Format("De draad eindigde met resultaat %d",));
Form1.Memo1.Lines.AddStrings((Afzender als tMyThread).ResultList);

// Vernietig de verwijzing naar de threadobjectinstantie.
// Omdat onze thread zelfvernietigend is (FreeOnTerminate:= True)
// Nadat de OnTerminate-handler is voltooid, zal de thread-objectinstantie zijn
// vernietigd (gratis), en alle verwijzingen ernaar worden ongeldig.
// Om te voorkomen dat je per ongeluk zo'n link tegenkomt, verwijder je MyThread
// Nogmaals: we zullen het object niet vernietigen, maar alleen de link verwijderen. Voorwerp
// zal zichzelf vernietigen!
MijnThread:= Nihil;
einde;

constructor tMyThread.Create(aParam1:String);
beginnen
// Maak een exemplaar van een SUSPENDED-thread (zie commentaar bij het maken van een exemplaar)
geërfd Create(True);

// Maak interne objecten (indien nodig)
ResultList:= tStringList.Create;

// Het verkrijgen van initiële gegevens.

// Kopiëren van de invoergegevens die via de parameter zijn doorgegeven
Param1:= aParam1;

// Voorbeeld van het ontvangen van invoergegevens van VCL-componenten in de threadobjectconstructor
// Dit is in dit geval acceptabel, omdat de constructor in de context wordt aangeroepen
// hoofdthema. Daarom zijn VCL-componenten hier toegankelijk.
// Maar ik vind dit niet leuk, omdat ik het slecht vind als een thread iets weet
// over een of andere vorm. Maar wat kun je niet doen ter demonstratie?
Param3:= Formulier1.CheckBox1.Aangevinkt;
einde;

destructor tMyThread.Destroy;
beginnen
// vernietiging van interne objecten
FreeAndNil(Resultatenlijst);
// het vernietigen van de basis tThread
geërfd;
einde;

procedure tMyThread.Execute;
var
t: Kardinaal;
s:String;
beginnen
Iteratienr:= 0; // resultatenteller (cyclusnummer)

// In mijn voorbeeld is de kern van de draad een lus die eindigt
// of op een extern “verzoek” wordt de Stopped-parameter die door de variabele is doorgegeven, voltooid,
// of simpelweg door 5 cycli te voltooien
// Het is prettiger voor mij om dit via een ‘eeuwige’ lus te schrijven.

Terwijl True begint

Inc(Iteratienr); // volgende cyclusnummer

LaatsteRandom:= Willekeurig(1000); // willekeurig getal - om aan te tonen dat parameters van de stream naar het formulier worden doorgegeven

T:= Willekeurig(5)+1; // tijd waarvoor we in slaap zullen vallen als we niet worden beëindigd

// Saaie werking (afhankelijk van de invoerparameter)
zo niet Param3 dan
Inc(Param2)
anders
december(Param2);

// Genereer een tussenresultaat
s:= Formaat("%s %5d %s %d %d",
);

// Voeg het tussenresultaat toe aan de lijst met resultaten
ResultaatLijst.Toevoegen(s);

//// Voorbeelden van het doorgeven van tussenresultaten aan het formulier

//// Een gesynchroniseerde methode doorlopen - de klassieke manier
//// Gebreken:
//// - de gesynchroniseerde methode is meestal een methode van de threadklasse (voor toegang
//// naar de velden van het streamobject), maar om toegang te krijgen tot de formuliervelden moet dit wel
//// "weet" erover en zijn velden (objecten), waar je meestal niet zo goed mee bent
//// standpunten van de programmaorganisatie.
//// - de huidige thread wordt opgeschort totdat de uitvoering is voltooid
//// gesynchroniseerde methode.

//// Voordelen:
//// - standaard en universeel
//// - in een gesynchroniseerde methode die u kunt gebruiken
//// alle velden van het streamobject.
// Eerst moet u, indien nodig, de verzonden gegevens opslaan in
// speciale velden van het objectobject.
SyncDataN:= Iteratienr;
SyncDataS:= "Synchroniseren"+s;
// en geef vervolgens een gesynchroniseerde methodeaanroep op
Synchroniseren(SyncMetod1);

//// Verzending via synchrone verzending van berichten (SendMessage)
//// in dit geval kunnen gegevens worden doorgegeven via berichtparameters (LastRandom),
//// en door de velden van het object, waarbij het adres van de instantie wordt doorgegeven in de berichtparameter
//// streamobject - Integer (Zelf).
//// Gebreken:
//// - de thread moet de handle van het formuliervenster kennen
//// - net als bij Synchroniseren wordt de huidige thread opgeschort tot
//// volledige verwerking van het bericht door de hoofdthread
//// - vereist aanzienlijke CPU-tijd voor elke oproep
//// (voor het wisselen van threads), dus een zeer frequente oproep is ongewenst
//// Voordelen:
//// - zoals bij Synchroniseren, kunt u bij het verwerken van een bericht gebruik maken van
//// alle velden van het streamobject (als het adres uiteraard is doorgegeven)


//// start de draad.
SendMessage(Form1.Handle,WM_USER_SendMessageMetod,Integer(Self),LastRandom);

//// Verzending via asynchrone verzending van berichten (PostMessage)
//// Omdat in dit geval, tegen de tijd dat de hoofdthread het bericht ontvangt,
//// de verzendende thread is mogelijk al voltooid en heeft het exemplaaradres doorgegeven
////threadobject is niet geldig!
//// Gebreken:
//// - de thread moet de handle van het formuliervenster kennen;
//// - vanwege asynchronie is gegevensoverdracht alleen mogelijk via parameters
//// berichten, wat de overdracht van grote gegevens aanzienlijk bemoeilijkt
//// meer dan twee machinewoorden. Handig om te gebruiken voor het overbrengen van Integer, enz.
//// Voordelen:
//// - in tegenstelling tot eerdere methoden zal de huidige thread dat NIET doen
//// opgeschort, maar zal de uitvoering onmiddellijk hervatten
//// - in tegenstelling tot een gesynchroniseerde oproep, een berichtenhandler
//// is een formuliermethode die kennis moet hebben van het threadobject,
//// of helemaal niets weten over de stream als er alleen gegevens worden verzonden
//// via berichtparameters. Dat wil zeggen, de thread weet mogelijk niets over het formulier
//// in het algemeen - alleen de Handle, die eerder als parameter kan worden doorgegeven
//// start de draad.
PostMessage(Form1.Handle,WM_USER_PostMessageMetod,IteratieNee,LastRandom);

//// Controleer op mogelijke voltooiing

// Controleer voltooiing per parameter
indien gestopt, dan pauze;

// Controleer af en toe de voltooiing
als IteratieNee >= 10, dan Break;

Slaap(t*1000); // Val t seconden in slaap
einde;
einde;

procedure tMyThread.SyncMetod1;
beginnen
// deze methode wordt aangeroepen via de Synchronize-methode.
// Dat wil zeggen, ondanks het feit dat het een methode is van de tMyThread-thread,
// het draait in de context van de hoofdthread van de applicatie.
// Daarom kan hij alles, of bijna alles :)
// Maar onthoud: het is niet nodig om hier lang aan te sleutelen

// De doorgegeven parameters kunnen we extraheren uit het speciale veld waar we
// opgeslagen voordat u belt.
Form1.Label1.Caption:= SyncDataS;

// of uit andere velden van het stroomobject, bijvoorbeeld als weerspiegeling van de huidige status
Form1.Label2.Caption:= Formaat("%d %d",);
einde;

Over het algemeen werd het voorbeeld voorafgegaan door mijn volgende gedachten over het onderwerp....

Ten eerste:
DE BELANGRIJKSTE regel voor multi-threaded programmeren in Delphi:
In de context van een niet-hoofdthread heb je geen toegang tot de eigenschappen en methoden van formulieren, en zelfs niet tot alle componenten die uit tWinControl “groeien”.

Dit betekent (enigszins vereenvoudigd) dat noch in de Execute-methode geërfd van TThread, noch in andere methoden/procedures/functies aangeroepen vanuit Execute, het is verboden geen directe toegang tot eigenschappen of methoden van visuele componenten.

Hoe je het goed doet.
Er zijn hier geen algemene recepten. Om precies te zijn, er zijn zoveel verschillende opties die u moet kiezen, afhankelijk van het specifieke geval. Daarom verwijzen ze je naar het artikel. Nadat hij het heeft gelezen en begrepen, zal de programmeur kunnen begrijpen hoe hij dit in een bepaald geval het beste kan doen.

In een notendop:

Meestal wordt een applicatie multi-threaded wanneer het nodig is om langdurig werk te doen, of wanneer het mogelijk is om meerdere dingen tegelijkertijd te doen die de processor niet zwaar belasten.

In het eerste geval leidt het implementeren van werk binnen de hoofdthread tot het "vertragen" van de gebruikersinterface - terwijl het werk wordt gedaan, wordt de berichtverwerkingslus niet uitgevoerd. Als gevolg hiervan reageert het programma niet op gebruikersacties en wordt het formulier bijvoorbeeld niet getekend nadat de gebruiker het heeft verplaatst.

In het tweede geval, wanneer het werk een actieve uitwisseling met de buitenwereld impliceert, dan tijdens gedwongen “downtime”. Terwijl u wacht tot gegevens worden ontvangen/verzonden, kunt u parallel iets anders doen, bijvoorbeeld opnieuw andere gegevens verzenden/ontvangen.

Er zijn andere gevallen, maar minder gebruikelijk. Dit doet er echter niet toe. Daar gaat het nu niet over.

Hoe wordt dit allemaal geschreven? Uiteraard wordt een bepaald meest voorkomende geval, enigszins gegeneraliseerd, in beschouwing genomen. Dus.

Werk dat in een aparte thread wordt uitgevoerd, heeft over het algemeen vier entiteiten (ik weet niet hoe ik het preciezer moet noemen):
1. Initiële gegevens
2. Het daadwerkelijke werk zelf (dit kan afhankelijk zijn van de brongegevens)
3. Tussenliggende gegevens (bijvoorbeeld informatie over de huidige stand van zaken)
4. Uitgang (resultaat)

Meestal worden visuele componenten gebruikt om de meeste gegevens te lezen en weer te geven. Maar zoals hierboven vermeld, hebt u geen directe toegang tot visuele componenten vanuit een stream. Hoe kan dit?
Delphi-ontwikkelaars raden aan om de Synchronize-methode van de TThread-klasse te gebruiken. Hier zal ik niet beschrijven hoe je het moet gebruiken - daarvoor is er het bovengenoemde artikel. Ik wil alleen maar zeggen dat het gebruik ervan, zelfs als het correct is, niet altijd gerechtvaardigd is. Er zijn twee problemen:

Ten eerste wordt de hoofdtekst van een methode die via Synchronize wordt aangeroepen altijd uitgevoerd in de context van de hoofdthread, en daarom wordt tijdens de uitvoering de vensterberichtverwerkingslus opnieuw niet uitgevoerd. Daarom moet het snel worden uitgevoerd, anders krijgen we dezelfde problemen als bij een single-threaded implementatie. Idealiter zou de via Synchronize aangeroepen methode over het algemeen alleen gebruikt moeten worden om toegang te krijgen tot de eigenschappen en methoden van visuele objecten.

Ten tweede is het uitvoeren van een methode via Synchroniseren een “duur” plezier, veroorzaakt door de noodzaak van twee schakelaars tussen threads.

Bovendien zijn beide problemen met elkaar verbonden en veroorzaken ze een tegenstrijdigheid: aan de ene kant is het, om de eerste op te lossen, nodig om de methoden die via Synchroniseren worden aangeroepen te ‘versnipperen’, en aan de andere kant moeten ze dan vaker worden aangeroepen, waardoor waardevolle informatie verloren gaat. processorbronnen.

Daarom moet je het, zoals altijd, verstandig benaderen en voor verschillende gevallen verschillende manieren gebruiken om met de buitenwereld te communiceren:

Initiële gegevens
Alle gegevens die naar de stream worden verzonden en tijdens de werking ervan niet veranderen, moeten worden verzonden voordat deze wordt gelanceerd, d.w.z. bij het maken van een draad. Om ze in de hoofdtekst van een thread te gebruiken, moet je er een lokale kopie van maken (meestal in de velden van een TThread-kind).
Als er brongegevens zijn die kunnen veranderen terwijl de thread actief is, dan moet toegang tot dergelijke gegevens plaatsvinden via gesynchroniseerde methoden (methoden aangeroepen via Synchronize) of via de velden van een threadobject (een afstammeling van TThread). Dit laatste vergt enige voorzichtigheid.

Tussen- en outputgegevens
Ook hier zijn er verschillende manieren (in volgorde van mijn voorkeur):
- Een methode voor het asynchroon verzenden van berichten naar het hoofdvenster van het programma.
Meestal gebruikt om berichten naar het hoofdvenster van het programma te sturen over de status van een proces, waarbij een kleine hoeveelheid gegevens wordt verzonden (bijvoorbeeld het voltooiingspercentage)
- Een methode voor het synchroon verzenden van berichten naar het hoofdvenster van het programma.
Het wordt meestal voor dezelfde doeleinden gebruikt als asynchroon verzenden, maar u kunt een grotere hoeveelheid gegevens overbrengen zonder een afzonderlijke kopie te maken.
- Gesynchroniseerde methoden, waar mogelijk, waarbij de overdracht van zoveel mogelijk gegevens in één methode wordt gecombineerd.
Kan ook worden gebruikt om gegevens uit een formulier te ontvangen.
- Via de velden van een streamobject, waardoor wederzijds uitsluitende toegang wordt gegarandeerd.
Je kunt meer lezen in het artikel.

Eh. Het ging weer niet goed

einde van bestand. Logboekvermeldingen die door verschillende processen worden uitgevoerd, worden dus nooit door elkaar gebruikt. Modernere Unix-systemen bieden een speciale syslog(3C)-service voor loggen.

Voordelen:

  1. Gemak van ontwikkeling. In feite draaien we veel kopieën van één enkele thread-applicatie en deze draaien onafhankelijk van elkaar. U hoeft geen specifiek multi-threaded API's te gebruiken en communicatiemiddelen tussen processen.
  2. Hoge betrouwbaarheid. Abnormale beëindiging van een proces heeft op geen enkele manier invloed op andere processen.
  3. Goede tolerantie. De applicatie werkt op elk multitasking besturingssysteem
  4. Hoge beveiliging. Verschillende applicatieprocessen kunnen als verschillende gebruikers draaien. Op deze manier is het mogelijk om het principe van de minste privileges te implementeren, waarbij elk proces alleen die rechten heeft die het nodig heeft om te kunnen functioneren. Zelfs als er een bug wordt ontdekt in een van de processen waarmee code op afstand kan worden uitgevoerd, kan de aanvaller alleen het toegangsniveau verkrijgen waarmee dit proces is uitgevoerd.

Gebreken:

  1. Niet alle toegepaste taken kunnen op deze manier worden uitgevoerd. Deze architectuur is bijvoorbeeld geschikt voor een server die statische HTML-pagina's bedient, maar is totaal ongeschikt voor een databaseserver en veel applicatieservers.
  2. Het aanmaken en vernietigen van processen is een kostbare operatie, waardoor deze architectuur voor veel taken niet optimaal is.

Unix-systemen nemen een hele reeks maatregelen om het maken van een proces en het uitvoeren van een nieuw programma in een proces zo goedkoop mogelijk te maken. U moet echter begrijpen dat het creëren van een thread binnen een bestaand proces altijd goedkoper zal zijn dan het creëren van een nieuw proces.

Voorbeelden: apache 1.x (HTTP-server)

Multiprocestoepassingen die communiceren via System V IPC-sockets, leidingen en berichtenwachtrijen

De genoemde IPC-tools (Interprocess Communication) verwijzen naar de zogenaamde harmonische interprocess-communicatietools. Hiermee kunt u de interactie van processen en threads organiseren zonder gebruik te maken van gedeeld geheugen. Programmeertheoretici zijn erg gesteld op deze architectuur, omdat deze vrijwel alle soorten concurrentiefouten elimineert.

Voordelen:

  1. Relatief ontwikkelingsgemak.
  2. Hoge betrouwbaarheid. Abnormale beëindiging van een van de processen heeft tot gevolg dat de pipe of socket wordt gesloten, en in het geval van berichtenwachtrijen tot het feit dat berichten niet meer aankomen of uit de wachtrij worden opgehaald. De rest van de applicatieprocessen kunnen deze fout gemakkelijk detecteren en herstellen, mogelijk (maar niet noodzakelijkerwijs) door simpelweg het mislukte proces opnieuw te starten.
  3. Veel van dergelijke applicaties (vooral die gebaseerd op sockets) kunnen eenvoudig opnieuw worden ontworpen om te draaien in een gedistribueerde omgeving, waar verschillende componenten van de applicatie op verschillende machines draaien.
  4. Goede tolerantie. De applicatie werkt op de meeste multitasking besturingssystemen, inclusief oudere Unix-systemen.
  5. Hoge beveiliging. Verschillende applicatieprocessen kunnen als verschillende gebruikers draaien. Op deze manier is het mogelijk om het principe van de minste privileges te implementeren, waarbij elk proces alleen die rechten heeft die het nodig heeft om te kunnen functioneren.

Zelfs als er een bug wordt ontdekt in een van de processen waarmee code op afstand kan worden uitgevoerd, kan de aanvaller alleen het toegangsniveau verkrijgen waarmee dit proces is uitgevoerd.

Gebreken:

  1. Een dergelijke architectuur is niet eenvoudig te ontwikkelen en te implementeren voor alle applicatieproblemen.
  2. Alle genoemde typen IPC-tools vereisen seriële gegevensoverdracht. Als willekeurige toegang tot gedeelde gegevens vereist is, is deze architectuur lastig.
  3. Het overbrengen van gegevens via een pipe, socket en berichtenwachtrij vereist het uitvoeren van systeemaanroepen en het tweemaal kopiëren van de gegevens: eerst van de adresruimte van het bronproces naar de adresruimte van de kernel, en vervolgens van de adresruimte van de kernel naar het geheugen doel proces. Het zijn dure operaties. Bij het overbrengen van grote hoeveelheden gegevens kan dit een serieus probleem worden.
  4. De meeste systemen hebben limieten voor het totale aantal leidingen, stopcontacten en IPC-voorzieningen. In Solaris zijn dus standaard niet meer dan 1024 open pipelines, sockets en bestanden per proces toegestaan ​​(dit komt door de beperkingen van de geselecteerde systeemaanroep). De architectonische limiet van Solaris bedraagt ​​65536 buizen, sockets en vijlen per proces.

    De limiet op het totale aantal TCP/IP-sockets is niet meer dan 65536 per netwerkinterface (vanwege het TCP-headerformaat). Systeem V IPC-berichtenwachtrijen bevinden zich in de adresruimte van de kernel, dus er zijn strikte beperkingen op het aantal wachtrijen in het systeem en op het volume en het aantal berichten dat tegelijkertijd in de wachtrij kan worden geplaatst.

  5. Het creëren en vernietigen van een proces, en het schakelen tussen processen zijn kostbare handelingen. Deze architectuur is niet in alle gevallen optimaal.

Multiprocesapplicaties die communiceren via gedeeld geheugen

Het gedeelde geheugen kan bestaan ​​uit gedeeld systeem V IPC-geheugen en bestands-naar-geheugen-toewijzing. Om de toegang te synchroniseren, kunt u System V IPC-semaforen, mutexen en POSIX-semaforen gebruiken, en bij het toewijzen van bestanden aan het geheugen, secties van het bestand vastleggen.

Voordelen:

  1. Efficiënte willekeurige toegang tot gedeelde gegevens. Deze architectuur is geschikt voor het implementeren van databaseservers.
  2. Hoge tolerantie. Kan worden geporteerd naar elk besturingssysteem dat System V IPC ondersteunt of emuleert.
  3. Relatief hoge beveiliging. Verschillende applicatieprocessen kunnen onder verschillende gebruikers draaien. Op deze manier is het mogelijk om het principe van de minste privileges te implementeren, waarbij elk proces alleen die rechten heeft die het nodig heeft om te kunnen functioneren. De scheiding van toegangsniveaus is echter niet zo strikt als bij de eerder besproken architecturen.

Gebreken:

  1. Relatieve complexiteit van ontwikkeling. Fouten in de toegangssynchronisatie – zogenaamde racefouten – zijn tijdens het testen zeer moeilijk te detecteren.

    Dit kan resulteren in 3 tot 5 maal hogere totale ontwikkelingskosten vergeleken met single-threaded of eenvoudigere multi-tasking architecturen.

  2. Lage betrouwbaarheid. Bij het afgebroken beëindigen van een van de processen van de applicatie kan het gedeelde geheugen in een inconsistente toestand achterblijven (en gebeurt dit vaak ook).

    Dit zorgt er vaak voor dat andere applicatietaken crashen. Sommige toepassingen, zoals Lotus Domino, beëindigen specifiek serverbrede processen als deze abnormaal worden beëindigd.

  3. Het creëren en vernietigen van een proces en het schakelen daartussen zijn kostbare handelingen.

    Daarom is deze architectuur niet optimaal voor alle toepassingen.

  4. Onder bepaalde omstandigheden kan het gebruik van gedeeld geheugen leiden tot escalatie van bevoegdheden. Als er een fout wordt gevonden in een van de processen die leidt tot het op afstand uitvoeren van code, is de kans groot dat een aanvaller deze kan gebruiken om op afstand code uit te voeren in andere applicatieprocessen.

    Dat wil zeggen dat in het ergste geval een aanvaller een toegangsniveau kan verkrijgen dat overeenkomt met het hoogste toegangsniveau van de applicatieprocessen.

  5. Applicaties die gebruikmaken van gedeeld geheugen moeten op dezelfde fysieke computer draaien, of op zijn minst op machines met gedeeld RAM. In werkelijkheid kan deze beperking worden omzeild, bijvoorbeeld door gedeelde bestanden in geheugenmaps te gebruiken, maar dit brengt aanzienlijke overhead met zich mee.

In feite combineert deze architectuur de nadelen van multi-process en multi-threaded applicaties. Een aantal populaire applicaties die in de jaren 80 en begin jaren 90 zijn ontwikkeld, voordat de multithreading-API's van Unix werden gestandaardiseerd, maken echter gebruik van deze architectuur. Dit zijn veel databaseservers, zowel commerciële (Oracle, DB2, Lotus Domino), freeware, moderne versies van Sendmail en enkele andere mailservers.

Eigenlijk multi-threaded applicaties

Threads of threads van een applicatie draaien binnen hetzelfde proces. De volledige adresruimte van een proces wordt gedeeld tussen threads. Op het eerste gezicht lijkt het erop dat je hiermee de interactie tussen threads kunt organiseren zonder dat je speciale API's nodig hebt. In werkelijkheid is dit niet waar: als meerdere threads aan een gedeelde datastructuur of systeembron werken, en ten minste één van de threads deze structuur wijzigt, zullen de gegevens op een bepaald moment inconsistent zijn.

Daarom moeten threads speciale middelen gebruiken om te communiceren. De belangrijkste kenmerken zijn primitieven voor wederzijdse uitsluiting (mutexen en lees-schrijfvergrendelingen). Met behulp van deze primitieven kan de programmeur ervoor zorgen dat geen enkele thread toegang krijgt tot gedeelde bronnen terwijl deze zich in een inconsistente staat bevinden (dit wordt wederzijdse uitsluiting genoemd). Systeem V IPC, alleen de structuren die zich in het gedeelde geheugensegment bevinden, worden gedeeld. Reguliere variabelen en dynamische datastructuren die op de gebruikelijke manier worden toegewezen, zijn uniek voor elk proces). Fouten in de toegang tot gedeelde gegevens – racefouten – zijn bij het testen zeer moeilijk te detecteren.

  • De hoge kosten voor het ontwikkelen en debuggen van applicaties, als gevolg van punt 1.
  • Lage betrouwbaarheid. De vernietiging van datastructuren, bijvoorbeeld als gevolg van bufferoverflows of pointerfouten, beïnvloedt alle threads van het proces en leidt meestal tot een abnormale beëindiging van het hele proces. Andere fatale fouten, zoals deling door nul in een van de threads, zorgen er meestal ook voor dat alle threads in het proces vastlopen.
  • Lage beveiliging. Alle applicatiethreads worden in hetzelfde proces uitgevoerd, dat wil zeggen onder dezelfde gebruikersnaam en met dezelfde toegangsrechten. Het is niet mogelijk om het principe van de minste privileges te implementeren; het proces moet worden uitgevoerd als een gebruiker die alle bewerkingen kan uitvoeren die vereist zijn voor alle threads van de applicatie.
  • Het maken van een thread is nog steeds een vrij dure operatie. Elke thread krijgt noodzakelijkerwijs zijn eigen stapel toegewezen, die standaard 1 megabyte RAM in beslag neemt op 32-bits architecturen en 2 megabytes op 64-bits architecturen, en enkele andere bronnen. Daarom is deze architectuur niet optimaal voor alle toepassingen.
  • Onvermogen om de applicatie uit te voeren op een computersysteem met meerdere machines. De technieken die in de vorige sectie zijn genoemd, zoals geheugentoewijzing van gedeelde bestanden, zijn niet van toepassing op een programma met meerdere threads.
  • Over het algemeen kan worden gezegd dat multi-threaded applicaties vrijwel dezelfde voor- en nadelen hebben als multi-procesapplicaties die gebruikmaken van gedeeld geheugen.

    De kosten voor het uitvoeren van een multithreaded applicatie zijn echter lager, en het ontwikkelen van een dergelijke applicatie is in sommige opzichten eenvoudiger dan een applicatie met gedeeld geheugen. Daarom zijn multi-threaded applicaties de afgelopen jaren steeds populairder geworden.