Skip to content

8. De uitvoeringsthreads

8.1. Inleiding

Wanneer een applicatie wordt gestart, wordt deze uitgevoerd in een uitvoeringsstroom die een thread wordt genoemd. De klasse .NET die een thread modelleert, is de klasse System.Threading.Thread en heeft de volgende definitie:

Image

We zullen slechts enkele eigenschappen en methoden van deze klasse gebruiken:

CurrentThread - propriété statique
geeft de thread die momenteel wordt uitgevoerd
Name - propriété d'objet
naam van de thread
isAlive - propriété d'objet
geeft aan of de thread actief is (true) of niet (false)
Start - méthode d'objet
start de uitvoering van een thread
Abort - méthode d'objet
stopt de uitvoering van een thread definitief
Sleep(n) - méthode statique
stopt de uitvoering van een thread gedurende n milliseconden
Suspend() - méthode d'objet
schort de uitvoering van een thread tijdelijk op
Resume() - méthode d'objet
hervat de uitvoering van een onderbroken thread
Join() - méthode d'objet
blokkerende bewerking – wacht tot de thread is voltooid om door te gaan naar de volgende instructie

Laten we eens kijken naar een eerste toepassing die het bestaan van een hoofduitvoeringsthread laat zien, namelijk degene waarin de functie Main van een klasse wordt uitgevoerd:


' gebruik van threads
Imports System
Imports System.Threading

Public Module thread1
    Public Sub Main()
        ' huidige thread initialiseren
        Dim main As Thread = Thread.CurrentThread
        ' weergave
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' de naam wordt gewijzigd
        main.Name = "main"
        ' controle
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' oneindige lus
        While True
            ' weergave
            Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
            ' tijdelijke stop
            Thread.Sleep(1000)
        End While
    End Sub
End Module

De schermresultaten:

dos>thread1
Thread courant :
Thread courant : main
main : 06:13:55
main : 06:13:56
main : 06:13:57
main : 06:13:58
main : 06:13:59

Het voorgaande voorbeeld illustreert de volgende punten:

  • de functie Main wordt correct uitgevoerd in een thread
  • men heeft toegang tot de kenmerken van deze thread via Thread.CurrentThread
  • de rol van de methode Sleep. Hier gaat de thread die Main uitvoert regelmatig 1 seconde in slaapstand tussen twee weergaven.

8.2. Aanmaken van uitvoeringsthreads

Er zijn toepassingen mogelijk waarbij stukken code „gelijktijdig“ in verschillende uitvoeringsthreads worden uitgevoerd. Wanneer men zegt dat thread'en gelijktijdig worden uitgevoerd, is dat vaak een verkeerde benaming. Als de machine slechts één processor heeft, zoals nog vaak het geval is, delen de thread'en deze processor: ze beschikken er om de beurt gedurende een kort moment (enkele milliseconden) over. Dit wekt de illusie van parallelle uitvoering. De tijd die aan een thread wordt toegewezen, hangt af van verschillende factoren, waaronder de prioriteit ervan, die een standaardwaarde heeft maar ook programmatisch kan worden ingesteld. Wanneer een thread de processor tot zijn beschikking heeft, gebruikt hij deze normaal gesproken gedurende de volledige tijd die hem is toegewezen. Hij kan deze echter ook vroegtijdig vrijgeven:

  • door te wachten op een gebeurtenis (wait, join, suspend)
  • door gedurende een bepaalde tijd in slaapstand te gaan (sleep)
  1. Een thread T wordt eerst aangemaakt door de constructor
Public Sub New(ByVal start As ThreadStart)

ThreadStart is van het type delegate en definieert het prototype van een functie zonder parameters:

Public Delegate Sub ThreadStart()

Een klassieke constructie ziet er als volgt uit:

dim T as Thread=new Thread(new ThreadStart(run));

De functie run, die als parameter wordt doorgegeven, wordt uitgevoerd bij het starten van de thread.

  1. De uitvoering van thread T wordt gestart door T.Start(): de functie [run] die aan de constructor van T wordt doorgegeven, wordt vervolgens door de thread T uitgevoerd. Het programma dat de instructie T.start() uitvoert, wacht niet tot de taak T is voltooid: het gaat onmiddellijk verder met de volgende instructie. Er zijn dan twee taken die parallel worden uitgevoerd. Deze moeten vaak met elkaar kunnen communiceren om te weten hoe ver het gezamenlijke werk is gevorderd. Dit is het probleem van de synchronisatie van threads.
  1. Eenmaal gestart, wordt de thread autonoom uitgevoerd. Deze stopt wanneer de functie start die hij uitvoert, zijn werk heeft voltooid.
  1. Er kunnen bepaalde signalen naar taak T worden gestuurd:
    1. T.Suspend() geeft aan dat hij tijdelijk moet stoppen
    2. T.Resume() geeft aan dat de taak zijn werk moet hervatten
    3. T.Abort() geeft aan dat de taak definitief moet stoppen
  1. Je kunt ook wachten tot de taak is voltooid met T.join(). Dit is een blokkerende instructie: het programma dat deze uitvoert, wordt geblokkeerd totdat taak T zijn werk heeft voltooid. Dit is een manier om te synchroniseren.

Laten we het volgende programma eens bekijken:


' opties
Option Strict On
Option Explicit On 

' naamruimten
Imports System
Imports System.Threading

Module thread2
    Public Sub Main()
        ' huidige thread initialiseren
        Dim main As Thread = Thread.CurrentThread
        ' de thread een naam toekennen
        main.Name = "main"

        ' uitvoeringsthreads aanmaken
        Dim tâches(4) As Thread
        Dim i As Integer
        For i = 0 To tâches.Length - 1
            ' de thread i wordt aangemaakt
            tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
            ' de naam van de thread wordt vastgelegd
            tâches(i).Name = "tache_" & i
            ' de thread i wordt gestart
            tâches(i).Start()
        Next i
        ' einde van de hoofdfunctie
        Console.Out.WriteLine(("fin du thread " + main.Name))
    End Sub

    Public Sub affiche()
        ' weergave van het begin van de uitvoering
        Console.Out.WriteLine(("Début d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
        ' 1 seconde in slaapstand
        Thread.Sleep(1000)
        ' weergave einde uitvoering
        Console.Out.WriteLine(("Fin d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
    End Sub
End Module

De hoofdthread, die de functie Main uitvoert, maakt 5 andere threads aan die de statische methode affiche moeten uitvoeren. De resultaten zijn als volgt:

dos>thread2
fin du thread main
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:53
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:54

Deze resultaten zijn zeer leerzaam:

  • allereerst zien we dat het starten van een thread geen blokkering veroorzaakt. De methode Main heeft de uitvoering van 5 threads parallel gestart en was zelf al klaar voordat de threads klaar waren. De bewerking
             ' de thread i wordt gestart
            tâches(i).Start()

start de uitvoering van de thread tâches[i], maar zodra dit is gebeurd, gaat de uitvoering onmiddellijk verder met de volgende instructie, zonder te wachten tot de thread is voltooid.

  • Alle aangemaakte threads moeten de methode affiche uitvoeren. De uitvoervolgorde is onvoorspelbaar. Ook al lijkt de uitvoervolgorde in het voorbeeld de volgorde van de uitvoerverzoeken te volgen, hieruit kunnen geen algemene conclusies worden getrokken. Het besturingssysteem beschikt hier over 6 threads en één processor. Het verdeelt de processor over deze 6 threads volgens zijn eigen regels.
  • In de resultaten zien we een gevolg van de methode Sleep. In het voorbeeld is het thread 0 die als eerste de methode affiche uitvoert. Het bericht dat de uitvoering begint, wordt weergegeven, waarna de methode Sleep wordt uitgevoerd, die de thread gedurende 1 seconde opschort. De thread verliest dan de processor, die daardoor beschikbaar komt voor een andere thread. Het voorbeeld laat zien dat thread 1 de processor krijgt. Thread 1 volgt hetzelfde traject, net als de andere threads. Wanneer de slaapperiode van 1 seconde van thread 0 is verstreken, kan de uitvoering ervan worden hervat. Het systeem wijst de processor aan deze thread toe en deze kan de uitvoering van de methode affiche voltooien.

Laten we ons programma aanpassen om de methode Main af te ronden met de instructies:

         ' einde van de hand
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

De uitvoer van het nieuwe programma is:

fin du thread main

De threads die door de functie Main zijn aangemaakt, worden niet uitgevoerd. Dit is de instructie

        Environment.Exit(0)

die hiervoor zorgt: deze verwijdert alle threads van de applicatie en niet alleen de thread Main. De oplossing voor dit probleem is dat de methode Main wacht tot de door haar aangemaakte threads zijn voltooid, voordat ze zelf wordt beëindigd. Dit kan worden gedaan met de methode Join van de klasse Thread:


         ' wachten op het einde van de uitvoering van alle threads
        For i = 0 To tâches.Length - 1
            ' wachten op het einde van de uitvoering van thread i
            tâches(i).Join()
        Next i        'for
         ' einde van de hoofdprocedure
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

Dit levert dan de volgende resultaten op:

Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:48
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:50
fin du thread main

8.3. Het belang van threads

Nu we hebben aangetoond dat er standaard één thread bestaat – namelijk degene die de methode Main uitvoert – en nu we weten hoe we andere threads kunnen aanmaken, gaan we even stilstaan bij het nut van threads voor ons en bij de reden waarom we ze hier behandelen. Er is een soort applicaties die zich goed lenen voor het gebruik van threads, namelijk client-server-applicaties op het internet. In zo’n toepassing beantwoordt een server op een machine S1 verzoeken van clients op externe machines C1, C2, …, Cn.

We maken dagelijks gebruik van internettoepassingen die aan dit schema voldoen: webservices, e-mail, het raadplegen van forums, bestandsoverdracht... In het bovenstaande schema moet de server S1 de clients Ci gelijktijdig bedienen. Als we het voorbeeld nemen van een server FTP (File Transfer Protocol) die bestanden aan zijn clients levert, weten we dat een bestandsoverdracht soms meerdere uren kan duren. Het is natuurlijk uitgesloten dat één client de server zo lang in zijn eentje monopoliseert. Wat gewoonlijk gebeurt, is dat de server evenveel uitvoeringsthreads aanmaakt als er clients zijn. Elke thread is dan verantwoordelijk voor een specifieke client. Aangezien de processor cyclisch wordt verdeeld over alle actieve threads van de machine, besteedt de server een beetje tijd aan elke client, waardoor de gelijktijdigheid van de dienstverlening wordt gewaarborgd.

8.4. Toegang tot gedeelde bronnen

In het hierboven genoemde client-servervoorbeeld bedient elke thread een client grotendeels onafhankelijk. Toch kan het voorkomen dat de threads moeten samenwerken om de door hun client gevraagde dienst te verlenen, met name bij de toegang tot gedeelde bronnen. Het bovenstaande schema doet denken aan de loketten van een grote overheidsinstantie, bijvoorbeeld een postkantoor, waar bij elk loket een medewerker een klant bedient. Stel dat deze medewerkers af en toe kopieën moeten maken van documenten die hun klanten meebrengen en dat er slechts één kopieermachine is. Twee medewerkers kunnen de kopieermachine niet tegelijkertijd gebruiken. Als medewerker i merkt dat de kopieermachine door medewerker j wordt gebruikt, moet hij wachten. Deze situatie wordt ‘toegang tot een gedeelde hulpbron’ genoemd en is in de informatica vrij lastig te beheren. Laten we het volgende voorbeeld nemen:

  • een applicatie genereert n threads, waarbij n als parameter wordt doorgegeven
  • de gedeelde hulpbron is een teller die door elke gegenereerde thread moet worden verhoogd
  • Aan het einde van de toepassing wordt de waarde van de teller weergegeven. We zouden dus n moeten vinden.

Het programma ziet er als volgt uit:


' opties
Option Explicit On 
Option Strict On

' gebruik van threads
Imports System
Imports System.Threading

Public Class thread3
    ' klassevariabelen
    Private Shared cptrThreads As Integer = 0

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' gebruiksaanwijzing
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' controle van het aantal argumenten
        If args.Length <> 1 Then
            ' fout
            Console.Error.WriteLine(syntaxe)
            ' afsluiten
            Environment.Exit(1)
        End If
        ' controle van de kwaliteit van het argument
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
            ' fout
            Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
            ' einde
            Environment.Exit(2)
        End Try
        ' aanmaken en genereren van threads
        Dim threads(nbThreads - 1) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' aanmaken
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' naamgeving
            threads(i).Name = "tache_" & i
            ' starten
            threads(i).Start()
        Next i
        ' wachten tot de threads zijn voltooid
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i        ' affichage compteur
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' verhoogt de threadteller
        ' teller uitlezen
        Dim valeur As Integer = cptrThreads
        ' opvolging
        Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' wachten
        Thread.Sleep(1000)
        ' teller verhogen
        cptrThreads = valeur + 1
        ' opvolging
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
    End Sub
End Class

We zullen niet verder ingaan op het gedeelte over het genereren van threads, dat we al hebben behandeld. Laten we ons in plaats daarvan richten op de methode incrémente, die door elke thread wordt gebruikt om de statische teller cptrThreads te verhogen.

  1. de teller wordt uitgelezen
  2. de thread stopt 1 s. Hij verliest dus de processor
  3. de teller wordt verhoogd

Stap 2 is er alleen om de thread te dwingen de processor te verliezen. Deze wordt vervolgens aan een andere thread toegewezen. In de praktijk is er geen garantie dat een thread niet wordt onderbroken tussen het moment waarop hij de teller leest en het moment waarop hij deze met 1 verhoogt. Het risico bestaat dat de processor wordt vrijgegeven tussen het moment waarop de waarde van de teller wordt uitgelezen en het moment waarop de met 1 verhoogde waarde wordt geschreven. De verhogingsbewerking bestaat namelijk uit meerdere elementaire instructies op processorniveau die kunnen worden onderbroken. Stap 2, waarbij de thread één seconde in slaapstand gaat, is er dus alleen om dit risico te systematiseren. De verkregen resultaten zijn als volgt:

dos>thread3 5
A 05:44:34, le thread tache_0 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_1 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_2 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_3 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_4 a lu la valeur du compteur : 0
A 05:44:35, le thread tache_0 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_1 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_2 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_3 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_4 a écrit la valeur du compteur : 1
Nombre de threads générés : 1

Als we deze resultaten bekijken, zien we duidelijk wat er gebeurt:

  • een eerste thread leest de teller af. Hij vindt 0.
  • hij stopt 1 s en geeft de processor dus vrij
  • een tweede thread neemt vervolgens de processor over en leest ook de waarde van de teller. Deze staat nog steeds op 0, aangezien de vorige thread deze nog niet heeft verhoogd. Ook deze stopt 1 s.
  • In 1 seconde hebben alle 5 threads de tijd gehad om langs te komen en allemaal de waarde 0 te lezen.
  • Wanneer ze een voor een weer actief worden, zullen ze de waarde 0 die ze hebben gelezen verhogen en de waarde 1 in de teller schrijven, wat door het hoofdprogramma (Main) wordt bevestigd.

Waar komt het probleem vandaan? De tweede thread heeft een verkeerde waarde gelezen omdat de eerste thread was onderbroken voordat hij zijn taak had voltooid, namelijk het bijwerken van de teller in het venster. Dit brengt ons bij het begrip ‘kritieke resource’ en ‘kritieke sectie’ van een programma:

  • een kritieke resource is een resource die slechts door één thread tegelijk kan worden bezet. In dit geval is de kritieke resource de teller.
  • Een kritieke sectie van een programma is een reeks instructies in de uitvoeringsstroom van een thread waarin deze toegang heeft tot een kritieke resource. Er moet worden gewaarborgd dat deze thread tijdens deze kritieke sectie als enige toegang heeft tot de resource.

8.5. Exclusieve toegang tot een gedeelde hulpbron

In ons voorbeeld is de kritieke sectie de code tussen het uitlezen van de teller en het schrijven van de nieuwe waarde:


         ' meteraflezing
        Dim valeur As Integer = cptrThreads
        ' in afwachting
        Thread.Sleep(1000)
        ' teller verhogen
        cptrThreads = valeur + 1

Om deze code uit te voeren, moet een thread gegarandeerd als enige actief zijn. De thread mag wel worden onderbroken, maar tijdens die onderbreking mag geen andere thread dezelfde code uitvoeren. Het platform .NET biedt verschillende hulpmiddelen om te zorgen dat kritieke codegedeelten slechts door één thread tegelijk worden uitgevoerd. We gebruiken hiervoor de klasse Mutex:

Image

We gebruiken hier alleen de volgende constructors en methoden:

public Mutex()
maakt een M-synchronisatieobject aan
public bool WaitOne()
De thread T1 die de bewerking M.WaitOne() uitvoert, vraagt de eigendom van het synchronisatieobject M aan. Als de mutex M door geen enkele thread wordt vastgehouden (wat in het begin het geval is), wordt deze „toegewezen” aan de thread T1 die erom heeft gevraagd. Als even later een thread T2 dezelfde bewerking uitvoert, wordt deze geblokkeerd. Een mutex kan namelijk slechts aan één thread toebehoren. De thread wordt gedeblokkeerd zodra de thread T1 de mutex M vrijgeeft die hij in bezit heeft. Zo kunnen meerdere threads geblokkeerd raken terwijl ze wachten op de mutex M.
public void
ReleaseMutex()
De thread T1 die de bewerking M.ReleaseMutex() uitvoert, geeft de bezit van de mutex M.Lorsque op; de thread T1 verliest de processor, waarna het systeem deze kan toewijzen aan een van de threads die op de mutex M wachten. Slechts één thread zal deze op zijn beurt krijgen; de andere threads die op M wachten, blijven geblokkeerd

Een mutex M regelt de toegang tot een gedeelde bron R. Een thread vraagt de bron R aan via M.WaitOne() en geeft deze terug via M.ReleaseMutex(). Een kritiek gedeelte van de code dat slechts door één thread tegelijk mag worden uitgevoerd, is een gedeelde bron. De uitvoering van de kritieke sectie kan als volgt worden gesynchroniseerd:

M.WaitOne()
' alleen deze thread komt hier binnen
' kritieke sectie
....
M.ReleaseMutex()

waarbij M een object Mutex is. Men mag natuurlijk nooit vergeten een Mutex vrij te geven dat niet langer nodig is, zodat een andere thread de kritieke sectie kan betreden; anders zullen de threads die wachten op een nooit vrijgegeven mutex nooit toegang krijgen tot de processor. Bovendien moet een deadlock-situatie (deadlock) worden vermeden, waarbij twee threads op elkaar wachten. Laten we de volgende acties bekijken die elkaar in de tijd opvolgen:

  • een thread T1 verkrijgt de eigendom van een mutex M1 om toegang te krijgen tot een gedeelde bron R1
  • een thread T2 verkrijgt de eigendom van een mutex M2 om toegang te krijgen tot een gedeelde bron R2
  • de thread T1 vraagt de mutex M2 aan. Hij wordt geblokkeerd.
  • De thread T2 vraagt de mutex M1 aan. Hij is geblokkeerd.

Hier wachten de threads T1 en T2 op elkaar. Dit komt voor wanneer threads twee gedeelde bronnen nodig hebben: de bron R1, die wordt beheerd door de mutex M1, en de bron R2, die wordt beheerd door de mutex M2. Een mogelijke oplossing is om beide resources tegelijkertijd aan te vragen met behulp van één enkele mutex M. Dit is echter niet altijd mogelijk, bijvoorbeeld als dit leidt tot een langdurige bezetting van een kostbare resource. Een andere oplossing is dat een thread die M1 bezit en M2 niet kan verkrijgen, vervolgens M1 vrijgeeft om een deadlock te voorkomen. Als we wat we zojuist hebben gezien toepassen op het vorige voorbeeld, ziet onze toepassing er als volgt uit:


' opties
Option Explicit On 
Option Strict On

' gebruik van threads
Imports System
Imports System.Threading

Public Class thread4
    ' klassevariabelen
    Private Shared cptrThreads As Integer = 0    ' compteur de threads
    Private Shared autorisation As Mutex

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' gebruiksaanwijzing
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' controle van het aantal argumenten
        If args.Length <> 1 Then
            ' fout
            Console.Error.WriteLine(syntaxe)
            ' afsluiten
            Environment.Exit(1)
        End If
        ' controle van de kwaliteit van het argument
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
        End Try

        ' initialisatie van de toegangsautorisatie tot een kritieke sectie
        autorisation = New Mutex

        ' aanmaken en genereren van threads
        Dim threads(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' aanmaken
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' naamgeving
            threads(i).Name = "tache_" & i
            ' starten
            threads(i).Start()
        Next i
        ' wachten tot de threads zijn voltooid
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i
        ' teller weergeven
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' verhoogt de threadteller
        ' toestemming gevraagd om de kritieke sectie te betreden
        autorisation.WaitOne()
        ' teller uitlezen
        Dim valeur As Integer = cptrThreads
        ' opvolging
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' wachten
        Thread.Sleep(1000)
        ' teller verhogen
        cptrThreads = valeur + 1
        ' opvolging
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
        ' toegang wordt verleend
        autorisation.ReleaseMutex()
    End Sub
End Class

De verkregen resultaten komen overeen met wat verwacht werd:

dos>thread4 5
A 05:51:10, le thread tache_0 a lu la valeur du compteur : 0
A 05:51:11, le thread tache_0 a écrit la valeur du compteur : 1
A 05:51:11, le thread tache_1 a lu la valeur du compteur : 1
A 05:51:12, le thread tache_1 a écrit la valeur du compteur : 2
A 05:51:12, le thread tache_2 a lu la valeur du compteur : 2
A 05:51:13, le thread tache_2 a écrit la valeur du compteur : 3
A 05:51:13, le thread tache_3 a lu la valeur du compteur : 3
A 05:51:14, le thread tache_3 a écrit la valeur du compteur : 4
A 05:51:14, le thread tache_4 a lu la valeur du compteur : 4
A 05:51:15, le thread tache_4 a écrit la valeur du compteur : 5
Nombre de threads générés : 5

8.6. Synchronisatie op basis van gebeurtenissen

Laten we eens kijken naar de volgende situatie, die soms ook wel de producent-consument-situatie wordt genoemd.

  1. We hebben een tabel waarin bepaalde processen gegevens opslaan (de producenten) en andere deze gegevens ophalen (de consumenten).
  2. De producenten zijn onderling gelijk, maar sluiten elkaar uit: er kan slechts één producent tegelijk zijn gegevens in de array plaatsen.
  3. De consumenten zijn onderling gelijk maar sluiten elkaar uit: er kan slechts één lezer tegelijk de gegevens uit de tabel lezen.
  4. Een consument kan de gegevens uit de tabel pas lezen wanneer een producent er gegevens in heeft geplaatst, en een producent kan pas nieuwe gegevens in de tabel plaatsen wanneer de gegevens die erin staan, zijn verbruikt.

In deze uiteenzetting kunnen we twee gedeelde bronnen onderscheiden:

    1. de tabel die kan worden bewerkt
    2. de tabel voor lezen

De toegang tot deze twee gedeelde bronnen kan worden geregeld door middel van mutexen, zoals eerder besproken, één voor elke bron. Zodra een consument de tabel in leesmodus heeft verkregen, moet hij controleren of er daadwerkelijk gegevens in staan. Hiervoor wordt een gebeurtenis gebruikt om hem hiervan op de hoogte te stellen. Evenzo moet een producent die de tabel in schrijfmodus heeft verkregen, wachten tot een consument deze heeft leeggemaakt. Ook hier wordt weer een gebeurtenis gebruikt.

De gebruikte gebeurtenissen maken deel uit van de klasse AutoResetEvent:

Image

Dit type gebeurtenis is vergelijkbaar met een booleaanse waarde, maar voorkomt actieve of semi-actieve wachttijden. Als het schrijfrecht dus wordt gecontroleerd door een booleaanse waarde peutEcrire, zal een producent vóór het schrijven een code uitvoeren zoals:

while(peutEcrire==false)        ' attente active

of

while(peutEcrire==false) ' attente semi-active
    Thread.Sleep(100)                ' attente de 100ms
end while

Bij de eerste methode belast de thread de processor onnodig. Bij de tweede methode controleert de thread elke 100 ms de status van de booleaanse variabele peutEcrire. Met de klasse AutoResetEvent kan dit nog verder worden verbeterd: de thread vraagt om gewekt te worden zodra de gebeurtenis waarop hij wacht, heeft plaatsgevonden:

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

De bewerking

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;

initialiseert de booleaanse variabele peutEcrire tot false. De bewerking

peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

wordt uitgevoerd door een thread en zorgt ervoor dat deze doorloopt als de booleaanse waarde peutEcrire waar is, of anders wordt geblokkeerd totdat deze waar wordt. Een andere thread zal deze op waar zetten via de bewerking peutEcrire.Set() of op onwaar via de bewerking peutEcrire.Reset().

Het producent-consumentprogramma is als volgt:


' gebruik van lees- en schrijfthreads
' illustreert het gelijktijdige gebruik van gedeelde bronnen en synchronisatie

' opties
Option Explicit On 
Option Strict On

' gebruik van threads
Imports System
Imports System.Threading

Public Class lececr

    ' klassevariabelen
    Private Shared data(5) As Integer    ' ressource partagée entre threads lecteur et threads écrivain
    Private Shared lecteur As Mutex    ' variable de synchronisation pour lire le tableau
    Private Shared écrivain As Mutex    ' variable de synchronisation pour écrire dans le tableau
    Private Shared objRandom As New Random(DateTime.Now.Second)    ' un générateur de nombres aléatoires
    Private Shared peutLire As AutoResetEvent    ' signale qu'on peut lire le contenu de data
    Private Shared peutEcrire As AutoResetEvent

    Public Shared Sub Main(ByVal args() As [String])

        ' het aantal te genereren threads
        Const nbThreads As Integer = 3

        ' initialisatie van vlaggen
        peutLire = New AutoResetEvent(False)        ' on ne peut pas encore lire
        peutEcrire = New AutoResetEvent(True)        ' on peut déjà écrire

        ' initialisatie van synchronisatievariabelen
        lecteur = New Mutex         ' synchronise les lecteurs
        écrivain = New Mutex         ' synchronise les écrivains

        ' aanmaken van lezer-threads
        Dim lecteurs(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' aanmaken
            lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
            lecteurs(i).Name = "lecteur_" & i
            ' starten
            lecteurs(i).Start()
        Next i

        ' aanmaken van schrijfthreads
        Dim écrivains(nbThreads) As Thread
        For i = 0 To nbThreads - 1
            ' aanmaken
            écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
            écrivains(i).Name = "écrivain_" & i
            ' starten
            écrivains(i).Start()
        Next i

        'einde hand
        Console.Out.WriteLine("fin de Main...")
    End Sub

    ' de inhoud van de tabel lezen
    Public Shared Sub lire()
        ' kritieke sectie
        lecteur.WaitOne()        ' un seul lecteur peut passer
        peutLire.WaitOne()        ' on doit pouvoir lire

        ' tabel lezen
        Dim i As Integer
        For i = 0 To data.Length - 1
            '1 seconde wachten
            Thread.Sleep(1000)
            ' weergave
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
        Next i

        ' kan niet meer worden gelezen
        peutLire.Reset()
        ' schrijven is mogelijk
        peutEcrire.Set()
        ' einde kritieke sectie
        lecteur.ReleaseMutex()
    End Sub

    ' in de tabel schrijven
    Public Shared Sub écrire()
        ' kritieke sectie
        ' slechts één schrijver mag door
        écrivain.WaitOne()
        ' we moeten wachten op schrijftoestemming
        peutEcrire.WaitOne()

        ' schrijven in tabel
        Dim i As Integer
        For i = 0 To data.Length - 1
            '1 seconde wachten
            Thread.Sleep(1000)
            ' weergave
            data(i) = objRandom.Next(0, 1000)
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : L'écrivain " & Thread.CurrentThread.Name & " a écrit le nombre " & data(i)))
        Next i

        ' schrijven is niet meer mogelijk
        peutEcrire.Reset()
        ' lezen is mogelijk
        peutLire.Set()
        'einde kritieke sectie
        écrivain.ReleaseMutex()
    End Sub
End Class

De uitvoering levert de volgende resultaten op:

dos>lececr
fin de Main...
05:56:56 : L'écrivain écrivain_0 a écrit le nombre 459
05:56:57 : L'écrivain écrivain_0 a écrit le nombre 955
05:56:58 : L'écrivain écrivain_0 a écrit le nombre 212
05:56:59 : L'écrivain écrivain_0 a écrit le nombre 297
05:57:00 : L'écrivain écrivain_0 a écrit le nombre 37
05:57:01 : L'écrivain écrivain_0 a écrit le nombre 623
05:57:02 : Le lecteur lecteur_0 a lu le nombre 459
05:57:03 : Le lecteur lecteur_0 a lu le nombre 955
05:57:04 : Le lecteur lecteur_0 a lu le nombre 212
05:57:05 : Le lecteur lecteur_0 a lu le nombre 297
05:57:06 : Le lecteur lecteur_0 a lu le nombre 37
05:57:07 : Le lecteur lecteur_0 a lu le nombre 623
05:57:08 : L'écrivain écrivain_1 a écrit le nombre 549
05:57:09 : L'écrivain écrivain_1 a écrit le nombre 34
05:57:10 : L'écrivain écrivain_1 a écrit le nombre 781
05:57:11 : L'écrivain écrivain_1 a écrit le nombre 555
05:57:12 : L'écrivain écrivain_1 a écrit le nombre 812
05:57:13 : L'écrivain écrivain_1 a écrit le nombre 406
05:57:14 : Le lecteur lecteur_1 a lu le nombre 549
05:57:15 : Le lecteur lecteur_1 a lu le nombre 34
05:57:16 : Le lecteur lecteur_1 a lu le nombre 781
05:57:17 : Le lecteur lecteur_1 a lu le nombre 555
05:57:18 : Le lecteur lecteur_1 a lu le nombre 812
05:57:19 : Le lecteur lecteur_1 a lu le nombre 406
05:57:20 : L'écrivain écrivain_2 a écrit le nombre 442
05:57:21 : L'écrivain écrivain_2 a écrit le nombre 83
^C

De volgende punten vallen op:

  • er is inderdaad slechts één lezer tegelijk, hoewel deze de processor verliest in de kritieke sectie lire
  • er is inderdaad slechts één schrijver tegelijk, hoewel deze de processor verliest in de kritieke sectie écrire
  • een lezer leest alleen wanneer er iets te lezen valt in de tabel
  • een schrijver schrijft alleen wanneer de tabel volledig is gelezen