Draad (informatica)

Van Wikipedia, de gratis encyclopedie
Spring naar navigatie Spring naar zoeken

In de informatica , Thread [ θɹɛd ] ( Engelse thread , 'thread', 'strand') - ook wel activity carrier of lichtgewicht proces genoemd - een uitvoeringsthread of een uitvoeringsvolgorde in de uitvoering van een programma . Een thread is onderdeel van een proces .

Er wordt onderscheid gemaakt tussen twee soorten draden:

  1. Threads in engere zin , de zogenaamde kernelthreads , draaien onder controle van het besturingssysteem .
  2. Daartegenover staan ​​de gebruikersthreads , die het computerprogramma van de gebruiker volledig zelf moet beheren.

Dit artikel behandelt de draad in engere zin, namelijk de kerndraad.

Discussies vanuit het oogpunt van het besturingssysteem

Meerdere kernelthreads in één proces (taak)

Een (kernel)thread is een sequentiële uitvoering die binnen een proces wordt uitgevoerd en een aantal resources deelt met de andere bestaande threads ( multithreading ) van het bijbehorende proces:

Historisch gezien bedacht Friedrich L. Bauer hiervoor de term sequentieel proces .

Threads binnen hetzelfde proces kunnen aan verschillende processors worden toegewezen. Elke thread heeft zijn eigen zogenaamde thread-context:

  • onafhankelijke registerset inclusief instructiewijzer,
  • zijn eigen stack , maar meestal in de gemeenschappelijke procesadresruimte.
  • Als speciale functie kunnen er bronnen zijn die alleen door de genererende thread kunnen of mogen worden gebruikt (bijvoorbeeld: thread-local storage , window handle).

Andere bronnen worden gedeeld door alle threads. Ook het gezamenlijk gebruik van middelen kan tot conflicten leiden. Deze moeten worden opgelost door het gebruik van synchronisatiemechanismen .

Aangezien threads die aan hetzelfde proces zijn toegewezen dezelfde adresruimte gebruiken, is de communicatie tussen deze threads vanaf het begin erg eenvoudig (vgl. interprocescommunicatie voor processen).

Elke "thread" is verantwoordelijk voor het uitvoeren van een specifieke taak. Zo kunnen de uitvoeringslijnen van de programmafuncties worden onderverdeeld in beheersbare eenheden en kunnen omvangrijke taken over meerdere processorcores worden verdeeld.

In de meeste besturingssystemen kan een thread een inactieve status hebben naast de statussen actief (actief), gereed en geblokkeerd (wachtend). In de 'computing' toestand (= actief = draaiend) vindt de uitvoering van commando's plaats op de CPU , in de 'computing ready' (= ready = ready) wordt de thread gestopt om een ​​andere thread te laten berekenen en in het geval van ' geblokkeerd' (= wachten) de thread wacht op een gebeurtenis (meestal dat een besturingssysteemservice is voltooid / uitgevoerd). Een thread in de status 'inactief' wordt meestal net opgezet door het besturingssysteem of is klaar met berekenen en kan nu door het besturingssysteem uit de threadlijst worden verwijderd of op een andere manier worden hergebruikt.

Verschil in betekenis tussen (kernel)thread en proces, taak- en gebruikersthread

Een proces beschrijft het uitvoeren van een computerprogramma op één of meer processor (s). Een adresruimte en andere bronnen van het besturingssysteem worden toegewezen aan een proces - met name processen zijn van elkaar afgeschermd: als een proces toegang probeert te krijgen tot adressen of bronnen die er niet aan zijn toegewezen (en mogelijk tot een ander proces behoren), zal mislukken en het besturingssysteem zal het gebruiken geannuleerd. Een proces kan meerdere threads bevatten of - als parallelle verwerking in de loop van het programma niet is voorzien - slechts één enkele thread. Threads delen processors, geheugen en andere besturingssysteemafhankelijke bronnen zoals bestanden en netwerkverbindingen binnen een proces. Hierdoor is de administratieve inspanning voor threads meestal minder dan die voor processen. Een belangrijk efficiëntievoordeel van threads is enerzijds dat, in tegenstelling tot processen, een volledige verandering van de procescontext niet nodig is bij het veranderen van threads, aangezien alle threads anderzijds een gemeenschappelijk deel van de procescontext gebruiken. , in de eenvoudige communicatie en snelle gegevensuitwisseling tussen threads.

Zogenaamde multitask-besturingssystemen bestonden al in de jaren tachtig, omdat, in tegenstelling tot de taakgerichte systemen, met name in de toen bekende procescomputertechnologie , meerdere taken parallel moesten worden uitgevoerd. In die tijd werd de term taak gebruikt om een ​​taak te beschrijven vanuit het oogpunt van het besturingssysteem, wat een synoniem is voor proces. De term taak (Duits: taak), maar wordt ook vaak gebruikt in de software-architectuur van focus voor gerelateerde taken, en wordt vooral zelden gebruikt als synoniem voor thread.

Een thread is letterlijk een enkele thread van uitvoering van een programma, maar de term thread wordt gebruikt voor de thread van uitvoering vanuit het oogpunt van het besturingssysteem (kernelthread). In een gebruikerssoftware kan deze uitvoeringsstreng door middel van geschikte programmering verder worden onderverdeeld in onafhankelijke individuele strengen. In de Engelstalige wereld is de term gebruikersthread (voor Microsoft fiber , Duits: fiber ) ingeburgerd voor een enkele dergelijke uitvoeringsstreng van de gebruikerssoftware. In het geval van gebruikersthreads is alleen de gebruikerssoftware verantwoordelijk voor het beheer van de uitvoeringsthreads.

Voorbeelden

De volgende tabel toont voorbeelden van de verschillende combinaties van proces, kernel en gebruikersthread:

Verwerken Kernel
Draad
Gebruiker
Draad
voorbeeld
Nee Nee Nee Een computerprogramma dat draait onder MS-DOS . Het programma kan slechts één van de drie acties tegelijk uitvoeren.
Nee Nee Ja Windows 3.1 op het oppervlak van DOS. Alle Windows-programma's draaien in een eenvoudig proces, een programma kan het geheugen van een ander programma vernietigen, maar dit wordt opgemerkt en een General Protection Fault ( GPF ) is het gevolg.
Nee Ja Nee Originele implementatie van het Amiga OS . Het besturingssysteem ondersteunt threads volledig en zorgt ervoor dat verschillende toepassingen onafhankelijk van elkaar kunnen worden uitgevoerd, gepland door de kernel van het besturingssysteem. Vanwege het gebrek aan procesondersteuning is het systeem efficiënter (omdat het de extra kosten van geheugenbescherming vermijdt), met de prijs dat toepassingsfouten de hele computer kunnen lamleggen.
Nee Ja Ja DR-DOS 7.01, 7.03, 8.0; Verbeterde DR-DOS alle versies
Mac OS 9 ondersteunt gebruikersthreads met behulp van Apple's Thread Manager en kernelthreads met Apple's Multiprocessing Services , die werkt met de nanokernel , geïntroduceerd in Mac OS 8.6. Dit betekent dat threads worden ondersteund, maar de MultiFinder- methode wordt nog steeds gebruikt om applicaties te beheren.
Ja Nee Nee Meest bekende implementaties van Unix (behalve Linux). Het besturingssysteem kan meer dan één programma tegelijk uitvoeren, de programma-uitvoeringen zijn tegen elkaar beveiligd. Wanneer een programma zich misdraagt, kan het zijn eigen proces verstoren, wat kan resulteren in het doden van dat ene proces zonder het besturingssysteem of andere processen te verstoren. De uitwisseling van informatie tussen processen kan echter foutgevoelig zijn (bij gebruik van technieken zoals gedeeld geheugen ) of complex (bij gebruik van technieken zoals het doorgeven van berichten ). De asynchrone uitvoering van taken vereist een complexe fork () systeemaanroep .
Ja Nee Ja Sun OS (Solaris) van Sun. Sun OS is de versie van Unix van Sun Microsystems. Sun OS implementeert gebruikersthreads als zogenaamde groene threads om een ​​eenvoudig proces mogelijk te maken om verschillende taken asynchroon uit te voeren, bijvoorbeeld het afspelen van een geluid, het opnieuw schilderen van een venster of om te reageren op een operatorgebeurtenis zoals de selectie van de stopknop . Hoewel processen preventief worden beheerd, werken de groene draden samen. Dit model wordt vaak gebruikt in plaats van echte threads en is nog steeds up-to-date in microcontrollers en zogenaamde embedded devices , en wordt zeer frequent gebruikt.

Windows 3.x in verbeterde modus bij gebruik van DOS-boxen valt ook in deze categorie, omdat de DOS-boxen onafhankelijke processen vertegenwoordigen met een aparte adresruimte.

Ja Ja Nee Dit is het algemene geval voor toepassingen onder Windows NT vanaf 3.51 SP3+, Windows 2000, Windows XP, Mac OS X, Linux en andere moderne besturingssystemen. Al deze besturingssystemen stellen de programmeur in staat om gebruikersthreads of bibliotheken te gebruiken die eigen gebruikersthreads gebruiken, maar niet alle programma's die deze mogelijkheid bieden. Verder kunnen gebruikersthreads ook automatisch door het besturingssysteem worden aangemaakt voor elke gestarte applicatie (bijvoorbeeld om de grafische gebruikersinterface te bedienen) zonder dat de programmeur dit expliciet hoeft te doen; dergelijke programma's worden dan automatisch multithreaded . Het is ook noodzakelijk om meerdere gebruikersthreads te gebruiken als u meerdere processors / processorcores in de applicatie wilt gebruiken.
Ja Ja Ja De meeste besturingssystemen sinds 1995 vallen in deze categorie. Het gebruik van threads om gelijktijdig uit te voeren is de gebruikelijke keuze, hoewel er ook toepassingen met meerdere processen en meerdere vezels bestaan. Deze worden bijvoorbeeld gebruikt zodat een programma zijn grafische gebruikersinterface kan verwerken terwijl het wacht op input van de gebruiker of ander werk op de achtergrond doet.

Opmerkingen:

  • Het gebruik van gebruikersthreads is in principe onafhankelijk van het besturingssysteem. Het kan dus met elk besturingssysteem. Het is alleen belangrijk dat de volledige status van de processor kan worden uitgelezen en teruggeschreven (er zijn ook gebruikersthreads geïmplementeerd in sommige 8-bits besturingssystemen, bijvoorbeeld als GEOS op de C64 / C128). Daarom moeten de waarden in de tabel worden gezien als referentiewaarden.
  • Sommige moderne besturingssystemen (bijvoorbeeld Linux) staan ​​niet langer een strikt onderscheid toe tussen processen en kernelthreads. Beide worden gedaan met dezelfde systeemaanroep (kloon (2)); u kunt op fijnmazige wijze specificeren welke bronnen moeten worden gedeeld en welke niet (uitzondering: CPU-register, stapel). Bij een aantal resources kan dit zelfs worden gewijzigd terwijl de thread loopt (geheugen: TLS vs. gedeeld geheugen, bestandshandle: socketpair).

Implementaties

Java

Het werken met meerdere threads is vanaf het begin bedoeld in Java . Multithreading werkt ook als het besturingssysteem het niet of slechts onvoldoende ondersteunt. Dit is mogelijk omdat de Java-virtuele machine het wisselen van threads kan overnemen, inclusief stackbeheer. In besturingssystemen met threadondersteuning kunnen de eigenschappen van het besturingssysteem direct worden gebruikt. De beslissing hierover ligt in de programmering van de virtuele machine.

In Java is er de klasse Thread in het basispakket java.lang . Instanties van deze klasse zijn administratieve eenheden van de threads. Thread kan ofwel worden gebruikt als de basisklasse voor een gebruikersklasse, of een instantie van Thread kent een instantie van een willekeurige gebruikersklasse. In het tweede geval moet de gebruikersklasse de java.lang.Runnable- interface implementeren en daarom een ​​methode run () bevatten.

Een thread wordt gestart door thread.start () aan te roepen. De toegewezen run()- methode wordt verwerkt. Zolang run () actief is, is de thread actief.

In de methode run () of in de methoden die vanaf daar worden aangeroepen, kan de gebruiker wait () gebruiken om de thread een bepaalde tijd (gespecificeerd in milliseconden) of voor een bepaalde tijd te laten wachten . Dit wachten wordt beëindigd met een notificatie () van een andere thread. Dit is een belangrijk mechanisme voor communicatie tussen threads. wait () en notificeren () zijn methoden van de klasse Object en kunnen op alle gegevensinstanties worden gebruikt. Geassocieerd wachten () en notificeren () moeten in dezelfde instantie worden georganiseerd (een gebruikersklasse).Het is logisch om de gegevens die de ene thread in deze instantie aan de andere wil doorgeven, over te dragen.

De realisatie van kritische secties gebeurt met gesynchroniseerd .

In de eerste versie van Java werden methoden van de klasse Thread geïntroduceerd om een ​​thread van buitenaf te onderbreken, door te gaan en af ​​te breken: suspend () , hervatten () en stop () . Deze methoden werden echter al snel als verouderd aangeduid in latere versies . In de uitgebreide toelichting stond dat een systeem onveilig is als een thread van buitenaf kan worden gestopt of afgebroken. De reden is, in een paar woorden, als volgt: Een thread bevindt zich mogelijk in een fase van een kritieke sectie en sommige gegevens zijn mogelijk gewijzigd. Als het wordt gestopt, wordt het kritieke gedeelte geblokkeerd en zijn deadlocks het gevolg. Als het wordt geannuleerd en de blokkering door het systeem wordt opgeheven, zijn de gegevens inconsistent. Op dit moment kan een runtime-systeem geen eigen beslissing nemen; alleen het gebruikersprogramma zelf kan een thread besturen die wordt gestopt of afgebroken.

.NETTO

.NET ondersteunt native thread-programmering. Dit wordt geïmplementeerd door de klassen in de naamruimte System.Threading .

Naast de hierboven genoemde proces- en threadconstructies, is er ook het concept van een applicatiedomein ( AppDomain ). Een proces kan meerdere applicatiedomeinen bevatten, deze zijn geïsoleerd van de runtime ("logisch proces"), een bron die door het .Net-framework wordt geleverd, is gekoppeld aan het genererende applicatiedomein. Bronnen van het onderliggende besturingssysteem (inclusief kernelthreads!) Zijn niet gebonden aan deze logische proceslimieten.

De .NET-runtime biedt ook een door de runtime beheerde threadpool, die door de runtime wordt gebruikt om asynchrone gebeurtenissen en invoer-/uitvoerbewerkingen te verwerken.

De .NET-runtime maakt ook onderscheid tussen voorgrondthreads en achtergrondthreads. Een thread wordt de achtergrondthread door de eigenschap Background in te stellen op true . Een proces eindigt wanneer de laatste voorgrondthread is voltooid. Alle achtergrondthreads die nog actief zijn, worden automatisch beëindigd. Discussiepoolthreads worden gestart als achtergrondthreads.

Een onafhankelijke thread wordt gestart via een nieuwe instantie van een threadklasse, waaraan een callback-functie ( delegate ) wordt doorgegeven in de constructor. De thread wordt vervolgens gestart met behulp van de instantiemethode Start () . De thread eindigt wanneer de callback-functie de controle teruggeeft aan de beller.

Als alternatief kan de threadpool van de .NET-runtime worden gebruikt voor korte achtergrondverwerking. Dit bevat een bepaald aantal threads dat kan worden gebruikt voor verwerking via ThreadPool.QueueUserWorkItem () . Na de terugkeer van de callback-functie wordt de thread niet vernietigd door het besturingssysteem, maar in de cache opgeslagen voor later gebruik. Het voordeel van deze klasse is het geoptimaliseerde, beperkte gebruik van de onderliggende apparatuur.

Externe controle van de threads is mogelijk ( Abort () , Suspend () , Resume () ), maar kan leiden tot onvoorspelbare gebeurtenissen zoals deadlocks of aborts van het toepassingsdomein. Daarom zijn Suspend en Resume gemarkeerd als verouderd in nieuwere versies van .NET.

De threads worden gesynchroniseerd met een WaitHandle . Dit wordt meestal gebruikt via de klasse Monitor , die een mutex gebruikt die door elk .NET-object beschikbaar wordt gesteld. In C # kun je het slot (object) {instructie; } Construct kan worden gebruikt. Veel klassen van het .Net Framework bestaan ​​ook in een threadveilige variant die kan worden gemaakt met behulp van een statische methode Synchronized () .

Unix / Linux

Onder Unix zijn er altijd gebruiksvriendelijke systeemaanroepen geweest voor het creëren van parallelle processen ( fork ). Dit betekent dat parallelle verwerking traditioneel wordt geïmplementeerd onder Unix / Linux. Threads werden toegevoegd in latere Unix-versies, maar overdraagbaarheid tussen eerdere afgeleiden was niet gegarandeerd. De standaard POSIX- thread ( Native POSIX Thread Library ) stelde uiteindelijk een uniform minimum aan functies en een uniforme API voor , die ook door de huidige Linux-versies ( NPTL ) wordt ondersteund. In vergelijking met een proces wordt een thread ook wel een lichtgewicht proces ( Solaris ) genoemd, omdat het schakelen tussen processen meer inspanning (rekentijd) in het besturingssysteem vereist dan het schakelen tussen threads van een proces.

ramen

Om uw eigen thread in C of C++ onder Windows te maken, heeft u direct toegang tot de Windows API-interfaces. Om dit te doen, moet je als een eenvoudig patroon aanroepen:

 #include <windows.h>
DWORD- thread-ID ;
HANDLE hThread = CreateThread ( NULL , 0 , runInThread , p , 0 , & threadId );
CloseHandle ( hThread );

runInThread is de subroutine die in deze thread moet worden uitgevoerd; deze wordt onmiddellijk daarna aangeroepen. Als runInThread beëindigd, wordt de thread ook beëindigd, vergelijkbaar met Thread.run() in Java.

Deze API is een C-georiënteerde interface. Om threads op een objectgeoriënteerde manier te programmeren, kan een methode van een klasse worden aangeroepen volgens het volgende schema in de runInThread subroutine:

 DWORD WINAPI runInThread (LPVOID runnableInstance)
{
   Runnable * runnable = static_cast < Runnable *> ( runnableInstance );
                        // Klasseaanwijzer of aanwijzer naar basisklasse
   return ( uitvoerbaar -> uitvoeren ()); // run methode van deze klasse wordt aangeroepen.
}

De klasse die de methode run () voor de thread bevat, bevindt zich hier in een klasse Runnable , die ook een basisklasse van een grotere klasse kan zijn. De pointer naar de instantie van een mogelijk van Runnable afgeleide klasse moet bij CreateThread als parameter (p) worden doorgegeven, en hoewel de (uitvoerbare *). Je hebt dus dezelfde technologie in handen als bij Java. De universele basisklasse (een interface) voor alle klassen waarvan de run ()-methoden in een aparte thread moeten worden uitgevoerd, wordt als volgt gedefinieerd:

 class Runnable // abstracte basisklasse (kan als interface worden gebruikt)
   {
      virtuele professionele DWORD- methode () = 0 ; // API om te erven
   publiek :
      DWORD run () { return ( foldMethod ()); } // API om aan te roepen
      virtual ~ Runnable () {} // Als het moet worden overgenomen: Dtor virtual
   };

De gebruikersklasse met de specialistische methode [1] wordt hieronder gedefinieerd:

 class MyThreadClass : public Runnable
   {
      DWORD fachMethode (); // Overschrijft / implementeert de specialistische methode
   };

De gebruikersklasse wordt vervolgens geïnstantieerd en de thread wordt gestart:

 MyThreadClass myThreadObject ;
hThread = CreateThread ( NULL , 0 , runInThread , & myThreadObject , 0 , & threadId );

Vanwege de dynamische binding wordt de gewenste methode myThread->fachMethode() aangeroepen. Let op: de levenscyclus van myThreadObject moet in acht worden genomen: u mag deze niet impliciet "opruimen" zolang de nieuwe thread er nog mee werkt! Draadsynchronisatie is hier vereist.

Verdere toegangen tot de thread op API-niveau kunnen bijvoorbeeld worden uitgevoerd met kennis van de geretourneerde HANDLE

 SetThreadPriority ( hThread , THREAD_PRIORITY_BELOW_NORMAL );

of om de geretourneerde waarde van de aangeroepen methode runInThread (in het voorbeeld 0):

 DWORD dwExitCode ;
GetExitCodeThread ( hThread , & dwExitCode );

moeilijkheden

Het gebruik van threads en eenvoudige synchronisatiemechanismen zoals mutexen en semaforen blijkt in de praktijk veeleisend. Aangezien de programmastroom niet langer eenvoudig sequentieel is, is het voor een ontwikkelaar moeilijk om deze te voorspellen. Aangezien de uitvoeringsvolgorde en de verandering tussen de threads wordt geregeld door de planner en de ontwikkelaar hier weinig invloed op heeft, kan een gelijktijdig programma gemakkelijk in een voorheen onbedoelde algehele staat terechtkomen, wat zich uit in deadlocks , live locks , datafouten en loopt vast. Deze effecten komen sporadisch voor en zijn daarom nauwelijks reproduceerbaar, wat het oplossen van problemen in een toepassing moeilijk maakt.

Thread-representatie in UML

In de Unified Modeling Language (UML) worden parallelle processen vaak weergegeven met statusdiagrammen . In een toestandsdiagram kunnen interne parallelle gedeeltelijke toestandsdiagrammen binnen een toestand worden weergegeven. Alle toestandsdiagrammen van het totale systeem worden quasi-parallel verwerkt. Het quasi-parallelisme wordt bereikt door het feit dat elke toestandsovergang erg kort is (in de praktijk enkele microseconden tot milliseconden) en daarom lijkt de opeenvolgende verwerking parallel te verlopen. De overgang van de ene toestand naar de andere wordt meestal getriggerd door een gebeurtenis die eerder in de zogenaamde gebeurteniswachtrij is geschreven. Volgens de hierboven gegeven definitie is deze overgang als gevolg van een gebeurtenis een gebruikersthread. In principe kan het op deze manier geïmplementeerde parallellisme worden bereikt met slechts een enkele thread van het besturingssysteem.

Als UML wordt gebruikt voor snelle systemen, dan speelt de kwestie van tijdprioritering een rol. Als statusovergangen lang kunnen duren of als een overgang ook op voorwaarden moet wachten (gebeurt al bij het lezen of schrijven naar een bestand), dan moet parallellisme met threads worden geïmplementeerd. Om deze reden moet de verwerking van het statusdiagram kunnen worden toegewezen aan verschillende threads van het systeem, die verschillende prioriteiten kunnen hebben. De UML-tool Rhapsody kent hiervoor de term active class . Elke actieve klasse krijgt een eigen thread toegewezen.

Naast het formuleren van parallel werk met toestandsdiagrammen, kan parallellisme met threads ook worden gemodelleerd in door UML ontworpen systemen. Hiervoor kan het programmeermodel van Java worden gebruikt. In dit geval moet een expliciete Thread-klasse met de in Java bekende eigenschappen in het gebruikersmodel worden opgenomen. Dit maakt het gemakkelijker en effectiever om zeer cyclische problemen het hoofd te bieden, zoals het volgende voorbeeld laat zien:

 ongeldig uitvoeren ()
{
   while ( not_abort ) // cyclisch tot de externe abort
   {
      gegevens . wacht (); // de cyclus begint wanneer er gegevens zijn
      iets doen (); // Verwerking van verschillende dingen
      als ( voorwaarde )
      {
         doeTheRightThing (); // Verwerking is afhankelijk van voorwaarden
         partnergegevens . informeren (); // breng andere threads op de hoogte
      }
   }
}

De methode run () die hier wordt getoond, is een methode van een gebruikersklasse, daarin wordt de volledige verwerking in de thread beschreven in programmaregels, zoals gebruikelijk is bij functionele verwerking in de UML. De UML wordt gebruikt om deze gebruikersklasse, de bijbehorende klasse Thread en hun relaties (toon klassendiagram ), aangevuld met sequence diagrammen , bijvoorbeeld. De programmering is duidelijk. Een toestandsdiagram biedt in dit geval geen betere grafische mogelijkheden.

Zie ook

literatuur

web links

Individueel bewijs

  1. cf. gotw.ca