Threads, racecondities en vergrendelingen

Leer hoe threads één programma meerdere dingen tegelijk laten doen en waarom het delen van gegevens tussen threads risico's meebrengt. Maak threads en wacht op hun beëindiging met POSIX-threads in C, volg hoe wijzigingen door racecondities verloren gaan en bescherm kritieke secties met mutexen. Ontdek hoe vergrendelingen worden opgebouwd met atomaire hardware-instructies zoals test-and-set en compare-and-swap, waarom besturingssystemen wachtende threads laten slapen en hoe je tellers, lijsten, wachtrijen en hashtabellen ontwerpt die met veel threads correct en snel blijven.

GemiddeldLes 5 van 9 in Cursus Besturingssystemen: processen, geheugen, threads en bestandssystemen

Wat je leert

  1. Beschrijf hoe de threads van één proces een adresruimte delen terwijl elke thread zijn eigen uitvoeringstoestand behoudt, en schrijf C-programma's die threads maken, er gegevens aan doorgeven en wachten tot ze klaar zijn.
    • Vergelijk threads met processen
      Leg uit wat een thread zelf heeft, wat die deelt met de andere threads van zijn proces, waarom programma's threads gebruiken en hoe een wisseling tussen threads verschilt van een wisseling tussen processen.
    • Maak threads, geef gegevens door en wacht op hun beëindiging met pthreads
      Gebruik de POSIX-thread-API in C om threads te starten, elke thread argumenten te geven, hun resultaten te verzamelen en ervoor te zorgen dat het programma vóór beëindiging op ze wacht.
  2. Volg hoe vervlochten machine-instructies op gedeelde gegevens onjuiste resultaten opleveren, benoem racecondities en kritieke secties in code en geef aan wat een correcte oplossing voor kritieke secties moet garanderen.
    • Volg hoe verhogingen verloren gaan bij onbeschermde wijzigingen
      Splits een wijziging van gedeelde gegevens op in machinestappen, volg vervlechtingen waarbij wijzigingen verloren gaan en leg uit waarom zulke fouten bij het testen zeldzaam zijn maar toch bestaan.
    • Herken racecondities, kritieke secties en atomaire bewerkingen
      Definieer de kernbegrippen van gelijktijdige uitvoering, herken racecondities bij lezen-wijzigen-schrijven en controleren en vervolgens handelen in code, en geef aan aan welke eisen een correcte oplossing voor het probleem van de kritieke sectie moet voldoen.
  3. Gebruik POSIX-mutexen om kritieke secties atomair te maken, bepaal wat elke vergrendeling beschermt en hoeveel code eronder valt, en voorkom fouten die vergrendeling ongemerkt onwerkzaam maken.
    • Vergrendel een POSIX-mutex vóór een kritieke sectie en ontgrendel die erna
      Leg uit wat een vergrendeling garandeert, initialiseer en gebruik een pthread-mutex correct en verhelp daarmee racecondities bij een gedeelde teller en een bankrekening.
    • Kies de vergrendelingsgranulariteit en beoordeel vergrendelingen
      Bepaal hoeveel gegevens elke vergrendeling beschermt en hoeveel code eronder valt, en beoordeel een vergrendeling op juistheid, eerlijkheid en prestaties.
    • Voorkom veelgemaakte fouten bij vergrendeling
      Herken de typische fouten waardoor gegevens onbeschermd blijven of threads voor altijd blijven wachten, en herstel elke fout.
  4. Laat zien waarom gewone laad- en opslagbewerkingen op moderne hardware niet voldoende zijn voor een correcte vergrendeling, bouw spinlocks met atomaire instructies, maak ze eerlijk en leg uit hoe ondersteuning door het besturingssysteem onnodig actief wachten voorkomt.
    • Leg uit waarom eenvoudige pogingen om een vergrendeling te bouwen mislukken
      Analyseer drie vroege benaderingen voor wederzijdse uitsluiting, interrupts uitschakelen, een gewone vlag en het algoritme van Peterson, en leg uit waar elke benadering tekortschiet.
    • Bouw een spinlock met atomaire instructies
      Beschrijf test-and-set, compare-and-swap, load-linked/store-conditional en fetch-and-add, bouw daarmee een spinlock en beoordeel die.
    • Maak spinlocks eerlijk met een ticketvergrendeling
      Bouw een ticketvergrendeling met fetch-and-add, volg het verloop ervan en leg uit hoe die garandeert dat elke wachtende thread aan de beurt komt.
    • Voorkom onnodig actief wachten door de CPU-beurt af te staan, te slapen en tweefasenvergrendelingen te gebruiken
      Leg uit hoe het afstaan van de CPU-beurt, slapen in een wachtrij, futexen en tweefasenvergrendelingen verspilde CPU-tijd verminderen, bepaal wanneer actief wachten de moeite waard is en herken prioriteitsinversie.
  5. Maak tellers, gekoppelde lijsten, wachtrijen en hashtabellen met vergrendelingen veilig voor veel threads en beoordeel elk ontwerp op juistheid en op hoe goed het schaalt wanneer threads worden toegevoegd.
    • Bouw schaalbare tellers
      Bescherm een teller met één vergrendeling, leg uit waarom die slecht schaalt en bouw een benaderende teller met tellers per processorkern en een drempel.
    • Maak gekoppelde lijsten threadveilig
      Bescherm een gekoppelde lijst met één vergrendeling, houd de kritieke secties klein en zorg dat elk pad ontgrendelt, en beoordeel wanneer vergrendeling per knoop helpt.
    • Ontwerp concurrente wachtrijen en hashtabellen
      Bouw een wachtrij met twee vergrendelingen en een dummyknoop en een hashtabel met een vergrendeling per bucket, en gebruik metingen om fijnmazigere vergrendeling toe te passen.

Threads, racecondities en vergrendelingen: uitleg

5 hoofdstukken

1

Threads en hun gedeelde geheugen

Een proces geeft een programma een adresruimte; threads geven het meerdere uitvoeringspaden binnen die ruimte. Maak eerst onderscheid tussen de toestand die elke thread bij zich draagt en de objecten die ze allemaal kunnen bereiken. Gebruik daarna de POSIX-threadinterface om werkthreads te starten en hun resultaten te verzamelen. De belangrijkste ontwerpvragen zijn wie eigenaar is van elk argument, hoe lang het geheugen ervan blijft bestaan en wanneer de aanmakende thread het antwoord veilig mag lezen.

1.1 Eén adresruimte, meerdere uitvoeringspaden

Een thread is één uitvoeringsreeks binnen een proces. Meerdere threads kunnen aan dezelfde programmagegevens werken, maar elke thread heeft een eigen positie in de code en eigen onafgemaakte functieaanroepen nodig.

Wat wordt gedeeld en wat hoort bij een thread?

Threads in één proces delen code, globale en statische variabelen, heapobjecten, open bestandsdescriptoren en de proces-ID. Elke thread heeft een eigen programmateller, registers, stack en thread-ID. Een globale int total duidt één gedeeld object aan. Een gewone lokale int i in de werkthreadfunctie duidt bij elke aanroep een ander object aan.

Een proces met drie threads heeft drie stacks op verschillende adressen in dezelfde adresruimte. Een stack is geen beschermde afgeschermde ruimte: een andere thread kan een lokale variabele bereiken als die het adres ervan krijgt. Dat delen is alleen geldig zolang de variabele bestaat. Een pointer naar het beëindigde stackframe van een functie kan de variabele niet in leven houden. Grote lokale arrays en diepe recursie kunnen de stack van een thread ook uitputten.

Wisselen tussen threads

Het besturingssysteem slaat de uitvoeringstoestand van elke thread op in een threadbesturingsblok, of TCB. Wanneer het van T1 naar T2 wisselt, slaat het de registers van T1 op en herstelt het de registers van T2. De onafgemaakte berekening van T1 blijft in de opgeslagen registers staan totdat T1 weer draait.

Bij threads in hetzelfde proces blijft de adresruimte tijdens de wisseling gelijk. De paginatabellen hoeven niet te veranderen, wat deze wisseling meestal goedkoper maakt dan wisselen tussen processen. Gedeeld geheugen betekent ook gedeelde fouten: een schrijfbewerking via een ongeldige pointer kan de gegevens van een andere thread beschadigen en een fout die het proces beëindigt, beëindigt elke thread.

Waarom meerdere threads gebruiken?

Parallellisme betekent uitvoering op hetzelfde moment op verschillende processorkernen. Vier onafhankelijke delen van een berekening kunnen op vier kernen een bijna viervoudige versnelling bereiken, als gedeeld werk en coördinatie geen knelpunten worden. Met threads kan een programma ook wachten met werk laten overlappen: terwijl één thread wacht op een schijfleesbewerking of netwerkantwoord, kan een andere de CPU gebruiken.

Gelijktijdigheid betekent dat uitvoeringen gedurende overlappende perioden voortgang maken. Op één kern wisselt de scheduler tussen threads, zodat ze gelijktijdig actief zijn zonder parallel te draaien. Ook op één kern kunnen fouten door gelijktijdige uitvoering optreden: een wisseling kan een wijziging van gedeelde gegevens tussen de machine-instructies onderbreken.

Wie plant de threads in?

Linux en Windows gebruiken een een-op-een-model: elke gewone toepassingsthread komt overeen met een thread die door de kernel wordt ingepland. Verschillende threads kunnen op verschillende kernen draaien en een blokkerende aanroep blokkeert de aanroepende thread. Een veel-op-een-bibliotheek plant meerdere gebruikersthreads op één kernelthread in. De wisselingen kunnen goedkoop zijn, maar de bibliotheek kan die threads niet op meerdere kernen laten draaien en een blokkerende systeemaanroep kan de hele groep stoppen.

Als je A vóór B aanmaakt, is dat geen garantie dat A als eerste draait. Als A “A” print en B “B” print, kan zowel “AB” als “BA” verschijnen. De aanmaakvolgorde bepaalt wanneer een thread beschikbaar komt om te draaien; de scheduler kiest wanneer die wordt uitgevoerd.

Belangrijkste punten

  • Threads delen de adresruimte van het proces, maar hebben elk eigen registers, een eigen programmateller en een eigen stack.
  • Een pointer kan een stackvariabele gedeeld maken; de levensduur ervan eindigt nog steeds met de bijbehorende functie.
  • Gelijktijdigheid is mogelijk op één kern; parallelle uitvoering vereist meerdere kernen.
  • De scheduler kiest welke uitvoerbare thread als volgende draait.
  • Het een-op-een-threadmodel ondersteunt parallelle uitvoering en blokkering per thread.

1.2 Start werkthreads en verzamel hun resultaten

De interface voor POSIX-threads, of pthreads, start een C-functie in een andere thread. Neem <pthread.h> op en bouw met een commando zoals gcc -pthread -o sum sum.c. De optie -pthread levert de compiler- en linkerinstellingen voor code met threads.

Maak een thread met stabiele argumenten

De aanroep pthread_create(&t, NULL, worker, arg) slaat een thread-ID op in t en start worker(arg). De attribuutpointer NULL vraagt om standaardwaarden. Een werkthreadfunctie heeft de signatuur void *worker(void *arg): het argument en de retourwaarde zijn pointers. Met een struct kan die ene pointer meerdere velden doorgeven.

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

struct range { int lo, hi; long result; };

void *sum_range(void *arg) {
    struct range *r = arg;
    long sum = 0;
    for (int n = r->lo; n <= r->hi; n++)
        sum += n;
    r->result = sum;
    return NULL;
}

De aanmakende thread bereidt twee structs voor, {1, 50, 0} en {51, 100, 0}, en geeft aan elke werkthread een ander adres door. Houd beide structs in leven totdat de werkthreads klaar zijn. Elke werkthread telt op in zijn eigen lokale sum en schrijft alleen naar zijn eigen veld result.

pthread_create geeft bij succes 0 terug en bij een fout een foutnummer. Controleer dat teruggegeven nummer rechtstreeks; de functie stelt errno niet in om de fout te melden. Alleen een geslaagde aanmaak levert een thread op waarop je met join kunt wachten.

Wacht met join voordat je resultaten leest

Nadat beide werkthreads met succes zijn aangemaakt, roept de hoofdthread pthread_join(t[0], NULL) en pthread_join(t[1], NULL) aan. Een join blokkeert totdat de opgegeven thread eindigt. Een geslaagde join maakt de schrijfbewerkingen van die werkthread ook zichtbaar voor de wachtende thread. De hoofdthread kan dan 1.275 en 3.775 lezen en optellen tot 5.050 zonder mutex: de werkthreads schreven naar afzonderlijke objecten en de hoofdthread wachtte voordat die ze las.

Als je beide threads start voordat je met join wacht, kan hun werk overlappen. Als je één werkthread aanmaakt en direct met join wacht voordat je de volgende aanmaakt, draaien de werkthreads na elkaar.

Hier is het hoofdprogramma. Het stopt bij een pthread-fout en gebruikt daardoor nooit een ID van een mislukte aanmaak:

static void require_success(int rc) {
    if (rc != 0) {
        fprintf(stderr, "pthread error: %d\n", rc);
        exit(EXIT_FAILURE);
    }
}

int main(void) {
    struct range jobs[2] = {{1, 50, 0}, {51, 100, 0}};
    pthread_t threads[2];
    for (int i = 0; i < 2; i++)
        require_success(pthread_create(&threads[i], NULL,
                                      sum_range, &jobs[i]));
    for (int i = 0; i < 2; i++)
        require_success(pthread_join(threads[i], NULL));
    printf("%ld\n", jobs[0].result + jobs[1].result);
    return 0;
}

Om een teruggegeven pointer te verzamelen, geef je het adres door waarop die moet worden opgeslagen: void *answer; pthread_join(t, &answer);. De pointer moet verwijzen naar opslag die langer bestaat dan de werkthread, zoals toegewezen heapgeheugen of een struct van de aanmakende thread. &local_result teruggeven vanuit de werkthread is ongeldig, omdat de lokale variabele ophoudt te bestaan wanneer de functie terugkeert.

Voorkom dat je een veranderende lusvariabele deelt

Als je in een aanmaaklus &i aan elke werkthread doorgeeft, geef je steeds hetzelfde adres door. Werkthreads kunnen dat lezen nadat de lus de waarde heeft gewijzigd, zodat meerdere threads ogenschijnlijk dezelfde ID krijgen. Gebruik in plaats daarvan een array: wijs ids[i] = i toe, geef &ids[i] door en houd de array in leven en ongewijzigd totdat alle join-aanroepen zijn voltooid.

Beëindig threads en processen bewust

Terugkeren uit main of exit() aanroepen in een willekeurige thread beëindigt het hele proces en kan werkthreads afbreken. De hoofdthread wacht normaal met join op werkthreads voordat die terugkeert. pthread_exit() aanroepen beëindigt alleen de aanroepende thread; andere threads kunnen doorgaan.

Een losgekoppelde thread geeft zijn threadbronnen automatisch vrij wanneer die eindigt en kan niet met join worden afgewacht. Zorg dat op elke thread waarop met join kan worden gewacht één keer met join wordt gewacht of dat die wordt losgekoppeld. Join wacht op voltooiing; het beschermt geen twee werkthreads die hetzelfde object wijzigen terwijl ze allebei draaien.

Belangrijkste punten

  • Controleer pthread-retourcodes rechtstreeks; wacht met join alleen op threads die met succes zijn aangemaakt.
  • Geef afzonderlijke, stabiele argumentobjecten door en houd ze in leven totdat de werkthreads klaar zijn.
  • Wacht met join voordat je een resultaat leest waarnaar een werkthread mogelijk nog schrijft.
  • Geef alleen pointers terug naar objecten die langer bestaan dan de werkthread.
  • Koppel een thread los als de bronnen ervan zonder join moeten worden vrijgemaakt.
  • Terugkeren uit main beëindigt het proces, inclusief onafgemaakte werkthreads.
2

Zoek de gedeelde stappen die elkaar kunnen verstoren

Een broncode-instructie kan meerdere machinebewerkingen verbergen. Door die bewerkingen in volgorde te volgen, wordt een fout door gelijktijdige uitvoering zichtbaar: één thread leest, een andere wijzigt het object en de eerste slaat een antwoord op dat op de verouderde waarde is gebaseerd. Gebruik uitgewerkte uitvoeringsvolgordes om de gedeelde reeks te vinden die bij elkaar moet blijven. Maak daarna onderscheid tussen een datarace en een grotere timingfout, en beoordeel een oplossing op uitsluiting, voortgang en wachten.

2.1 Volg een verloren wijziging instructie voor instructie

De instructie counter++ lijkt klein, maar een gewone verhoging van een gedeeld geheel getal is geen ondeelbare bewerking. Meestal wordt een waarde geladen, in een register verhoogd en wordt het resultaat opgeslagen. Tussen die stappen kan een andere thread draaien.

Een uitvoeringsverloop waarbij een verhoging verloren gaat

Begin met counter gelijk aan 50. Beschouw laden, optellen en opslaan als afzonderlijke machinestappen:

StapDraaiende threadHandelingGedeelde teller
1T1Laadt 50 en telt er 1 bij op in zijn register50
2T2Laadt 50 en telt er 1 bij op in zijn register50
3T2Slaat zijn registerwaarde 51 op51
4T1Slaat zijn registerwaarde 51 op51

Twee verhogingen hadden 52 moeten opleveren. T1 overschrijft in plaats daarvan het resultaat van T2 met een waarde die is berekend uit de eerdere waarde 50. Dit is een verloren wijziging.

Een contextwisseling bewaart de registers van T1 in zijn TCB. Wanneer T1 verdergaat, bevat zijn register nog steeds 51; de thread laadt de gedeelde teller niet opnieuw alleen omdat T2 heeft gedraaid. De fout zit in de onbeschermde reeks, niet in het herstellen van een verkeerd register door de scheduler.

Vergelijk met een sequentiële uitvoering

Als T1 alle drie de stappen voltooit voordat T2 laadt, slaat T1 51 op en slaat T2 daarna 52 op. Als de volledige threads in omgekeerde volgorde draaien, geeft dat ook 52. Het resultaat 51 van de vervlochten uitvoering kan bij geen van beide sequentiële volgordes ontstaan. Om dit soort fouten te vinden, markeer je een leesbewerking, plaats je daarna een schrijfbewerking van een andere thread en hervat je de berekening van de eerste thread met de verouderde waarde.

Dit verloop is een model van de fout op machineniveau. In daadwerkelijke C-code vormen niet-gesynchroniseerde conflicterende toegangen tot een gewoon gedeeld geheel getal een datarace, die tot ongedefinieerd gedrag leidt. C beperkt dat programma niet tot de resultaten van een eenvoudig model van vervlochten instructies.

Waarom geslaagde tests weinig bewijzen

Twee threads die elk 10.000.000 verhogingen uitvoeren, moeten 20.000.000 opleveren. Experimentele uitvoeringen zonder bescherming kunnen verschillende kleinere totalen printen, zoals 19.345.221 en 19.221.041 in gepubliceerde voorbeelden. Een goed ogende uitvoering betekent alleen dat het waargenomen resultaat toevallig aanvaardbaar was.

De kwetsbare periode kan maar enkele instructies breed zijn. Een ander aantal kernen, andere compilerkeuzes of een andere systeembelasting kunnen een uitvoeringsvolgorde blootleggen die duizenden eerdere uitvoeringen misten. Als twee threads in het model elk één verhoging vanaf nul uitvoeren, staat het model 1 of 2 toe. Bij minstens twee verhogingen per thread kunnen zeldzame uitvoeringsvolgordes een totaal van slechts 2 opleveren, ook als het aantal bedoelde verhogingen groeit.

Herstel de bewerking, niet het uiterlijk ervan

Het gehele getal als volatile declareren maakt het laden, optellen en opslaan niet ondeelbaar en synchroniseert threads niet. Al het werk op één regel schrijven verandert niets. Zelfs een x86-instructie die rechtstreeks bij geheugen optelt, heeft de juiste atomaire vorm nodig om tussen kernen te coördineren.

Bescherm de volledige reeks lezen-wijzigen-schrijven met een gedeelde mutex of gebruik een atomaire C11-bewerking voor lezen-wijzigen-schrijven, zoals atomic_fetch_add. Op één kern is dat ook nodig: een timer kan een wisseling veroorzaken tussen het laden en opslaan, ook zonder parallelle uitvoering.

Belangrijkste punten

  • Een gewone verhoging kan bestaan uit laden, optellen en opslaan.
  • Een opslag op basis van een verouderde waarde kan een voltooide wijziging van een andere thread overschrijven.
  • Contextwisselingen bewaren de eigen registers van elke thread.
  • Volatile biedt noch atomiciteit, noch synchronisatie tussen threads.
  • Een datarace in C leidt tot ongedefinieerd gedrag; een uitgewerkt verloop laat de fout zien zonder de mogelijke gevolgen te begrenzen.

2.2 Houd een beslissing en de bijbehorende wijziging bij elkaar

Een fout door gelijktijdige uitvoering kan één geheugentoegang of een hele bedrijfsregel betreffen. Bepaal zowel welke objecten worden gedeeld als welke bewerking voor andere threads ondeelbaar moet lijken.

Raceconditie of datarace?

Een raceconditie ontstaat wanneer de juistheid afhangt van de relatieve timing. Een datarace ontstaat wanneer verschillende threads zonder synchronisatie dezelfde geheugenlocatie benaderen, minstens één thread erin schrijft en minstens één toegang niet-atomair is. C11 en C++11 kennen aan een datarace ongedefinieerd gedrag toe.

Atomaire toegang kan een datarace voorkomen terwijl een grotere bewerking nog steeds een raceconditie heeft. Afzonderlijk beschermde lees- en schrijfbewerkingen houden bijvoorbeeld een beslissing tussen die bewerkingen niet geldig. Twee threads kunnen elk een toereikend saldo lezen, de vergrendeling vrijgeven en later op basis van die verouderde beslissing geld opnemen.

Werk een opname uit

Een saldo begint op 100. Twee threads vragen elk om 80. De regel is dat een opname alleen wordt toegestaan als het saldo toereikend is.

  1. T1 controleert 100 en besluit dat de opname kan slagen.
  2. T2 controleert ook 100 en neemt dezelfde beslissing.
  3. T1 trekt 80 af, zodat 20 overblijft.
  4. T2 handelt op basis van de eerdere beslissing en trekt 80 van 20 af, zodat −60 overblijft.

Alleen de aftrekking beschermen herstelt die reeks niet. Bescherm de controle en de aftrekking met dezelfde vergrendeling, die je onafgebroken vasthoudt. Dan laat één thread 20 over en controleert de volgende thread 20 en weigert de opname. Bij een andere onbeschermde uitvoeringsvolgorde kan een aftrekking verloren gaan en 20 overblijven nadat beide opnames zijn uitbetaald, wat ook onjuist is.

Zoek de kritieke sectie

Een kritieke sectie is het gebied met gedeelde gegevens dat concurrerende toegang moet uitsluiten. Een reeks lezen-wijzigen-schrijven leest eerst gegevens, berekent een nieuwe waarde en schrijft die terug. Een reeks controleren en vervolgens handelen beslist of een handeling is toegestaan en voert die daarna uit. Lijstbewerkingen, uitgestelde initialisatie en het aanmaken van bestanden kunnen deze patronen ook bevatten.

Een vergrendeling biedt wederzijdse uitsluiting: hoogstens één deelnemende thread bevindt zich tegelijk in het beschermde gebied. Een atomaire bewerking lijkt voor andere deelnemende threads één volledige bewerking; ze kunnen geen half voltooide toestand waarnemen. Een mutex biedt dat beeld alleen voor code die dezelfde vergrendelingsafspraak volgt.

Niet elk object heeft een vergrendeling nodig. Gegevens die door één thread worden benaderd, blijven privé tenzij hun adres wordt gedeeld. Gegevens die volledig zijn geïnitialiseerd voordat werkthreads starten en daarna alleen worden gelezen, kunnen zonder vergrendeling worden gedeeld. Beoordeel wie een object kan bereiken en of schrijfbewerkingen met het gebruik ervan overlappen, niet alleen waar het is gedeclareerd.

Wat een correcte oplossing belooft
  • Wederzijdse uitsluiting: concurrerende threads kunnen niet samen naar binnen.
  • Voortgang: wanneer de sectie vrij is en threads naar binnen willen, kunnen threads die niet naar binnen proberen te gaan de keuze van een thread die naar binnen mag niet voor onbepaalde tijd uitstellen.
  • Begrensd wachten: na een verzoek bestaat er een grens aan het aantal keren dat concurrerende threads eerder naar binnen kunnen.

De C11-bewerking atomic_fetch_add(&count, 1) voert één atomaire wijziging uit op een atomic_int. Atomiciteit is gegarandeerd, ook op een platform dat de bewerking met een interne bibliotheekvergrendeling implementeert. Twee rekeningvelden afzonderlijk atomair maken verandert een overboeking ertussen nog steeds niet in één atomaire transactie; ook de samenhang tussen de velden heeft bescherming nodig.

Belangrijkste punten

  • Dataraces gaan over conflicterende, niet-gesynchroniseerde geheugentoegang; racecondities gaan over juistheid die van timing afhangt.
  • Een controle en de handeling die ervan afhangt horen in één kritieke sectie.
  • Wederzijdse uitsluiting, voortgang en begrensd wachten zijn afzonderlijke eisen.
  • Afzonderlijke atomaire toegangen maken een bewerking met meerdere stappen niet atomair.
3

Bescherm gedeelde bewerkingen met mutexen

Een vergrendeling werkt door een gezamenlijke afspraak: elke bewerking op beschermde gegevens volgt dezelfde regel. Leer de levenscyclus van een POSIX-mutex en bepaal daarna waar een kritieke sectie begint en eindigt. Korte secties verminderen de wachttijd, maar een controle moet bij de handeling blijven die ermee wordt toegestaan. Door code te controleren op ontbrekende ontgrendelingen, verkeerde vergrendelingen en onveilige levensduren maak je die afspraak controleerbaar en onderhoudbaar.

3.1 Gebruik een mutex van initialisatie tot vrijgave

Een mutex is een vergrendeling waarmee één thread tegelijk eigenaar van een beschermd gebied kan zijn. Een thread verkrijgt de mutex voordat die beschermde gegevens benadert en geeft die daarna vrij. Als een andere thread de mutex houdt, wacht een blokkerende vergrendelingsaanroep.

Initialiseer de gedeelde mutex

Gebruik pthread_mutex_t uit <pthread.h>. Een statische mutex kan beginnen met pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;. Een dynamisch aangemaakt object kan zijn veld initialiseren met pthread_mutex_init(&obj->lock, NULL). De initialisatie moet voltooid zijn voordat een thread de mutex gebruikt.

Hier is een tellerbewerking met een statisch geïnitialiseerde gedeelde mutex. De hulpfunctie controleert de pthread-retourwaarden, die rechtstreeks foutnummers zijn:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
static long counter = 0;  // guarded by m

static void check(int rc) {
    if (rc != 0) {
        fprintf(stderr, "pthread error: %d\n", rc);
        exit(EXIT_FAILURE);
    }
}

void increment(void) {
    check(pthread_mutex_lock(&m));
    counter = counter + 1;
    check(pthread_mutex_unlock(&m));
}

Elke gelijktijdige leesbewerking of wijziging van counter moet dezelfde regel volgen. Een afzonderlijke functie die een andere mutex gebruikt, zou niet met increment coördineren.

Wat gebeurt er terwijl de mutex wordt vastgehouden?

T1 vergrendelt m, laadt de teller, rekent en slaat op. Als T2 in die periode pthread_mutex_lock(&m) aanroept, kan die pas voorbij de aanroep zodra T2 het eigenaarschap verkrijgt nadat T1 de mutex heeft vrijgegeven. T1 kan nog steeds worden onderbroken: een mutex voorkomt dat concurrerende threads naar binnen gaan, geen contextwisselingen.

Houd voor de saldoregel de mutex vast voordat je balance >= amount beoordeelt. Trek alleen af als de controle slaagt, leg succes of mislukking vast en ontgrendel dan. Bij een saldo van 100 en twee verzoeken om 80 slaagt er precies één en blijft 20 over.

Kies hoe je wacht

pthread_mutex_trylock(&m) keert direct terug: 0 betekent dat de aanroepende thread de mutex heeft verkregen; EBUSY betekent dat dit niet is gelukt omdat de mutex bezet was. Bij een bezette mutex kan de thread ander werk doen en mag die de beschermde gegevens niet benaderen. pthread_mutex_timedlock wacht tot een absolute deadline. Ook bij deze interfaces moet je retourcodes controleren en een vergrendeling alleen vrijgeven nadat die met succes is verkregen.

Respecteer eigenaarschap en levensduur

De thread die eigenaar is, geeft een mutex vrij. Een normale niet-recursieve mutex blokkeert als de eigenaar die opnieuw probeert te vergrendelen. Een mutex met foutcontrole meldt fouten zoals opnieuw vergrendelen met EDEADLK of ontgrendelen door een andere thread dan de eigenaar met EPERM. Een recursieve mutex telt herhaalde verkrijgingen door de eigenaar en vereist evenveel vrijgaven. Vertrouw er niet op dat een standaardmutex verkeerd gebruik meldt: het gedrag hangt af van het type waaraan de implementatie die koppelt en van de bewerking.

Wanneer alle threads de mutex niet meer gebruiken en die ontgrendeld is, roep je pthread_mutex_destroy aan. Een object vernietigen of vrijgeven terwijl een andere thread het nog kan vergrendelen, schendt de levensduur van de mutex. Door de mutex via een pointer door te geven blijft de identiteit behouden; kopiëren maakt geen andere geldige verwijzing naar dezelfde vergrendeling.

Belangrijkste punten

  • Initialiseer een mutex voordat je die beschikbaar maakt voor werkthreads.
  • Verkrijg dezelfde gedeelde mutex vóór elke concurrerende toegang tot de gegevens die de mutex beschermt.
  • Een vastgehouden mutex voorkomt concurrerende toegang terwijl de eigenaar nog steeds kan worden onderbroken.
  • Als trylock EBUSY teruggeeft, is geen eigenaarschap verkregen.
  • Houd de mutex in leven totdat geen thread die nog kan gebruiken.
  • Mutexen met foutcontrole melden fouten in eigenaarschap; recursieve mutexen tellen geneste verkrijgingen.

3.2 Kies wat één vergrendeling beschermt

Vergrendelingsgranulariteit is de hoeveelheid gedeelde toestand die door één vergrendeling wordt beschermd. Die bepaalt welke bewerkingen op elkaar moeten wachten, ook wanneer ze op verschillende kernen draaien.

Eén grote vergrendeling of meerdere kleinere?

Een grofmazig ontwerp gebruikt één mutex voor een hele structuur of een heel subsysteem. Het heeft een eenvoudige beschermingsregel, maar niet-gerelateerde bewerkingen worden na elkaar uitgevoerd. Een fijnmazig ontwerp wijst afzonderlijke mutexen toe aan afzonderlijke rekeningen, buckets of gebieden. Threads kunnen dan tegelijk in verschillende gebieden werken, ten koste van meer vergrendelingsbewerkingen en ingewikkeldere regels.

Plaats de vergrendeling naast de toestand die die beschermt:

struct account {
    pthread_mutex_t lock;
    long balance;
    unsigned id;
};

Een bewerking op één rekening verkrijgt de bijbehorende mutex. Een overboeking tussen twee verschillende rekeningen heeft beide mutexen nodig, omdat die de relatie tussen de rekeningen verandert. Kies een vaste volgorde, bijvoorbeeld op oplopende rekening-ID, en gebruik die op elk pad. Anders kan één thread A vasthouden en op B wachten terwijl een andere B vasthoudt en op A wacht.

Een vergrendeling stelt een bovengrens aan de doorvoer

Stel dat elke bewerking één globale vergrendeling 2 µs vasthoudt. Het beschermde werk kan hoogstens één bewerking per 2 µs voltooien:

maximale doorvoer=12×10−6=500.000 bewerkingen per seconde.

Door kernen toe te voegen kunnen geen twee bewerkingen dezelfde mutex tegelijk vasthouden. Overhead van de vergrendeling en ander werk kunnen de werkelijk behaalde snelheid verder verlagen. De duur van het beschermde werk is daarom een nuttige grens om te berekenen voordat je versnelling verwacht.

Verkort de sectie zonder de bewerking te verstoren

Bereid privéwerk voor voordat je de mutex verkrijgt. Wijs bij het invoegen in een lijst eerst de nieuwe knoop toe en vul de sleutel ervan in. Verkrijg de lijstmutex pas om de huidige kop te lezen en de knoop te koppelen. Als de toewijzing mislukt, gebeurt dat dan zonder dat een vergrendeling wordt vastgehouden.

Trage I/O, printen of een onbekende callback kan alle wachtende threads laten vastlopen. Verplaats zulk werk naar buiten als de gegevens daar geldig kunnen blijven. Houd een beslissing en de wijziging die ervan afhangt bij elkaar: als je een opname verkort door tussen de saldocontrole en de aftrekking te ontgrendelen, is de uitkomst niet meer correct.

Beoordeel drie verschillende eigenschappen

Bij wederzijdse uitsluiting vraag je of het ontwerp conflicterende toegang echt voorkomt. Bij eerlijkheid vraag je of een wachtende thread uiteindelijk aan de beurt komt of kan uithongeren. Bij prestaties vraag je wat de vergrendeling kost met één thread, meerdere threads op één CPU en threads op meerdere CPU's. Een vergrendeling kan correct zijn en toch oneerlijk of traag.

Concurrentie om een vergrendeling ontstaat wanneer threads wedijveren om een vergrendeling die al wordt vastgehouden. Bij zware concurrentie verplaatsen ze ook gedeelde cachelijnen met de vergrendeling en gegevens tussen kernen. In een gepubliceerd tellerexperiment duurde een miljoen wijzigingen door één thread ongeveer 0,03 s, maar een miljoen wijzigingen per thread door twee threads meer dan 5 s; dit zijn metingen van die opstelling, geen universele tijden.

Houd voor een schaalbaarheidsexperiment het werk per thread gelijk. Bij ideale schaalbaarheid doen N threads op N kernen N keer zoveel werk in de oorspronkelijke tijd. Begin met een eenvoudige correcte structuur, meet de wachttijd en totale tijd en splits een vergrendeling alleen op waar de extra gelijktijdigheid de extra overhead goedmaakt.

Belangrijkste punten

  • Grofmazige vergrendelingen vereenvoudigen de bescherming; fijnmazigere vergrendelingen laten onafhankelijke gebieden tegelijk doorgaan.
  • Eén vergrendeling laat het beschermde werk na elkaar uitvoeren en begrenst de doorvoer.
  • Verkrijg meerdere vergrendelingen in één vaste volgorde.
  • Beoordeel uitsluiting, eerlijkheid en prestaties afzonderlijk.
  • Meet voordat je een eenvoudige vergrendeling door een ingewikkelder ontwerp vervangt.

3.3 Controleer elk pad door de vergrendelde code

Een mutex kan perfect zijn geïmplementeerd terwijl een programma die toch verkeerd gebruikt. Controleer het hele pad door een functie, inclusief leesbewerkingen, fouten, callbacks en de vernietiging van objecten.

Geef eigenaarschap vrij op elk afsluitpad

Deze functie laat de vergrendeling op het foutpad vastgehouden:

pthread_mutex_lock(&m);
if (failed) return -1;
update_shared_state();
pthread_mutex_unlock(&m);

De terugkerende thread kan eindigen, maar de mutex blijft vergrendeld. Een latere aanroepende thread wacht dan op een vrijgave die nooit zal plaatsvinden. Gebruik één opruimpad of plaats vóór elke terugkeer een ontgrendeling. Met gecontroleerde pthread-aanroepen kan de structuur zo zijn:

int rc = -1;
check(pthread_mutex_lock(&m));
if (!failed) {
    update_shared_state();
    rc = 0;
}
check(pthread_mutex_unlock(&m));
return rc;

Eén vrijgavepunt controleren is eenvoudiger dan veel verspreide punten controleren. Het gaat erom dat elke met succes verkregen vergrendeling op elk normaal afsluitpad wordt vrijgegeven.

Volg de beschermingsregel ook bij lezen

Een statusfunctie die “alleen leest” kan een raceconditie met een wijzigende thread hebben. Die kan ook samenhangende velden in verschillende stadia van één wijziging waarnemen. Als een lijstbewerking een knoop publiceert voordat de velden ervan volledig zijn ingevuld, kan een onbeschermde lezer een pointer naar onvoltooide gegevens volgen. Verkrijg de vastgelegde vergrendeling voor elke toegang waarvan de juistheid daarvan afhangt.

Verschillende mutexen voor hetzelfde object gebruiken helpt niet. Als T1 lockA verkrijgt en T2 lockB, kunnen beide verkrijgingen slagen. Een mutex die in elke werkthread als nieuwe lokale variabele wordt gedeclareerd, maakt deze fout gemakkelijk te missen: elke thread vergrendelt zijn eigen object, dus er is geen gezamenlijke uitsluiting.

Behoud zowel de identiteit als de grenzen van de bewerking

Je kunt een pthread_mutex_t niet door toewijzing of door het kopiëren van de struct waarin die staat kopiëren en vervolgens als tweede handle voor de mutex gebruiken. Gebruik van de kopie leidt tot ongedefinieerd gedrag. Geef een pointer naar de oorspronkelijke gedeelde mutex door.

Controleer ook waar vrijgaven plaatsvinden. Een functie die vergrendelt om een saldo te controleren, ontgrendelt en later opnieuw vergrendelt om af te trekken, heeft afzonderlijke toegangen beschermd maar de beslissing laten verouderen. Houd de volledige reeks controleren en vervolgens handelen onder onafgebroken eigenaarschap.

Voorkom dat je op jezelf wacht

Onbekende code aanroepen terwijl je een mutex vasthoudt, kan onverwacht opnieuw toegang tot hetzelfde beschermde object proberen te krijgen. Bij een normale mutex blokkeert een tweede vergrendelingsaanroep door de eigenaar die eigenaar. Een mutex met foutcontrole meldt deze fout, terwijl een recursieve mutex genest eigenaarschap toestaat en bijpassende ontgrendelingen nodig heeft. Kies bewust een type; gebruik recursie niet om een onduidelijk beschermingsontwerp te verbergen.

Houd trage I/O en slapen waar mogelijk buiten een gedeelde kritieke sectie. Controleer voordat je de mutex vernietigt of het object met de mutex vrijgeeft dat geen thread eigenaar is of de mutex opnieuw kan benaderen. Initialisatie, gebruik en vernietiging moeten een duidelijke volgorde hebben.

Gebruik runtimehulpmiddelen om waargenomen fouten bloot te leggen

ThreadSanitizer, ingeschakeld met -fsanitize=thread, en Helgrind van Valgrind kunnen dataraces en verkeerd gebruik van vergrendelingen melden in uitvoeringen die ze waarnemen. Combineer hun meldingen met een controle van gedeelde toegangen en mogelijke vervlechtingen. Een uitvoering zonder meldingen stelt vast dat het hulpmiddel in die uitvoeringen geen probleem vond; over een ongetest pad moet je nog steeds redeneren.

Belangrijkste punten

  • Geef elke met succes verkregen mutex op elk afsluitpad vrij.
  • Ook lezers moeten de beschermingsregel volgen wanneer schrijvers gelijktijdig kunnen draaien.
  • Een mutex wordt via zijn adres gedeeld; afzonderlijke of gekopieerde mutexen coördineren de toegang niet.
  • Scheid een controle niet van de bijbehorende handeling door te ontgrendelen.
  • Onbekende callbacks kunnen code opnieuw binnengaan en proberen de vastgehouden mutex te verkrijgen.
  • Een uitvoering zonder meldingen van een raceconditiedetector betreft de waargenomen uitvoeringen, dus controleer ook ongeteste paden.
4

Hoe vergrendelingen worden verkregen, wachten en worden overgedragen

Een vergrendeling moet de beslissing over eigenaarschap ondeelbaar maken en de beschermde geheugentoegangen ordenen. Gewone vlaggen doen geen van beide. Ga van mislukte pogingen om een vergrendeling te bouwen naar atomaire uitwisseling en voeg daarna eerlijkheid toe met ticketnummers. Kijk ten slotte naar de wachtende thread: actief wachten, de CPU-beurt afstaan en slapen hebben verschillende kosten, en slapen vereist een protocol dat geen weksignaal kan verliezen. De planningsprioriteit kan bepalen welke wachtstrategie voortgang maakt.

4.1 Waarom een gewone vlag controleren niet genoeg is

Een vergrendeling heeft een betrouwbare manier nodig om één eigenaar te kiezen. Een gewone leesbewerking gevolgd door een gewone schrijfbewerking laat een tussenruimte in die beslissing, ook als het beschermde werk zelf heel kort is.

Interrupts regelen heeft een beperkte rol

Op één CPU kan een kernel interrupts kort uitschakelen rond een niet-blokkerende kritieke sectie om te voorkomen dat die daar door een interrupt wordt onderbroken. Dit berust erop dat de thread op de CPU blijft zonder vrijwillig te blokkeren of te wisselen. De kernel gebruikt deze techniek voor korte interne bewerkingen.

Het is geen algemene toepassingsmutex. Instructies om interrupts te regelen zijn geprivilegieerd; als een toepassing ze onbeperkt mocht gebruiken, zou die de CPU kunnen monopoliseren. Interrupts op één kern uitschakelen houdt andere kernen niet tegen om dezelfde gegevens te benaderen. Lang uitschakelen vertraagt ook de afhandeling van interrupts en kan op sommige apparaten tot verloren gebeurtenissen leiden.

Volg de ondeugdelijke vergrendeling met een vlag

Bekijk deze C-achtige pseudocode:

while (flag == 1) { }
flag = 1;
// critical section
flag = 0;

Begin met flag op 0. T1 test de vlag en verlaat de lus, maar pauzeert vóór de toewijzing. T2 test dezelfde 0, stelt de vlag in op 1 en gaat naar binnen. T1 gaat verder, stelt de vlag in op 1 en gaat ook naar binnen. Beide kwamen door de test voordat een van beide eigenaarschap opeiste.

Als je het lezen en schrijven afzonderlijk atomair maakt, verdwijnt een datarace op de vlag, maar blijven er in dit protocol nog steeds twee afzonderlijke bewerkingen over. Het opeisen van eigenaarschap vereist een atomaire bewerking voor lezen-wijzigen-schrijven of een vollediger synchronisatiealgoritme.

De lege lus is actief wachten: herhaaldelijk controleren terwijl CPU-tijd wordt verbruikt. Op één CPU kan een wachtende thread die actief wacht nadat die de eigenaar heeft onderbroken, de eigenaar niet sneller laten vrijgeven. De wachtende thread moet de eigenaar uiteindelijk laten draaien.

De oplossing van Peterson voor twee threads

Het algoritme van Peterson coördineert twee threads met een belangstellingsvlag per thread en een gedeelde beurt. Thread i voert uit:

flag[i] = 1;
turn = 1 - i;
while (flag[1 - i] == 1 && turn == 1 - i) { }
// critical section
flag[i] = 0;

De thread meldt belangstelling, geeft de andere thread bij gelijke aanspraak voorrang en wacht alleen zolang de andere belangstelling heeft en voorrang heeft. Onder sequentiële consistentie behouden de bewerkingen van elke thread hun programmavolgorde en zien alle threads één gemeenschappelijke volgorde. In dat model biedt het algoritme van Peterson wederzijdse uitsluiting, voortgang en begrensd wachten voor twee threads.

Ook de geheugenvolgorde is belangrijk

Moderne processors en compilers kunnen geheugenbewerkingen zichtbaar laten worden in een andere volgorde dan in de broncode. Een opslagbewerking kan in een buffer blijven staan terwijl een latere laadbewerking geheugen leest. Beide threads kunnen dan de belangstellingsvlag van de andere als 0 zien en allebei naar binnen gaan. Gewone gedeelde gehele getallen in C veroorzaken bovendien dataraces volgens C.

Een geldige implementatie heeft geschikte atomaire bewerkingen en ordening nodig, zoals sequentieel consistente atomaire C11-bewerkingen voor de vlaggen en de beurt. volatile biedt die synchronisatie niet. Praktische vergrendelingen combineren een ondeelbare eigendomsbewerking met garanties voor geheugenordening, zodat beschermde toegangen aan de juiste kant van verkrijgen en vrijgeven blijven.

Belangrijkste punten

  • Door testen en instellen te scheiden kunnen twee threads dezelfde vrije vergrendeling opeisen.
  • Interrupts op één CPU uitschakelen sluit een andere kern niet uit.
  • Het algoritme van Peterson voor twee threads vereist sequentieel consistent gedrag.
  • Zowel atomair eigenaarschap als correcte geheugenordening is nodig voor een praktische vergrendeling.

4.2 Kies één eigenaar met een atomaire instructie

Een atomaire bewerking voor lezen-wijzigen-schrijven combineert het lezen en bijwerken van een locatie tot één ondeelbare handeling. Daarmee wordt de tussenruimte gesloten waardoor twee threads samen door een gewone vlagtest kunnen komen.

Wissel de vlag uit en bekijk de oude waarde

Test-and-set, of atomaire uitwisseling, schrijft een nieuwe waarde en geeft de oude terug. Als een vlag op 0 begint, wisselt T1 die uit voor 1 en ontvangt 0, zodat T1 naar binnen gaat. T2 wisselt de vlag uit voor 1 en ontvangt 1, zodat T2 wacht. Totdat de vrijgave 0 schrijft, kan geen andere uitwisseling 0 teruggeven.

Hier is een C11-spinlock met de standaard sequentieel consistente atomaire bewerkingen:

#include <stdatomic.h>

static atomic_flag held = ATOMIC_FLAG_INIT;

void lock(void) {
    while (atomic_flag_test_and_set(&held)) { }
}

void unlock(void) {
    atomic_flag_clear(&held);
}

De ordening is net zo belangrijk als de vlag. Verkrijging moet ervoor zorgen dat latere toegangen binnen de kritieke sectie na de geslaagde verkrijging blijven; vrijgave moet ervoor zorgen dat eerdere toegangen binnen de kritieke sectie vóór de vrijgave blijven. Ook het wissen van de vlag moet atomair zijn; gewone opslagbewerkingen mengen met atomaire toegang implementeert deze overdracht niet.

Vergelijk vóór je vervangt

Compare-and-swap, of CAS, verandert een waarde alleen als die gelijk is aan een verwachte waarde. Stel dat twee threads een teller op 7 lezen en elk 1 willen optellen. T2 verandert 7 met succes in 8. De poging van T1 om 7 in 8 te veranderen mislukt nu, dus probeert T1 het opnieuw vanaf 8 en verandert die waarde in 9. Door opnieuw te proberen blijven beide verhogingen behouden.

atomic_compare_exchange_strong van C11 geeft succes of mislukking terug en werkt bij mislukking het argument met de verwachte waarde bij. Sommige hardwareachtige pseudocode geeft in plaats daarvan de oude opgeslagen waarde terug. Controleer welke afspraak een uitgewerkt verloop gebruikt. Op x86 implementeert xchg met een geheugenoperand uitwisseling, terwijl lock cmpxchg atomaire vergelijking en uitwisseling biedt.

Andere hardwarebouwstenen

Load-linked/store-conditional, of LL/SC, leest terwijl een reservering wordt vastgelegd en probeert daarna te schrijven. De voorwaardelijke schrijfbewerking mislukt als een andere schrijfbewerking de reservering ongeldig heeft gemaakt; die kan ook om andere redenen mislukken en hoort daarom in een lus die het opnieuw probeert. LDXR/STXR van ARM en lr/sc van RISC-V ondersteunen dit patroon.

Fetch-and-add telt atomair op en geeft de oude waarde terug. C11 biedt dit als atomic_fetch_add; x86 biedt lock xadd. Doordat de oude waarde wordt teruggegeven, is de bewerking bruikbaar om unieke ticketnummers uit te delen.

C garandeert het atomaire gedrag van zijn atomaire bibliotheekbewerkingen. De taal vereist niet dat elk atomair type vergrendelingsvrij is; sommige bewerkingen gebruiken een interne bibliotheekvergrendeling. Voor atomic_flag is vergrendelingsvrijheid gegarandeerd en atomic_is_lock_free controleert andere objecten.

Correct betekent niet eerlijk of snel

Een vergrendeling met test-and-set biedt wederzijdse uitsluiting, maar geen volgorde van binnenkomst: een thread kan na vrijgave herhaaldelijk de race verliezen en uithongeren. Op één CPU kunnen vier threads met round-robintijdschijven van 10 ms in drie wachtende threads 30 ms verspillen aan actief wachten voordat de onderbroken eigenaar weer draait. Op meerdere kernen, met een draaiende eigenaar en korte secties, kan kort actief wachten de overhead van slapen en wekken voorkomen.

Belangrijkste punten

  • Test-and-set geeft de oude vlagwaarde terug terwijl de nieuwe atomair wordt geschreven.
  • CAS probeert het na een verouderde verwachte waarde opnieuw, zodat concurrerende wijzigingen behouden blijven.
  • LL/SC-code moet een mislukte voorwaardelijke opslag afhandelen door het opnieuw te proberen.
  • Fetch-and-add geeft de oude waarde terug terwijl de verhoging wordt uitgevoerd.
  • Het verkrijgen en vrijgeven van een vergrendeling moet ook beschermde geheugentoegangen ordenen.
  • Een spinlock met test-and-set biedt uitsluiting maar geen garantie tegen uithongering.

4.3 Deel beurten uit met een ticketvergrendeling

Een vergrendeling met test-and-set onthoudt of die bezet is, maar niet wie heeft gewacht. Een ticketvergrendeling voegt een volgorde van binnenkomst toe met een atomaire ticketuitgever en een beurtteller.

Neem een nummer en wacht erop

Beide tellers beginnen op 0. Elke aanroepende thread verhoogt ticket atomair en bewaart de oude waarde als mine. De thread gaat pas naar binnen wanneer turn gelijk is aan mine. De eigenaar geeft de vergrendeling vrij door turn één keer te verhogen.

#include <stdatomic.h>

static atomic_uint ticket = 0;
static atomic_uint turn = 0;

void lock(void) {
    unsigned mine = atomic_fetch_add(&ticket, 1);
    while (atomic_load(&turn) != mine) { }
}

void unlock(void) {
    atomic_fetch_add(&turn, 1);
}

Deze standaard atomaire C11-bewerkingen bieden zowel atomiciteit als sequentieel consistente ordening. Elke aanroepende thread bewaart mine in zijn eigen lokale variabele. De gedeelde ticketteller geeft unieke nog geldige nummers uit.

Volg drie aankomsten

A neemt ticket 0 en gaat naar binnen omdat turn 0 is. B neemt ticket 1 en wacht. C neemt ticket 2 en wacht. Het volgende ongebruikte ticket is nu 3, terwijl de huidige beurt nog steeds 0 is.

Wanneer A vrijgeeft, wordt turn 1, dus gaat B naar binnen. C wacht nog steeds op 2. Door de vrijgave van B gaat turn naar 2 en gaat C naar binnen. Een nieuw aankomende thread krijgt ticket 3 en kan C niet voorbijgaan.

Dit onderscheid is belangrijk bij het volgen van het verloop: ticket houdt het volgende uit te delen nummer bij; turn houdt bij welk nummer aan de beurt is. Een ticket nemen verandert alleen het uit te delen nummer. Vrijgave laat de beurt opschuiven.

Wat eerlijkheid garandeert

Een aanroepende thread wacht achter de threads met eerdere tickets. Als elke houder uiteindelijk vrijgeeft en de threads ervoor kunnen draaien, kunnen latere aankomsten de thread niet inhalen. Het aantal concurrerende kritieke secties ervoor is begrensd door de eerdere nog geldige tickets. Dit biedt begrensd wachten en voorkomt uithongering onder die voorwaarden voor voortgang.

De garantie betreft de volgorde van binnenkomst, niet de verstreken tijd. Een houder die lang vastloopt, kan ook alle latere aanroepende threads lang laten wachten. Een ticketnummer kan de scheduler niet dwingen de eigenaar ervan te laten draaien.

Kosten die door de ticketvolgorde blijven bestaan

Elke wachtende thread wacht nog steeds actief. Als de thread die als volgende aan de beurt is tijdelijk niet draait, wachten latere uitvoerbare threads ook al bevindt zich momenteel geen thread in de kritieke sectie. Dit zijn planningskosten van de strikte volgorde.

Wachtende threads lezen ook één gedeelde locatie turn. Elke vrijgave verandert de cachelijn die ze allemaal volgen en veroorzaakt verkeer tussen kernen. Spinlocks met een wachtrij verminderen dat verkeer door elke wachtende thread een afzonderlijke lokale locatie te laten volgen; Linux stapte over van ticketspinlocks op ontwerpen met een wachtrij.

Werkelijke tickettellers gebruiken rekenkundige bewerkingen zonder teken. Gelijkheidscontroles kunnen bij telleromslag werken wanneer het aantal nog geldige tickets kleiner blijft dan het bereik van de teller. Een nummer opnieuw gebruiken terwijl een eerder gelijk ticket nog geldig is, zou uniciteit en uitsluiting tenietdoen.

Belangrijkste punten

  • Tickettoewijzing is atomair; een aanroepende thread gaat pas naar binnen als het nummer gelijk is aan de huidige beurt.
  • Vrijgave verhoogt turn, terwijl verkrijging een nummer uit ticket toewijst.
  • Houders van eerdere tickets kunnen niet worden ingehaald door latere aankomsten.
  • Een eerlijke volgorde laat actief wachten, vertraging door planning en gedeeld cacheverkeer bestaan.

4.4 Laat wachtende threads slapen zonder een vrijgave te missen

Een correcte vergrendeling kan het grootste deel van een CPU aan wachten verspillen. De juiste wachtstrategie hangt ervan af of de eigenaar kan draaien en hoe lang die de vergrendeling vasthoudt.

De CPU-beurt afstaan

Een wachtende thread kan yield() aanroepen na een mislukte poging om eigenaarschap te verkrijgen. Door de CPU-beurt af te staan gaat de thread van de toestand in uitvoering naar de gereedtoestand, zodat de scheduler een andere thread kan kiezen. De thread blijft uitvoerbaar en kan dus snel opnieuw worden gekozen en weer falen. Met 100 threads op één CPU kunnen maximaal 99 wachtende threads elk draaien en de CPU-beurt afstaan voordat een onderbroken eigenaar weer aan de beurt komt. Dit vermindert actief wachten, maar biedt geen eerlijke volgorde van binnenkomst.

Blokkeren haalt de wachtende thread uit het uitvoerbare werk

Een vergrendeling met slapen plaatst een aanroepende thread die de vergrendeling niet krijgt in een wachtrij. De thread gebruikt geen CPU terwijl die geblokkeerd is. Ontgrendelen wekt een wachtende thread zodat die weer kan draaien. Een kleine interne spinlock kan de vlag en wachtrij beschermen; die wordt alleen kort vastgehouden voor de administratie.

Het scheiden van “besluiten te gaan slapen” en “daadwerkelijk gaan slapen” brengt het risico van een verloren weksignaal mee:

  1. Een wachtende thread plaatst zichzelf in de wachtrij en geeft de interne vergrendeling vrij.
  2. De thread wordt gepauzeerd voordat die park() aanroept.
  3. De eigenaar geeft vrij en roept voor die thread unpark() aan.
  4. Als dat weksignaal wordt weggegooid, gaat de wachtende thread later slapen terwijl er niemand meer is om die te wekken.

setpark() van Solaris kondigt het voornemen aan om te gaan slapen. Een tussentijdse aanroep van unpark laat de latere aanroep van park dan direct terugkeren. Het protocol verbindt de beslissing met de daadwerkelijke blokkering.

Linux gebruikt atomair vergelijken en blokkeren

Een futex combineert een woord in gebruikersgeheugen met kernelondersteuning voor wachten. futex_wait(addr, expected) blokkeert alleen als het woord nog gelijk is aan de verwachte waarde; de controle en de beslissing om te blokkeren zijn atomair ten opzichte van bewerkingen op dat futexwoord. Als een ontgrendeling het al heeft gewijzigd, gaat de aanroepende thread niet slapen op basis van de verouderde verwachting.

futex_wake(addr, n) wekt maximaal n wachtende threads. Een vergrendeling zonder concurrentie kan worden verkregen en vrijgegeven met atomaire instructies in de gebruikersruimte, zonder systeemaanroep. De pthread-mutexen van glibc in Linux gebruiken futexen om bij concurrentie te wachten. Een weksignaal maakt een thread uitvoerbaar; het vergrendelingsprotocol moet nog steeds eigenaarschap vaststellen voordat de thread de beschermde gegevens benadert.

Wacht kort actief en ga dan slapen

Een tweefasenvergrendeling wacht eerst kort actief en blokkeert daarna als het wachten voortduurt. Actief wachten kan de moeite waard zijn als de eigenaar op een andere kern draait en de verwachte wachttijd korter is dan slapen en wekken. Blokkeren heeft de voorkeur bij lange wachttijden of wanneer er meer uitvoerbare threads dan kernen zijn. Op één CPU kan een actief wachtende thread een tijdelijk niet draaiende eigenaar niet helpen om klaar te komen.

Planningsprioriteit kan voortgang verhinderen

Bij prioriteitsinversie wacht een thread met hoge prioriteit op een mutex die wordt vastgehouden door een thread met lage prioriteit, terwijl werk met middelhoge prioriteit de eigenaar van de CPU houdt. De wachtende thread met hoge prioriteit laten slapen voorkomt actief wachten, maar neemt die planningsvertraging niet weg.

Prioriteitsovererving verhoogt de prioriteit van de eigenaar tijdelijk tot die van de wachtende thread met de hoogste prioriteit, zodat de eigenaar kan afronden en vrijgeven. NASA's Mars Pathfinder kreeg in 1997 te maken met herstarts door prioriteitsinversie; het inschakelen van prioriteitsovererving voor de mutex verhielp het probleem. Een actief wachtende thread met hoge prioriteit op één CPU met strikte prioriteitsplanning kan nog erger zijn: die kan volledig verhinderen dat de eigenaar met lage prioriteit draait.

Belangrijkste punten

  • Bij het afstaan van de CPU-beurt blijft een wachtende thread uitvoerbaar; slapen blokkeert de thread en gebruikt geen CPU.
  • Een correct slaapprotocol sluit de tussenruimte tussen besluiten te slapen en daadwerkelijk blokkeren.
  • Futex-wait controleert de verwachte waarde atomair samen met de beslissing om te blokkeren.
  • Een tweefasenvergrendeling wacht kort actief voordat de thread gaat slapen.
  • Prioriteitsovererving helpt een eigenaar met lage prioriteit een vergrendeling vrij te geven die nodig is voor werk met hogere prioriteit.
5

Bouw gedeelde structuren die correct blijven

Een structuur is threadveilig wanneer gelijktijdige aanroepen bij hun mogelijke vervlechtingen aan de specificatie voldoen. Begin met één mutex en een expliciete regel voor elk gedeeld veld. Onderzoek daarna waar bewerkingen elkaar echt moeten uitsluiten: tellerwijzigingen, het doorlopen van lijsten, tegenoverliggende uiteinden van wachtrijen en afzonderlijke hashbuckets hebben verschillende behoeften. Elke verfijning moet veilige publicatie en geheugenlevensduur behouden en de extra complexiteit moet zich in metingen terugverdienen.

5.1 Ruil tellernauwkeurigheid in voor minder gedeelde wijzigingen

Een gedeelde teller is eenvoudig met één mutex te beschermen. De bewerkingen increment, decrement en get verkrijgen elk dezelfde vergrendeling, voeren hun taak uit en geven die vrij. De teller is exact, maar alle wijzigingen gaan via één gedeelde plek.

Waarom de exacte teller een knelpunt wordt

Op meerdere kernen laat een mutex de tellerbewerkingen na elkaar uitvoeren. De kernen dragen ook het eigenaarschap van de cachelijnen met de vergrendeling en teller over. Werkthreads toevoegen kan de wachttijd sneller laten toenemen dan het nuttige werk. De mutex vervangen door atomic_fetch_add neemt op gangbare processors de expliciete vergrendelingsoverhead weg, maar elke wijziging schrijft nog steeds naar dezelfde gedeelde locatie.

Een benaderende teller, ook wel slordige teller genoemd, verdeelt wijzigingen over lokale tellers en brengt ze in groepen over naar een globale teller. De gerapporteerde globale waarde laat recente lokale verhogingen bewust weg.

Bouw de wijziging van lokaal naar globaal op

Houd per kern één teller en mutex bij, daarnaast een globale teller met een eigen mutex en een drempel S. Voor elke verhoging:

  1. Kies de lokale plek voor deze wijziging en verkrijg de bijbehorende mutex.
  2. Tel 1 bij de lokale telling op.
  3. Als die S bereikt, verkrijg dan de globale mutex terwijl je de lokale nog vasthoudt.
  4. Tel die lokale telling bij de globale telling op, zet de lokale telling terug op 0 en geef de globale mutex vrij.
  5. Geef de lokale mutex vrij.

Houd de gekozen plek tijdens de hele wijziging gelijk. Threads die verschillende plekken gebruiken, raken meestal verschillende vergrendelingen aan en verkrijgen de globale mutex alleen bij een overdracht. Ook het lezen van de globale telling volgt de bijbehorende vergrendelingsregel.

Volg een groepsgewijze overdracht

Stel dat bij vier kernen en S gelijk aan 5 hun aantallen wijzigingen 7, 3, 4 en 0 zijn. Kern 1 draagt bij de vijfde verhoging 5 over en houdt daarna 2 lokale verhogingen over. De uiteindelijke lokale tellingen zijn 2, 3, 4 en 0. De globale telling is 5 en het werkelijke totaal is 14: negen verhogingen blijven lokaal.

Na voltooide verhogingen met één eenheid ligt elke lokale telling tussen 0 en S − 1, inclusief de grenzen. Bij P kernen kan het globale totaal hoogstens achterlopen met:

P(S−1).

Voor vier kernen en S gelijk aan 5 is de bovengrens 16. Deze grens betreft voltooide wijzigingen en hun resterende lokale tellingen; pas die niet halverwege een overdracht toe.

Kies de drempel voor de taak

Een kleinere drempel maakt de globale waarde actueler, maar vereist meer globale verkrijgingen. Bij S gelijk aan 1 wordt elke verhoging globaal overgedragen. Dat voegt overhead van de lokale vergrendeling toe zonder het globale knelpunt te vermijden. Een grotere drempel verbetert de schaalbaarheid van wijzigingen, maar vergroot de mogelijke achterstand. In een gepubliceerd experiment konden vier threads met S gelijk aan 1.024 vier miljoen wijzigingen uitvoeren in bijna de tijd die één thread voor één miljoen nodig had.

Voor een exacte momentopname moeten alle lokale mutexen in een vaste volgorde en daarna de globale mutex worden vastgehouden voordat de tellingen worden opgeteld. Die volgorde komt overeen met wijzigingen die eerst een lokale en daarna de globale vergrendeling verkrijgen. De momentopname houdt gelijktijdige overdrachten tegen tijdens het lezen; elk veld afzonderlijk lezen zou een overdracht dubbel kunnen tellen of missen.

Benaderende tellers zijn geschikt voor statistieken waarbij vertraagde zichtbaarheid aanvaardbaar is. Een referentieteller die bepaalt wanneer geheugen kan worden vrijgegeven, heeft een exacte methode nodig: een te lage telling zou een object kunnen vrijgeven dat nog in gebruik is.

Belangrijkste punten

  • Eén atomaire teller kan nog steeds een knelpunt in de gedeelde cache zijn.
  • Benaderende tellers brengen lokale verhogingen in groepen over naar een beschermde globale teller.
  • Na voltooide verhogingen met één eenheid is de maximale achterstand P(S − 1).
  • Grotere overdrachtsdrempels verminderen de globale concurrentie en vergroten tegelijk de mogelijke achterstand.
  • Een exacte momentopname houdt alle relevante vergrendelingen vast in een volgorde die overeenkomt met die van wijzigingen.

5.2 Publiceer en doorloop knopen van een gekoppelde lijst veilig

Een gedeelde gekoppelde lijst moet zowel de koppelingen als de levensduur van de knopen behouden. Eén mutex rond elke bewerking is een nuttig eerste ontwerp, omdat elke gedeelde doorloop en wijziging één regel volgt.

Hoe twee invoegingen een knoop verliezen

Stel dat de lijst bij A begint. T1 bereidt knoop X voor en T2 bereidt knoop Y voor. Zonder gedeelde vergrendeling kunnen beide head == A lezen, next van hun nieuwe knoop op A instellen en vervolgens hun eigen knoop in head opslaan. De latere toewijzing aan de kop verbergt de eerst ingevoegde knoop. Die knoop wordt onbereikbaar, waardoor een invoeging verloren gaat en het geheugen ervan lekt.

De beschermde reeks moet het lezen van de huidige kop, het instellen van de nieuwe koppeling en het publiceren van de nieuwe kop omvatten. Een vergrendeling alleen rond de laatste toewijzing zou beide threads toestaan koppelingen te maken vanuit dezelfde verouderde kop.

Bereid privégegevens voor voordat je de vergrendeling verkrijgt

Een nieuwe knoop blijft privé totdat die in de gedeelde lijst wordt gekoppeld. Toewijzing en initialisatie van de sleutel kunnen vóór het verkrijgen van de vergrendeling plaatsvinden. Met de eerdere pthread-hulpfunctie die fouten controleert, kan invoegen zo worden geschreven:

int insert(list_t *list, int key) {
    node_t *node = malloc(sizeof(*node));
    if (node == NULL) return -1;
    node->key = key;

    check(pthread_mutex_lock(&list->lock));
    node->next = list->head;
    list->head = node;
    check(pthread_mutex_unlock(&list->lock));
    return 0;
}

Als de toewijzing mislukt, keert de functie terug voordat een mutex wordt vastgehouden. De gedeelde sectie blijft klein terwijl de controle van head en het bijwerken van de koppeling bij elkaar blijven. Threadveilige toewijzing maakt de lijst zelf niet threadveilig; de koppelingen hebben nog steeds hun eigen bescherming nodig.

Lezers volgen dezelfde regel

Een zoekopdracht verkrijgt de lijstmutex voordat die head leest en houdt de mutex vast tijdens het doorlopen. Dit coördineert met invoegen en verwijderen. Stel vóór de lus een resultaatvariabele in, werk die bij een overeenkomst bij en verlaat de lus naar één ontgrendelpunt voordat je terugkeert. Een vroegtijdige terugkeer bij succes binnen de vergrendelde lus zou anders een eigen vrijgave nodig hebben.

In dit ontwerp houdt verwijderen dezelfde mutex vast terwijl een knoop wordt ontkoppeld. Andere threads kunnen die knoop niet aan het doorlopen zijn, omdat ze ook de mutex nodig hebben. Geef de knoop pas vrij wanneer geen andere bewerking die nog kan bereiken. Als een API een aanroepende thread na het ontgrendelen een knooppointer laat behouden, heeft dat extra gebruik een eigen afspraak over de levensduur nodig; de lijstvergrendeling dekt dit niet langer.

Bepaal of vergrendelingen per knoop lonen

Doorgeefvergrendeling verkrijgt de vergrendeling van de volgende knoop voordat die van de huidige knoop wordt vrijgegeven. Die overlap behoudt een beschermd pad terwijl het doorlopen vordert. Meerdere bewerkingen kunnen zich dan in verschillende gebieden van de lijst bevinden.

De extra gelijktijdigheid heeft een prijs: elke bezochte knoop voegt vergrendelings- en ontgrendelingsbewerkingen toe, ook als geen andere thread in de buurt is. Een werklast met veel doorlopen kan trager worden dan de versie met één mutex. Groepen knopen vergrendelen is een middenweg. Vergelijk het ingewikkeldere ontwerp onder de werkelijke werklast met het eenvoudige ontwerp, inclusief mislukte zoekopdrachten en verwijderpaden.

Belangrijkste punten

  • Bescherm het lezen van de oude kop en het publiceren van de nieuwe kop samen.
  • Wijs privégegevens van een knoop toe en initialiseer ze voordat je de lijst vergrendelt.
  • Zoeken en verwijderen volgen dezelfde beschermingsregel als invoegen.
  • Verkrijg bij doorgeefvergrendeling de vergrendeling van de volgende knoop voordat je die van de huidige vrijgeeft.

5.3 Scheid de uiteinden van wachtrijen en hashbuckets

Eén mutex per structuur is een verstandig uitgangspunt, maar kan bewerkingen die verschillende gebieden benaderen na elkaar laten uitvoeren. Twee nuttige verfijningen zijn een vergrendeling per uiteinde van een wachtrij en een vergrendeling per hashbucket.

Houd een dummy aan de kop van de wachtrij

Een wachtrij met twee vergrendelingen begint met één dummyknoop D en head == tail == D. D bevat geen huidig wachtrijelement. Het eerste echte element staat, als dat bestaat, op head->next. Een kopmutex laat verwijderingen na elkaar uitvoeren; een staartmutex doet hetzelfde voor toevoegingen. Daardoor kunnen één toevoeging en één verwijdering samen doorgaan.

Toevoegen bereidt de waarde van een nieuwe knoop en een lege volgende koppeling privé voor. Onder de staartmutex publiceert de bewerking die knoop via het veld next van de oude staart en verplaatst tail naar de nieuwe knoop. Verwijderen gebruikt de kopmutex om de volgende koppeling van de oude dummy te lezen. Als die leeg is, ontgrendelt de bewerking en meldt dat de wachtrij leeg is. Anders kopieert de bewerking de eerste echte waarde, verplaatst head naar die knoop, ontgrendelt en geeft de oude dummy vrij.

De knoop die de teruggegeven waarde bevatte, wordt de volgende dummy. Als je die knoop direct zou vrijgeven, zou head naar vrijgegeven geheugen blijven wijzen.

Volg de verschuivende dummy

Begin met D. De toevoeging van 5 maakt N5 en koppelt D -> N5, met tail == N5. De toevoeging van 8 voegt N8 toe en levert D -> N5 -> N8 op. Een verwijdering geeft 5 terug, verplaatst head naar N5 en geeft D vrij. N5 is nu de dummy; alleen 8 blijft als waarde in de wachtrij. Na nog een verwijdering wordt N8 de dummy en is de wachtrij leeg.

De twee uiteinden delen nog steeds een koppeling

Wanneer de wachtrij leeg is, kan toevoegen naar next van de huidige dummy schrijven terwijl verwijderen die onder een andere mutex leest. De twee mutexen alleen synchroniseren die toegangen niet. Declareer next in C11 als een atomaire pointer en publiceer en lees die met de juiste ordening; standaard sequentieel consistente atomaire opslag- en laadbewerkingen volstaan. Publicatie moet de voorbereide knoopwaarde zichtbaar maken voordat verwijderen die leest.

Wachten op een element vereist naast vergrendeling een signaleringsmechanisme.

Bepaal de hash op basis van de sleutel en vergrendel dan de bucket

Een hashtabel met een vaste grootte kan in elke bucket een threadveilige lijst gebruiken, elk met een eigen mutex. Met 101 buckets en bucket = key % 101 gebruiken sleutels 7 en 50 verschillende vergrendelingen. Sleutels 7 en 108 worden allebei aan bucket 7 gekoppeld en moeten daar na elkaar worden verwerkt. Botsingen beïnvloeden zowel de concurrentie als de lengte van zoekopdrachten.

Alle toegang tot de lijst van een bucket volgt de vergrendelingsregel van die bucket, inclusief lezen. Het wijzigen van de tabelgrootte verandert de koppeling aan buckets en verplaatst vermeldingen, dus dit eenvoudige ontwerp heeft tijdens die wijziging exclusieve toegang tot de hele tabel nodig. Eén bucketmutex kan een wijziging die elke bucket treft niet beschermen.

Verfijn pas nadat je het knelpunt hebt gevonden

Begin met een overzicht van de gedeelde toestand: welke velden worden gedeeld, welke mutex of atomaire bewerking beschermt elk veld en wanneer worden knopen bereikbaar of kunnen ze worden vrijgegeven? Controleer elk terugkeerpad en elk pad waarop toewijzing mislukt. Redeneer over vervlechtingen, test onder belasting met een raceconditiedetector en meet concurrentie voordat je fijnmazigere vergrendelingen toevoegt.

Belangrijkste punten

  • Een dummy laat bewerkingen aan de kop en staart van een wachtrij afzonderlijke vergrendelingen gebruiken.
  • Een geslaagde verwijdering geeft de oude dummy vrij en maakt van de knoop met de waarde de nieuwe dummy.
  • De gedeelde next-koppeling moet in een C11-wachtrij met twee vergrendelingen atomair worden gepubliceerd en gelezen.
  • Afzonderlijke bucketvergrendelingen laten bewerkingen in verschillende buckets overlappen.
  • Het wijzigen van de grootte van een hashtabel vereist coördinatie over de hele tabel.
  • Verfijn een correct ontwerp op basis van gemeten concurrentie en gecontroleerde geheugenlevensduren.

Threads, racecondities en vergrendelingen: voorbeelden

5 Walkthroughs

1

Volg een werkthread van aanmaak tot voltooiing

14 stappen
Maak onderscheid tussen gedeeld geheugen en threadtoestand, geef stabiele argumenten door en verzamel resultaten nadat je met join hebt gewacht.
  1. 1
    IntroductieBegin met twee soorten toestand

    Een programma kan meerdere threads hebben die de code doorlopen. Elke uitvoering heeft een eigen huidige instructie en onafgemaakte berekeningen nodig, terwijl de threads allemaal dezelfde programmaobjecten kunnen bereiken.

    Volg eerst die twee soorten toestand. Bouw daarna een som met twee werkthreads en bepaal wanneer de hoofdthread de resultaten mag lezen. De veiligheid van dat ontwerp hangt af van de levensduur van objecten en van voltooiing, niet van raden welke werkthread als eerste draait.

  2. 2
    UitlegGeef elke uitvoering een eigen plek

    Een thread heeft een eigen programmateller, registers, stack en thread-ID. De stack slaat de frames voor functieaanroepen en gewone lokale variabelen op. Het besturingssysteem bewaart de uitvoeringstoestand in een TCB, of threadbesturingsblok.

    Wanneer de scheduler tussen threads wisselt, herstelt die de toestand van de nieuwe thread. Een berekening in het register van de eerste thread blijft onafgemaakt totdat die thread verdergaat.

  3. 3
    VoorbeeldZoek de gedeelde objecten

    Stel dat total een globale variabele is en elke werkthread een lokale i declareert. Alle werkthreads bereiken één gedeelde total, maar elke functieaanroep heeft een eigen i.

    De threads delen ook de heap en open bestandsdescriptoren. Hun stacks staan op verschillende adressen binnen dezelfde adresruimte van het proces. Door het adres van een lokale variabele van een werkthread door te geven kan een andere thread die bereiken; afzonderlijke stacks dwingen geen isolatie af.

  4. 4
    VerbandLaat werk overlappen, ook op één kern

    Op één processorkern vervlecht de scheduler threads: ze maken gelijktijdig voortgang, maar worden niet op hetzelfde moment uitgevoerd. Op meerdere kernen kunnen ze parallel worden uitgevoerd.

    Threads kunnen onafhankelijke berekeningen versnellen of wachten op I/O laten overlappen met nuttig werk. Bij een wisseling binnen hetzelfde proces blijven de adresruimte en paginatabellen behouden, zodat die meestal minder kost dan een proceswisseling. Door gedeeld geheugen kan één ongeldige schrijfbewerking ook gegevens van andere threads beschadigen.

  5. 5
    UitlegBekijk wat de kernel kan inplannen

    In een een-op-een-model is elke toepassingsthread een thread die door de kernel wordt ingepland. De scheduler kan ze op verschillende kernen plaatsen en een blokkerende systeemaanroep stopt de aanroepende thread. Linux en Windows gebruiken dit model voor gewone threads.

    Een veel-op-een-bibliotheek voert gebruikersthreads uit op één kernelthread. Die gebruikersthreads kunnen niet op meerdere kernen worden uitgevoerd en één blokkerende systeemaanroep kan hun groep stoppen.

  6. 6
    UitdagingVeroorzaakt een extra kern de fout?

    Een werkthread pauzeert na het laden van gedeelde gegevens en een andere werkthread wijzigt die voordat de eerste verdergaat. Kan dat op één kern gebeuren?

    ---reveal---

    Ja. De scheduler kan wisselen tussen het laden en het latere gebruik van de waarde. Parallelle uitvoering kan meer overlap blootleggen, maar gelijktijdigheid alleen staat deze uitvoeringsvolgorde al toe. Een bewering dat een uitvoering op één kern veilig is, moet rekening houden met wisselingen tussen machine-instructies.

  7. 7
    UitlegStart een functie met één pointer

    Neem <pthread.h> op en bouw met -pthread. Een werkthreadfunctie heeft de signatuur void *worker(void *arg). pthread_create(&t, NULL, worker, arg) start de functie, vraagt standaardattributen aan en slaat de thread-ID op in t.

    De aanroep geeft bij succes 0 terug of rechtstreeks een foutnummer. Controleer dat resultaat voordat je ervan uitgaat dat de thread is aangemaakt. De nieuwe thread kan onmiddellijk draaien; de initialisatie moet al voltooid zijn.

  8. 8
    VoorbeeldVerdeel de som in afzonderlijke taken

    Bereid twee structs voor met lo, hi en result: het ene bereik is 1–50 en het andere 51–100. Geef aan elke werkthread een ander structadres door.

    Elke werkthread telt zijn bereik op in een eigen lokale somvariabele en schrijft daarna naar zijn eigen resultaatveld. De eerste krijgt 1.275 en de tweede 3.775. Geen werkthread schrijft naar het resultaat van de andere, zodat het ontwerp concurrerende wijzigingen van één somvariabele voorkomt.

  9. 9
    VoorbeeldWacht voordat je de antwoorden leest

    Maak beide werkthreads aan voordat je met join op een van beide wacht, zodat hun berekeningen kunnen overlappen. Daarna roept de hoofdthread voor elke geslaagde aanmaak pthread_join aan. Een join wacht op de voltooiing van de opgegeven thread en maakt de schrijfbewerkingen van die werkthread zichtbaar voor de wachtende thread.

    De hoofdthread kan nu de twee afzonderlijke resultaatvelden lezen en optellen tot 5.050. Door op slechts één werkthread met join te wachten staat niet vast dat de andere zijn eigen resultaat heeft voltooid.

  10. 10
    TipHoud argumenten en resultaten in leven

    Een argumentpointer verlengt de levensduur van een object niet. Houd structs van de aanmakende thread in leven totdat de werkthreads klaar zijn. Een teruggegeven pointer kan verwijzen naar heapgeheugen of geschikte opslag van de aanmakende thread; die kan niet veilig verwijzen naar een lokale variabele van de werkthread die niet meer bestaat.

    Geef in een aanmaaklus afzonderlijke stabiele array-elementen door in plaats van herhaaldelijk &i door te geven. Dat laatste laat elke werkthread naar dezelfde veranderende lusvariabele wijzen.

  11. 11
    UitdagingWelk resultaat is klaar?

    Twee werkthreads schrijven naar afzonderlijke resultaatvelden. De hoofdthread wacht met succes met join op werkthread A en leest daarna beide velden terwijl werkthread B mogelijk nog draait. Is die volledige leesbewerking veilig?

    ---reveal---

    Het resultaat van A is na de join klaar en zichtbaar. Het veld van B kan nog veranderen, dus moet de hoofdthread in dit ontwerp ook met join op B wachten voordat die het veld leest. Afzonderlijke opslag voorkomt dat werkthreads elkaar verstoren; join regelt de overdracht van elke werkthread naar de hoofdthread.

  12. 12
    TipOnderscheid het beëindigen van een thread van het beëindigen van zijn proces

    Terugkeren uit main of exit() aanroepen beëindigt het hele proces, inclusief onafgemaakte werkthreads. De hoofdthread wacht normaal met join op benodigde werkthreads voordat die terugkeert; pthread_exit() beëindigt alleen de aanroepende thread.

    Een losgekoppelde werkthread geeft zijn threadbronnen automatisch vrij wanneer die klaar is en kan niet met join worden afgewacht. Loskoppelen houdt het proces niet in leven en geeft de werkthread geen eigen kopie van de argumentobjecten van de aanmakende thread.

  13. 13
    UitdagingBeëindig het programma op het juiste moment

    De hoofdthread start twee werkthreads die zijn lokale array lezen, koppelt er één los en wacht met join op de andere. De losgekoppelde werkthread draait nog. Kan die werkthread dankzij zijn afzonderlijke stack de array blijven gebruiken nadat main terugkeert?

    ---reveal---

    Nee. Terugkeren uit main beëindigt het hele proces, inclusief die werkthread. De afzonderlijke stack kopieert de array ook niet: de pointer bereikt het object van de hoofdthread in hun gedeelde adresruimte. Door op één werkthread met join te wachten staat alleen de voltooiing van die werkthread vast.

  14. 14
    SamenvattingControleer de overdracht van de werkthread

    Zoek bij een ontwerp met een werkthread de uitvoeringstoestand, de objecten die de thread deelt en de levensduur van elk argument en resultaat. Bepaal daarna welke gebeurtenis een resultaat gereedmaakt voor de lezer.

    Afzonderlijke resultaatobjecten plus join-aanroepen kunnen vergrendelingen geheel overbodig maken. Gedeelde wijzigingen terwijl werkthreads overlappen vereisen extra coördinatie. Aanmaakvolgorde, afzonderlijke stacks en loskoppeling beantwoorden elk een andere vraag; geen van deze stelt vast dat een andere thread klaar is met schrijven.

2

Volg de bewerking die bescherming nodig heeft

11 stappen
Volg een verloren wijziging, onderscheid twee soorten racecondities en bescherm een beslissing samen met de bijbehorende handeling.

Sla deze les op om elke Walkthrough stap voor stap te volgen.

  1. 1
    IntroductieWerk de gedeelde bewerking uit
  2. 2
    UitlegOnderscheid geheugen van eigen registers
  3. 3
    VoorbeeldPauzeer vóór het opslaan
  4. 4
    VoorbeeldGa verder met het verouderde resultaat
  5. 5
    UitdagingVerander het moment van inplannen
  6. 6
    TipKies een oplossing met synchronisatie
  7. 7
    UitlegOnderscheid de twee racebegrippen
  8. 8
    VoorbeeldKeur een opname twee keer goed
  9. 9
    KernpuntBenoem de volledige sectie
  10. 10
    UitdagingVolg goedkeuring en verlaging samen
  11. 11
    SamenvattingBepaal de grens voordat je vergrendelt
3

Controleer een beschermingsregel met een mutex

14 stappen
Gebruik één gedeelde vergrendeling, kies de reikwijdte ervan en volg elk succes- en foutpad tot en met de vrijgave.

Sla deze les op om elke Walkthrough stap voor stap te volgen.

  1. 1
    IntroductieMaak de afspraak zichtbaar
  2. 2
    UitlegInitialiseer voordat je het object publiceert
  3. 3
    VoorbeeldPlaats de volledige wijziging binnen de vergrendeling
  4. 4
    UitdagingLees een trylock-resultaat
  5. 5
    TipKies de mutexsoort bewust
  6. 6
    UitlegLaat onafhankelijke gebieden overlappen
  7. 7
    VoorbeeldBereken de bovengrens door seriële uitvoering
  8. 8
    UitdagingVerkort het juiste werk
  9. 9
    KernpuntMeet drie verschillende uitkomsten
  10. 10
    VoorbeeldVolg de vroegtijdige terugkeer
  11. 11
    UitlegHoud één vergrendeling en één levensduur aan
  12. 12
    UitdagingHerken een terugkerende callback
  13. 13
    UitdagingKies een volledige correctie bij de controle
  14. 14
    SamenvattingLees code vanuit eigenaarschap
4

Bouw eigenaarschap, eerlijke beurten en efficiënt wachten op

21 stappen
Ga van ondeugdelijke vlagtests naar atomaire vergrendelingen en vergelijk daarna ticketvolgorde, slaapprotocollen en prioriteitsovererving.

Sla deze les op om elke Walkthrough stap voor stap te volgen.

  1. 1
    IntroductieBouw de vergrendeling in lagen op
  2. 2
    VoorbeeldTwee aanroepende threads komen door dezelfde test
  3. 3
    UitlegOrdening maakt deel uit van het algoritme
  4. 4
    UitdagingBeschermt het regelen van interrupts twee kernen?
  5. 5
    UitlegWissel de vlag atomair uit
  6. 6
    VoorbeeldMaak een overdraagbare vlag voor actief wachten
  7. 7
    VoorbeeldEen mislukte vergelijking is nuttige informatie
  8. 8
    UitlegKen de andere atomaire resultaten
  9. 9
    UitdagingProbeer de wijziging opnieuw, niet het oude antwoord
  10. 10
    KernpuntUitsluiting belooft geen beurt
  11. 11
    AnalogieGebruik een wachtrij met volgnummers
  12. 12
    VoorbeeldSchuif de beurt per eigenaar op
  13. 13
    UitdagingOnderscheid het uitdelen van de beurt
  14. 14
    UitlegDe CPU-beurt afstaan en slapen veranderen verschillende toestanden
  15. 15
    VoorbeeldZoek de periode waarin een weksignaal verloren kan gaan
  16. 16
    UitlegVergelijk en blokkeer als één beslissing
  17. 17
    UitdagingEen weksignaal komt vóór het wachten aan
  18. 18
    VerbandWacht kort actief wanneer de eigenaar kan afronden
  19. 19
    VoorbeeldBekijk prioriteitsinversie vanuit de eigenaar
  20. 20
    UitdagingCombineer planning met de vergrendeling
  21. 21
    SamenvattingVraag waarop de wachtende thread wacht
5

Volg gedeelde tellers, knopen en buckets

16 stappen
Werk tellers met groepsgewijze overdracht, lijstpublicatie, de verschuivende wachtrijdummy en vergrendeling per bucket uit.

Sla deze les op om elke Walkthrough stap voor stap te volgen.

  1. 1
    IntroductieBepaal de grens van het gedeelde gebied
  2. 2
    UitlegVerdeel frequente schrijfbewerkingen naar een teller
  3. 3
    VoorbeeldDraag één volledige groep over
  4. 4
    KernpuntStel een grens aan de ontbrekende verhogingen
  5. 5
    UitdagingKies de gewenste actualiteit voor een nieuwe werklast
  6. 6
    TipZet overdrachten stil voor een exacte momentopname
  7. 7
    VoorbeeldPubliceer de nieuwe lijstknoop als laatste
  8. 8
    UitlegLezers en verwijderaars delen de vergrendeling
  9. 9
    UitdagingDoorloop de lijst met vergrendelingen per knoop
  10. 10
    VoorbeeldInitialiseer de wachtrij zonder echt element
  11. 11
    VoorbeeldVerplaats de dummy bij het verwijderen
  12. 12
    KernpuntBij een lege wachtrij raken de twee uiteinden elkaar
  13. 13
    UitdagingVolg de volgende dummy
  14. 14
    VerbandScheid hashbuckets en behoud de indeling
  15. 15
    UitdagingCombineer een bucketvergrendeling met veilig invoegen
  16. 16
    SamenvattingMeet het volledige ontwerp

Threads, racecondities en vergrendelingen: oefenvragen

40 Oefenvragen in 4 sets

Set 1: Threadlevensduren en gedeelde wijzigingen10 Oefenvragen

Vraag 1 van 10

Twee threads horen bij hetzelfde proces. Welke uitvoeringstoestand houdt elke thread afzonderlijk bij?
  1. De eigen uitvoerbare code en heap
  2. De eigen programmateller en registers
  3. De eigen open bestanden en proces-ID
  4. De eigen globale variabelen en paginatabellen
Toon antwoordVerberg antwoord
Antwoord: B
Elke thread heeft een eigen programmateller en registers nodig om zijn eigen onafgemaakte uitvoering te kunnen hervatten. De heap hoort bij de gedeelde adresruimte, dus afzonderlijke uitvoeringspaden geven de threads geen afzonderlijke heapobjecten. Een object wordt privé door de manier waarop het wordt gebruikt, niet alleen doordat een thread het heeft toegewezen.

Vraag 2 van 10

Een server met één kern wisselt tussen twee verzoekthreads terwijl één thread op een netwerkantwoord wacht. Hoe beschrijf je hun uitvoering?
  1. Parallel, zonder gelijktijdige voortgang
  2. Gelijktijdig, met parallelle uitvoering
  3. Sequentieel, zonder overlappende voortgang
  4. Gelijktijdig, zonder parallelle uitvoering
Toon antwoordVerberg antwoord
Antwoord: D
De verzoeken maken gedurende overlappende perioden voortgang, wat gelijktijdigheid is. Eén kern voert op een bepaald moment slechts één thread uit, dus de threads worden niet parallel uitgevoerd. Wachten op een netwerkantwoord laten overlappen met CPU-werk vereist niet dat twee threads tegelijkertijd worden uitgevoerd.

Vraag 3 van 10

Een werkthread geeft een pointer naar een toegewezen resultaat terug. Welke aanroep wacht op thread t en plaatst die pointer in void *result?
  1. pthread_join(t, &result)
  2. pthread_join(t, result)
  3. pthread_create(&t, NULL, result, NULL)
  4. pthread_detach(t)
Toon antwoordVerberg antwoord
Antwoord: A
Het tweede join-argument is het adres van de variabele die de teruggegeven pointer ontvangt. Als je result zelf doorgeeft, geef je de huidige pointerwaarde door in plaats van de locatie die het resultaat moet ontvangen. Loskoppelen maakt automatische vrijgave van bronnen mogelijk, maar neemt de mogelijkheid weg om met join te wachten en die retourwaarde te verzamelen.

Vraag 4 van 10

In een model met laden, optellen en opslaan begint een teller op 36. A en B laden die elk, tellen elk lokaal 1 op en slaan daarna in die volgorde op. Wat is de eindwaarde van de teller?
  1. 36
  2. 38
  3. 37
  4. 39
Toon antwoordVerberg antwoord
Antwoord: C
Beide lokale berekeningen beginnen bij 36, dus beide opslagbewerkingen schrijven 37. De tweede opslag overschrijft dezelfde waarde; die telt niet nogmaals op bij het gedeelde geheugen. Voor een eindwaarde van 38 zou één thread moeten laden nadat de andere had opgeslagen, wat deze uitvoeringsvolgorde uitsluit.

Vraag 5 van 10

Een atomair saldo begint op 90. Twee threads lezen het elk atomair, keuren een verzoek om 60 goed en voeren daarna elk een atomaire aftrekking van 60 uit. Welke fout blijft mogelijk?
  1. Eén goedkeuring gaat verloren en het saldo wordt 30.
  2. De atomaire toegangen vormen een datarace in C.
  3. Beide keuren goed en het saldo wordt −30.
  4. Beide aftrekkingen worden één enkele wijziging.
Toon antwoordVerberg antwoord
Antwoord: C
Beide controles kunnen 90 zien voordat een aftrekking plaatsvindt, zodat beide verzoeken worden goedgekeurd en de twee atomaire aftrekkingen −30 overlaten. De atomaire bewerkingen voorkomen een datarace en behouden beide aftrekkingen. Ze houden de eerdere saldocontrole niet geldig totdat de bijbehorende handeling is voltooid.

Vraag 6 van 10

Een aanroep van pthread_mutex_trylock(&m) geeft EBUSY terug. Welk eigenaarschap heeft de aanroepende thread door die aanroep?
  1. De thread heeft geen eigenaarschap van m verkregen.
  2. De thread is eigenaar van m tot de volgende blokkering.
  3. De thread deelt m met de huidige houder.
  4. De thread is eigenaar van m zodra de houder ontgrendelt.
Toon antwoordVerberg antwoord
Antwoord: A
De melding dat de mutex bezet is betekent dat de verkrijgingspoging niet is geslaagd, zodat de aanroepende thread geen eigenaarschap heeft en de beschermde sectie niet kan betreden. Trylock reserveert geen toekomstige overdracht. De thread kan onafhankelijk werk doen of later opnieuw proberen de mutex te verkrijgen.

Vraag 7 van 10

Een startfunctie start twee werkthreads door herhaaldelijk &i, het adres van zijn lokale lusindex, door te geven en keert daarna terug terwijl ze draaien. De hoofdthread wil afzonderlijke resultaten van beide. Welk vervangend ontwerp maakt de overdracht van argumenten en resultaten veilig?
  1. De hoofdthread houdt één veranderende taakstruct in leven, start beide werkthreads en wacht vóór het lezen van elk resultaat met join op de bijbehorende thread.
  2. De startfunctie houdt twee lokale taakstructs bij, start beide werkthreads en keert terug; de hoofdthread wacht later met join voordat die leest.
  3. De hoofdthread houdt twee afzonderlijke taakstructs in leven, start beide werkthreads en leest de resultaten voordat die met join op een van beide wacht.
  4. De hoofdthread houdt twee afzonderlijke taakstructs in leven, start beide werkthreads en wacht vóór het lezen van elk resultaat met join op de bijbehorende thread.
Toon antwoordVerberg antwoord
Antwoord: D
Elke werkthread heeft een afzonderlijk stabiel argumentobject nodig en beide objecten moeten langer bestaan dan de werkthreads ze gebruiken. De hoofdthread moet daarna met join op elke werkthread wachten voordat die het resultaat leest waaraan de werkthread mogelijk nog schrijft. Join kan een lokaal object waarvan de levensduur al is geëindigd niet herstellen, en evenmin verhelpen dat twee werkthreads hetzelfde veranderende argument ontvangen.

Vraag 8 van 10

Een programma verliest gedeelde verhogingen op één kern. Waarom biedt het toevoegen van volatile aan de teller mogelijk geen oplossing?
  1. De volatile-kwalificatie dwingt alle threads registers te delen.
  2. Laden en opslaan kunnen nog steeds door een threadwisseling worden gescheiden.
  3. De teller wordt privé voor de thread die op dat moment draait.
  4. De kwalificatie stelt threadwisselingen uit tot na het laden.
Toon antwoordVerberg antwoord
Antwoord: B
Volatile combineert lezen, berekenen en schrijven niet tot één atomaire wijziging en synchroniseert de threads niet. Een contextwisseling kan nog steeds een andere wijziging tussen het laden en opslaan plaatsen. Elke thread heeft eigen registers voor de teller, waardoor de hervatte thread juist een verouderd resultaat kan behouden.

Vraag 9 van 10

Een capaciteitswaarde begint op 7. Verzoeken om 3, 5 en 2 controleren die elk onder een mutex voordat een verlaging plaatsvindt, zodat ze allemaal worden goedgekeurd. Daarna verlagen ze de waarde onder die mutex in dezelfde volgorde. Welke eindwaarde ontstaat en welke grens van de mutex herstelt de regel?
  1. Eindwaarde 2; behoud afzonderlijke eigendomsperioden voor controle en verlaging.
  2. Eindwaarde −3; behoud eigenaarschap tijdens elke controle en voorwaardelijke verlaging.
  3. Eindwaarde −3; behoud de afzonderlijke perioden maar maak elke toegang ook atomair.
  4. Eindwaarde −1; behoud eigenaarschap alleen tijdens elke al goedgekeurde verlaging.
Toon antwoordVerberg antwoord
Antwoord: B
Alle drie de verzoeken worden op basis van 7 goedgekeurd, zodat hun latere verlagingen achtereenvolgens 4, −1 en −3 overlaten. Onafgebroken eigenaarschap tijdens elke controle en de bijbehorende verlaging voorkomt de verouderde goedkeuringen. Atomaire afzonderlijke toegangen of al beschermde aftrekkingen sluiten de tussenruimte tussen beslissing en handeling niet.

Vraag 10 van 10

T1 is eigenaar van een mutex terwijl die twee samenhangende velden wijzigt en wordt daarna onderbroken. T2 roept pthread_mutex_lock aan op dezelfde mutex. Wat gebeurt er met de beschermde toegang?
  1. T2 wacht op verkrijging; T1 kan verdergaan en de wijziging afronden.
  2. T2 gaat naar binnen omdat T1 niet meer op de processor draait.
  3. T2 neemt eigenaarschap en voltooit de onafgemaakte wijziging van T1.
  4. T1 verliest eigenaarschap en moet beide veldwijzigingen opnieuw beginnen.
Toon antwoordVerberg antwoord
Antwoord: A
Eigenaarschap blijft behouden bij onderbreking, dus T2 kan niet naar binnen alleen omdat T1 niet op de CPU draait. T1 kan verdergaan met de mutex nog steeds vastgehouden, de wijziging van beide velden voltooien en de mutex vrijgeven. Een mutex regelt de toegang tot beschermde code; die vereist niet dat de eigenaar zonder onderbreking draait.
Set 2: Mutexgrenzen en atomair eigenaarschap10 Oefenvragen

Sla deze les op om elk antwoord te zien en elke set te oefenen.

Vraag 1 van 10

Eén mutex beschermt elke bucket in een tabel. Welke vergrendelingsgranulariteit is dit?
  1. Fijnmazige vergrendeling
  2. Doorgeefvergrendeling
  3. Grofmazige vergrendeling
  4. Vergrendeling per kern
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 2 van 10

Een zoekopdracht in een lijst vergrendelt de mutex, keert bij een overeenkomst direct terug en ontgrendelt alleen na de lus als er geen overeenkomst is. Waarom kunnen latere bewerkingen vastlopen?
  1. Een geslaagde zoekopdracht keert terug met de mutex nog vergrendeld.
  2. Een mislukte zoekopdracht geeft de mutex vóór het lezen van de kop vrij.
  3. Een geslaagde zoekopdracht vernietigt automatisch de mutex van de knoop.
  4. Een teruggegeven resultaat draagt mutexeigenaarschap over aan de aanroepende thread.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 3 van 10

In een vlagprotocol ziet A een niet-ingestelde vlag en pauzeert voordat A die instelt. B ziet de vlag als niet ingesteld, stelt die in en gaat naar binnen. Wat kan gebeuren wanneer A verdergaat?
  1. A moet de vlag opnieuw testen vóór de nog uit te voeren toewijzing.
  2. A stelt de vlag in en gaat ook naar binnen, waardoor uitsluiting wordt geschonden.
  3. De toewijzing van B verandert de waarde die A al heeft waargenomen.
  4. De contextwisseling van A maakt de voltooide beschikbaarheidstest ongedaan.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 4 van 10

Een atomaire uitwisseling slaat 1 op in een vlag die momenteel 0 bevat. Welke waarde geeft test-and-set terug?
  1. 1
  2. Het adres van de vlag
  3. Het ticket van de thread
  4. 0
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 5 van 10

Een dienst met 16 kernen gebruikt één mutex per rekening. Een overboeking verkrijgt eerst de mutex van de bronrekening en daarna die van de doelrekening. Gelijktijdige overboekingen kunnen in tegengestelde richtingen gaan. Welke regel laat onafhankelijke rekeningen gelijktijdig werken en verhelpt het probleem met de vergrendelingsvolgorde?
  1. Verkrijg eerst de bronmutex en geef die vrij vóór vergrendeling van de doelmutex.
  2. Verkrijg bij elke overboeking in beide richtingen eerst de doelmutex.
  3. Verkrijg één globale mutex rond elke bewerking op elke rekening.
  4. Verkrijg beide rekeningmutexen in oplopende volgorde van rekening-ID.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 6 van 10

Een CAS-lus leest een teller op 12 en bereidt 13 voor. Een andere thread verandert de teller vóór de CAS in 13. Hoe behoudt de eerste thread zijn verhoging?
  1. Herhaal dezelfde poging met verwachte waarde 12 totdat die slaagt.
  2. Probeer het opnieuw vanaf de nieuwe tellerwaarde en probeer 14 op te slaan.
  3. Sla de voorbereide 13 op zonder de teller opnieuw te controleren.
  4. Beschouw de mislukte poging als een al voltooide verhoging.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 7 van 10

Een ticketvergrendeling heeft turn == 6 en als volgend ongebruikt ticket ticket == 9. Een nieuwe aanroepende thread neemt een ticket. Op welk nummer wacht die?
  1. 9
  2. 6
  3. 8
  4. 10
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 8 van 10

Werkthreads kopiëren een registerstruct met een mutex en een pointer naar gedeelde records. Ze vergrendelen hun kopieën, keren vroegtijdig terug als validatie mislukt en lezen de status met afzonderlijke lokale mutexen. Welke correctie legt één beschermingsafspraak vast en geeft eigenaarschap vrij bij een fout?
  1. Gebruik het oorspronkelijke register via een pointer, bescherm schrijfbewerkingen met de mutex ervan en laat lezers eigen lokale vergrendelingen behouden.
  2. Initialiseer elke gekopieerde mutex, bescherm lezen en schrijven met die kopie en laat foutpaden via de ontgrendeling lopen.
  3. Gebruik het oorspronkelijke register via een pointer, bescherm lezen en schrijven met de mutex ervan en laat foutpaden via ontgrendeling lopen.
  4. Gebruik het oorspronkelijke register via een pointer, bescherm lezen en schrijven met de mutex ervan en keer bij een fout direct terug.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 9 van 10

Twee threads voeren het protocol van Peterson uit. Hun eigen schrijfbewerkingen naar de vlaggen blijven gebufferd, zodat beide bij het later lezen van de vlag de andere als niet geïnteresseerd zien. Alleen turn is atomair; de vlaggen zijn gewone gehele getallen in C. Welke conclusie en correctie volgen hieruit?
  1. Slechts één kan naar binnen; een atomaire turn behoudt op zichzelf het hele protocol.
  2. Beide kunnen naar binnen; gebruik correct geordende atomaire toegang voor vlaggen en turn.
  3. Beide kunnen naar binnen; alleen de gewone vlaggen volatile maken herstelt de volgorde.
  4. Slechts één kan naar binnen; afzonderlijke cachelijnen voorkomen dataraces op gewone vlaggen.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 10 van 10

Elke wijziging van een paar gedeelde velden gebruikt mutex m, maar een diagnosefunctie leest beide velden zonder m. Wat moet de diagnosefunctie doen om een consistent beschermd paar te verkrijgen?
  1. De lokale kopieën volatile declareren.
  2. Een eigen mutex vasthouden tijdens het lezen.
  3. m vasthouden tijdens het lezen van beide velden.
  4. Het eerste veld twee keer zonder vergrendeling lezen.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Set 3: Wachtprotocollen en gedeelde structuren10 Oefenvragen

Sla deze les op om elk antwoord te zien en elke set te oefenen.

Vraag 1 van 10

Een thread die op een vergrendeling wacht, slaapt in een kernelwachtrij. Hoeveel CPU-tijd verbruikt die zolang die geblokkeerd blijft?
  1. Een volledige tijdschijf in elke schedulercyclus.
  2. Voortdurend één kern totdat de eigenaar vrijgeeft.
  3. Kort actief wachten bij elke beschermde geheugentoegang.
  4. Geen voor het uitvoeren van die geblokkeerde thread.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 2 van 10

A heeft ticket 20; B en C hebben 21 en 22. A geeft vrij, B draait tijdelijk niet en C blijft draaiend wachten. D neemt nu het volgende ticket en er komen geen threads meer bij. Wat is het volgende ongebruikte ticket en welke volgorde van binnenkomst moet volgen zodra B kan draaien?
  1. Volgende ongebruikte ticket 23; B, dan C, dan D.
  2. Volgende ongebruikte ticket 24; C, dan D, dan B.
  3. Volgende ongebruikte ticket 24; B, dan C, dan D.
  4. Volgende ongebruikte ticket 22; B, dan D, dan C.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 3 van 10

Een wachtende thread besluit te gaan slapen met park, pauzeert en ontvangt een unpark voordat die park aanroept. De unpark wordt weggegooid. Welk risico ontstaat?
  1. De thread kan gaan slapen nadat het enige weksignaal al voorbij is.
  2. De thread kan twee tickets voor dezelfde plek in de wachtrij krijgen.
  3. De thread moet tot de volgende tijdschijf voortdurend actief wachten.
  4. De thread wordt direct wakker omdat park unpark altijd onthoudt.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 4 van 10

Een benaderende teller gebruikt drie lokale plekken en drempel 4. Vanaf nul zijn hun aantallen wijzigingen 6, 1 en 3. Wat zijn de globale telling en het werkelijke totaal nadat alle wijzigingen zijn voltooid?
  1. Globaal 8; werkelijk totaal 10.
  2. Globaal 4; werkelijk totaal 10.
  3. Globaal 6; werkelijk totaal 10.
  4. Globaal 4; werkelijk totaal 6.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 5 van 10

Welk werk kan zonder de lijstmutex worden gedaan voordat een nieuw toegewezen lijstknoop in een gedeelde structuur wordt gekoppeld?
  1. De gedeelde kop lezen voor een latere wijziging van de koppeling.
  2. De gedeelde kop wijzigen om de nieuwe knoop te publiceren.
  3. Het sleutelveld van de privéknoop initialiseren.
  4. Gedeelde knopen doorlopen die bij verwijdering kunnen worden vrijgegeven.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 6 van 10

Een wachtrij bevat dummy R, gevolgd door knopen met 14 en 29. Welke knoop wordt na één geslaagde verwijdering vrijgegeven en welke wordt de dummy?
  1. Geef de knoop met 14 vrij; R blijft de dummy.
  2. Geef R vrij; de knoop met 14 wordt de dummy.
  3. Geef R vrij; de knoop met 29 wordt de dummy.
  4. Geef de knoop met 29 vrij; R blijft de dummy.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 7 van 10

Een vaste hashtabel gebruikt 17 buckets, key % 17 en één mutex per bucket. Werkthreads wijzigen sleutels 8, 25 en 26. Welk paar moet na elkaar worden verwerkt en welke sleutel gebruikt een afzonderlijke vergrendeling?
  1. 8 en 25 na elkaar; 26 gebruikt een afzonderlijke vergrendeling.
  2. 8 en 26 na elkaar; 25 gebruikt een afzonderlijke vergrendeling.
  3. 25 en 26 na elkaar; 8 gebruikt een afzonderlijke vergrendeling.
  4. Alle drie na elkaar; elk gebruikt dezelfde bucketvergrendeling.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 8 van 10

Een benaderende teller heeft vier lokale plekken en verhogingen met één eenheid. De achterstand na voltooide wijzigingen mag nooit groter zijn dan 20. Kies de grootste gehele drempel die aan die grens voldoet. De plekken voeren daarna vanaf nul 17, 8, 5 en 0 wijzigingen uit. Welke drempel en globale waarde ontstaan?
  1. Drempel 5; globale waarde 25.
  2. Drempel 7; globale waarde 21.
  3. Drempel 6; globale waarde 30.
  4. Drempel 6; globale waarde 18.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 9 van 10

Een thread die een lijst met doorgeefvergrendeling doorloopt, houdt de vergrendeling van de huidige knoop vast. In welke volgorde moet de thread naar de volgende knoop gaan?
  1. Geef de huidige vergrendeling vrij en verkrijg daarna de volgende.
  2. Geef de huidige vergrendeling vrij en kopieer daarna de volgende pointer.
  3. Verkrijg alle latere knoopvergrendelingen voordat je er één vrijgeeft.
  4. Verkrijg de volgende vergrendeling en geef daarna de huidige vrij.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 10 van 10

Op één CPU met strikte prioriteitsplanning houdt L met lage prioriteit een vergrendeling vast, wacht H met hoge prioriteit daar actief op en is M met middelhoge prioriteit gereed. Eerst wordt de vergrendeling veranderd in een ticketspinlock. Daarna wordt die vervangen door een mutex met prioriteitsovererving waarop H blokkeert. Welke voortgang staat de tweede wijziging toe die de ticketvolgorde alleen niet toestaat?
  1. L draait vóór M, geeft vrij en laat H doorgaan; bij alleen ticketvolgorde verhindert H dat L draait.
  2. H gaat vóór L naar binnen, geeft vrij en laat M doorgaan; bij alleen ticketvolgorde moet M eerst vrijgeven.
  3. M draait vóór L, geeft de mutex van H vrij en laat H doorgaan; bij alleen ticketvolgorde blokkeren alle prioriteiten gelijk.
  4. L blijft onder M totdat M klaar is; alleen ticketvolgorde zou L juist tot de prioriteit van H verhogen.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Set 4: Beoordeel concurrente ontwerpen over grenzen heen10 Oefenvragen

Sla deze les op om elk antwoord te zien en elke set te oefenen.

Vraag 1 van 10

Een werkthread waarop met join kan worden gewacht, zal zijn retourwaarde nooit laten verzamelen en moet zijn threadbronnen vrijmaken wanneer die eindigt. Welke pthread-bewerking regelt dit?
  1. Roep pthread_join aan zonder op de thread te wachten.
  2. Koppel de werkthread los met pthread_detach.
  3. Keer terug uit main voordat de werkthread eindigt.
  4. Kopieer de pthread_t van de werkthread lokaal.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 2 van 10

W ziet een futexvergrendelingswoord op 1. De eigenaar zet het op 0 en wekt voordat W wait aanroept. C verkrijgt de vergrendeling daarna en zet het woord op 1. W roept futex-wait aan met verwachte waarde 1 en C wist het woord later en wekt. Kan W bij die aanroep blokkeren en geeft het latere weksignaal eigenaarschap?
  1. W kan blokkeren op de door C vastgehouden toestand; na het wekken moet W opnieuw proberen te verkrijgen.
  2. W kan niet blokkeren omdat een eerdere eigenaar al een wekmelding stuurde.
  3. W kan blokkeren op de door C vastgehouden toestand; het latere weksignaal geeft automatisch eigenaarschap.
  4. W moet voor altijd blokkeren omdat de eerdere leesbewerking naar een andere eigenaar verwees.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 3 van 10

Een wachtrij met twee vergrendelingen is leeg bij dummy D. E bereidt knoop R met waarde 11 voor, publiceert D->next = R en pauzeert voordat de staart wordt verplaatst. Q verwijdert daarna met succes een element; vervolgens voltooit E het verplaatsen van de staart naar R. Welke toestand blijft over en hoe moet toegang tot next tussen de uiteinden in C11 worden geïmplementeerd?
  1. Eén element 11 bij R; publiceer en lees next atomair met geschikte ordening.
  2. Leeg met dummy R; gewone toegang tot next wordt door de afzonderlijke uiteindemutexen geordend.
  3. Leeg met dummy D; volatile toegang tot next biedt de vereiste publicatievolgorde.
  4. Leeg met dummy R; publiceer en lees next atomair met geschikte ordening.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 4 van 10

Elk verzoek houdt één gedeelde mutex 5 µs vast. Als je alle andere kosten buiten beschouwing laat, welke bovengrens aan de doorvoer legt dat beschermde werk op?
  1. 5.000 verzoeken per seconde.
  2. 1.000.000 verzoeken per seconde.
  3. 200.000 verzoeken per seconde.
  4. 5.000.000 verzoeken per seconde.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 5 van 10

Een teller heeft één lokale plek op 5 en een globale telling op 40. Een lezer kopieert de globale telling onder de bijbehorende vergrendeling en geeft die vrij. Een werkthread draagt de lokale 5 globaal over en zet de lokale telling terug. De lezer kopieert daarna de lokale telling onder de bijbehorende vergrendeling. Welk totaal berekent de lezer en hoe kan die een exacte momentopname verkrijgen?
  1. De lezer berekent 40; houd voor de momentopname alle lokale vergrendelingen en daarna de globale vast.
  2. De lezer berekent 45; lees globaal twee keer zonder lokale vergrendelingen te verkrijgen.
  3. De lezer berekent 50; vergrendel tijdens de hele momentopname alleen de globale teller.
  4. De lezer berekent 40; vergrendel elk veld alleen kort tijdens het kopiëren ervan.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 6 van 10

Een cachewijziging houdt een normale mutex vast en roept een callback aan die dezelfde cache bevraagt. De callback na ontgrendeling uitvoeren zou herintreding voorkomen, maar een andere werkthread zou dan de knoop kunnen vrijgeven waarnaar de ruwe pointer van de callback verwijst. Welk herontwerp laat de callback buiten de vergrendeling uitvoeren en houdt de invoer geldig?
  1. Behoud de ruwe knooppointer, ontgrendel vóór de callback en laat de callback bij het starten dezelfde mutex verkrijgen.
  2. Kopieer de benodigde velden onder de mutex naar opslag van de callback, ontgrendel en roep de callback aan met die kopie.
  3. Kopieer de velden naar opslag van de callback, houd de mutex vast en laat de callback via dezelfde mutex gegevens opvragen.
  4. Behoud de ruwe knooppointer, ontgrendel en maak de pointer volatile zodat latere verwijdering de knoop niet ongeldig kan maken.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 7 van 10

Een store-conditional mislukt na een load-linked, hoewel de code geen aanwijzing heeft voor een andere schrijver. Hoe moet de wijzigingsroutine reageren?
  1. Beschouw de mislukte store-conditional als geslaagd.
  2. Schrijf de waarde met een gewone onvoorwaardelijke opslag.
  3. Probeer de LL/SC-reeks opnieuw met een nieuwe load-linked.
  4. Geef de oude waarde terug en meld dat de wijziging is vastgelegd.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 8 van 10

Twee zoekopdrachten vinden een knoop. A kopieert de gehele waarde onder de lijstmutex naar opslag van de aanroepende thread. B bewaart een pointer en ontgrendelt. Een verwijderbewerking ontkoppelt en bevrijdt daarna de knoop; het adres kan opnieuw worden gebruikt voordat B leest. Welk resultaat blijft geldig en wat moet de interface van B bieden?
  1. Beide resultaten blijven geldig; B hoeft de lijst alleen opnieuw te vergrendelen voordat die het opgeslagen adres leest.
  2. Alleen de pointer van B blijft geldig; A moet de lijst steeds vergrendelen wanneer die de gekopieerde waarde leest.
  3. De gekopieerde waarde van A blijft geldig; de opgeslagen pointer van B volatile maken behoudt de verwijderde knoop.
  4. De gekopieerde waarde van A blijft geldig; B heeft een waardekopie of bescherming van de verdere levensduur van de knoop nodig.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 9 van 10

Het volgende ongebruikte ticket van een ticketvergrendeling is 15 en de huidige beurt is 12. Drie aanroepende threads nemen tickets vóór een vrijgave. Wat zijn hun tickets en wat is het nieuwe volgende uit te delen nummer?
  1. Tickets 15, 16, 17; volgende ongebruikte ticket 18.
  2. Tickets 12, 13, 14; volgende ongebruikte ticket 15.
  3. Tickets 16, 17, 18; volgende ongebruikte ticket 18.
  4. Tickets 15, 15, 15; volgende ongebruikte ticket 18.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 10 van 10

Een tabel groeit van 11 naar 17 buckets en gebruikt de sleutel modulo het aantal buckets. Een werkthread heeft de oude bucket voor sleutel 24 geselecteerd. De bewerking die de tabelgrootte wijzigt, vergrendelt alleen de nieuwe doelbucket, verplaatst de vermelding en geeft de oude bucketarray vrij. Welke doelbucket en overgangsregel houden dit eenvoudige ontwerp intact?
  1. Bucket 2; behoud de oude indeling terwijl het nieuwe aantal buckets voor gewone bewerkingen wordt gepubliceerd.
  2. Bucket 7; bescherm alleen de nieuwe doelbucket omdat de oude werkthread zijn index eerder berekende.
  3. Bucket 7; sluit gewone tabelbewerkingen uit en laat oude gebruikers afronden voordat je de oude opslag vrijgeeft.
  4. Bucket 2; bescherm alleen de oude bronbucket terwijl je na de verplaatsing elke oude bucket vrijgeeft.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Kom je niet uit je eigen opgave? Huiswerkhulp

Kernbegrippen

75 Kernbegrippen, elk met een definitie

A

Aanmaken van een thread
Het aanmaken van een thread start een nieuw uitvoeringspad bij een opgegeven functie, met een argumentpointer en instelbare threadattributen.
De hoofdthread bereidt een bereikstruct voor voordat die het adres ervan doorgeeft aan een met succes aangemaakte werkthread die een som berekent.
Actief wachten
Actief wachten is het herhaaldelijk testen van een conditie terwijl de thread uitvoerbaar blijft en CPU-tijd gebruikt, totdat die conditie voortgang toestaat.
Een wachtende thread kan zijn hele tijdschijf besteden aan het controleren van een vlag terwijl de onderbroken eigenaar niet kan draaien.
Adresruimte
Een adresruimte is de verzameling geheugenadressen die beschikbaar zijn voor een proces; de threads van dat proces delen de code en objecten daarin.
De stacks van twee werkthreads bevinden zich in verschillende gebieden van één adresruimte, zodat een pointer tussen die gebieden kan verwijzen.
Afstaan van de CPU-beurt
Bij het afstaan van de CPU-beurt geeft de aanroepende thread zijn huidige beurt op de CPU op, maar blijft die gereed om opnieuw te draaien wanneer de scheduler de thread kiest.
Veel wachtende threads die hun CPU-beurt afstaan kunnen herhaalde contextwisselingen veroorzaken voordat de eigenaar weer aan de beurt komt.
Algoritme van Peterson
Het algoritme van Peterson is een uitsluitingsprotocol voor twee threads met belangstellingsvlaggen en een beurtvariabele, dat correct is onder sequentiële consistentie.
Wanneer beide threads om toegang vragen, bepaalt de beurtvariabele welke geïnteresseerde thread wacht.
Atomiciteit
Atomiciteit betekent dat een bewerking voor de betrokken waarnemers ondeelbaar lijkt, zodat zij geen gedeeltelijk voltooide tussentoestand kunnen waarnemen.
Eén gedeelde mutex kan een wijziging van twee velden atomair maken voor andere gebruikers van dezelfde mutex.

B

Begrensd wachten
Begrensd wachten stelt een grens aan het aantal keren dat concurrerende threads een kritieke sectie mogen betreden nadat een thread om toegang heeft gevraagd en voordat die thread naar binnen gaat.
Eerdere nog geldige tickets begrenzen het aantal keren dat threads naar binnen gaan vóór de beurt van een wachtende thread bij een ticketvergrendeling.
Benaderende teller
Een benaderende teller draagt beschermde lokale wijzigingen in groepen over naar een beschermde globale telling, waarvan de gelezen waarde kan achterlopen op het exacte totaal.
Een statistiekverzamelaar kan accepteren dat recente verhogingen lokaal blijven totdat hun overdrachtsdrempel is bereikt.
Beschermingsregel
Een beschermingsregel wijst gedeelde toestand toe aan een bepaalde vergrendeling en bepaalt welke toegangen diezelfde vergrendeling moeten verkrijgen.
Een commentaar naast een teller kan de mutex noemen die elke gelijktijdige leesbewerking en wijziging moet verkrijgen.
Beurtteller
Een beurtteller houdt het ticket bij dat momenteel een ticketvergrendeling mag betreden en wordt één keer verhoogd wanneer de eigenaar vrijgeeft.
Een nieuw ticket nemen verhoogt het uit te delen nummer terwijl de huidige beurt ongewijzigd blijft.
Bucketvergrendeling
Een bucketvergrendeling beschermt de vermeldingen van één hashbucket, zodat bewerkingen op verschillende buckets onder verschillende vergrendelingen kunnen doorgaan.
Twee sleutels met dezelfde bucketindex concurreren om dezelfde vergrendeling, ook al hebben ze verschillende waarden.

C

Compare-and-swap
Compare-and-swap vervangt atomair een waarde alleen wanneer die overeenkomt met een verwachte waarde en geeft voldoende informatie terug om mislukking te herkennen.
Een thread die een teller bijwerkt, probeert het opnieuw wanneer een concurrerende verhoging de verwachte waarde verouderd maakt.
Concurrentie om gedeelde bronnen
Concurrentie om gedeelde bronnen ontstaat wanneer threads om een gedeelde bron wedijveren, bijvoorbeeld wanneer ze een mutex proberen te verkrijgen die een andere thread vasthoudt.
Werkthreads aan één vergrendelde teller toevoegen kan de wachttijd en het gedeelde cacheverkeer verhogen in plaats van de doorvoer.

D

Datarace
Een datarace is conflicterende, niet-gesynchroniseerde toegang tot één geheugenlocatie door verschillende threads, waaronder een schrijfbewerking en een niet-atomaire toegang.
Een statusleesbewerking kan een race met een gewone schrijfbewerking naar een teller hebben, ook al verandert de statusfunctie niets.
Doorgeefvergrendeling
Doorgeefvergrendeling verkrijgt de vergrendeling van de volgende knoop voordat die van de huidige wordt vrijgegeven, zodat de bescherming tijdens het doorlopen behouden blijft.
Verschillende doorlopen kunnen zich in afzonderlijke lijstgebieden bevinden, hoewel elke bezochte knoop vergrendelingsoverhead toevoegt.
Dummyknoop
Een dummyknoop verankert een gekoppelde wachtrij zonder een huidig element te vertegenwoordigen; de volgende knoop bevat, indien aanwezig, de eerste waarde in de wachtrij.
Een verwijdering geeft de waarde van de volgende knoop terug, geeft de oude dummy vrij en maakt de knoop met de waarde de nieuwe dummy.

E

Een-op-een-threadmodel
Een een-op-een-threadmodel koppelt elke toepassingsthread aan een thread die door de kernel wordt ingepland, zodat parallelle uitvoering en blokkering per thread mogelijk zijn.
De scheduler van het besturingssysteem kan twee gewone Linux-threads op afzonderlijke kernen plaatsen.
Eigenaarschap van een vergrendeling
Eigenaarschap van een vergrendeling is het exclusieve recht dat door een geslaagde verkrijging ontstaat en behouden blijft totdat de eigenaar de mutex vrijgeeft.
Een contextwisseling pauzeert de eigenaar zonder een wachtende thread toestemming te geven om naar binnen te gaan.
Exacte momentopname
Een exacte momentopname leest samenhangende gedeelde waarden consistent in één beschermde toestand en voorkomt dat overdrachten de waarneming splitsen.
Een momentopname van een benaderende teller houdt de lokale en globale vergrendelingen vast terwijl alle tellingen worden opgeteld.

F

Fetch-and-add
Fetch-and-add telt atomair een hoeveelheid bij een opgeslagen waarde op en geeft de waarde terug die vóór de optelling bestond.
Een ticketuitgever geeft 12 terug aan een aanroepende thread terwijl het volgende beschikbare nummer naar 13 wordt verhoogd.
Fijnmazige vergrendeling
Fijnmazige vergrendeling geeft afzonderlijke vergrendelingen aan kleinere gebieden van gedeelde toestand, zodat onafhankelijke bewerkingen gelijktijdig kunnen doorgaan.
Afzonderlijke rekeningmutexen laten twee niet-gerelateerde rekeningparen tegelijk bijwerken.
Futex
Een futex is een woord in gebruikersgeheugen met ondersteuning van de Linux-kernel om atomair een verwachte waarde te controleren en te blokkeren, en om wachtende threads te wekken.
Een mutex zonder concurrentie kan atomaire bewerkingen in de gebruikersruimte gebruiken en de kernel alleen aanroepen wanneer bij concurrentie moet worden gewacht.

G

Geblokkeerde thread
Een geblokkeerde thread kan niet voor uitvoering worden gekozen totdat een gebeurtenis die weer uitvoerbaar maakt en gebruikt daarom tijdens de blokkering geen CPU.
Een slapende thread die op een mutex wacht, verlaat de verzameling uitvoerbare threads totdat een weksignaal die weer laat concurreren.
Geheugenlek
Een geheugenlek is toegewezen opslag die niet meer nodig is maar niet kan worden vrijgemaakt omdat het programma de bruikbare verwijzing ernaar is kwijtgeraakt.
Twee concurrerende invoegingen vooraan kunnen de kop overschrijven en de eerste nieuwe knoop onbereikbaar achterlaten.
Gelijktijdigheid
Gelijktijdigheid is voortgang van meerdere uitvoeringen gedurende overlappende perioden, inclusief vervlochten uitvoering op één processorkern.
Een timer kan wisselen tussen gelijktijdig actieve werkthreads, ook wanneer de machine op een bepaald moment maar één thread kan uitvoeren.
Grofmazige vergrendeling
Grofmazige vergrendeling beschermt een grote structuur of een groot subsysteem met één vergrendeling, wat correcte bescherming vereenvoudigt maar onafhankelijke toegang beperkt.
Eén lijstmutex laat elke zoekopdracht wachten op een invoeging, ook als het gezochte element ver daarvandaan ligt.

H

Hashbotsing
Een hashbotsing ontstaat wanneer verschillende sleutels aan dezelfde tabelbucket worden gekoppeld en daardoor een zoekgebied en de bijbehorende vergrendeling delen.
Bij 13 buckets botsen sleutels 2 en 15 omdat beide rest 2 hebben bij deling door 13.
Hashbucket
Een hashbucket is een tabelgebied dat door de hashindeling van een sleutel wordt gekozen en vermeldingen bevat die aan diezelfde locatie zijn gekoppeld.
Modulo 101 plaatst sleutels 7 en 108 in dezelfde bucket, hoewel hun numerieke waarden verschillen.
Herintredende aanroep
Een herintredende aanroep gaat opnieuw een bewerking binnen voordat de eerdere aanroep is voltooid en kan daarbij proberen een vastgehouden vergrendeling opnieuw te verkrijgen.
Een callback voor logregistratie kan dezelfde cache bevragen terwijl de buitenste wijziging nog eigenaar is van de cachemutex.

K

Kritieke sectie
Een kritieke sectie is een codegebied dat gedeelde toestand benadert en concurrerende bewerkingen moet uitsluiten om de juistheid van de bewerking te behouden.
De kritieke sectie van een opname begint vóór de saldocontrole en omvat de voorwaardelijke aftrekking.

L

Levensduur van een object
De levensduur van een object is de periode waarin een opgeslagen object bestaat en via een verder geldige verwijzing mag worden benaderd.
Een pointer naar een lokale variabele van een werkthread teruggeven werkt niet, omdat dat object ophoudt te bestaan wanneer de functie eindigt.
Lezen-wijzigen-schrijven
Een bewerking van het type lezen-wijzigen-schrijven leest een opgeslagen waarde, berekent een gewijzigde waarde en schrijft het resultaat terug naar die gedeelde locatie.
Bij een gewone tellerverhoging zit een tussenruimte tussen het eerste laden en het uiteindelijke opslaan.
Load-linked/store-conditional
Load-linked/store-conditional koppelt een leesbewerking die een reservering vastlegt aan een schrijfpoging die kan mislukken en bij mislukking opnieuw moet worden geprobeerd.
Een ongeldig gemaakte reservering laat store-conditional mislukken in plaats van een concurrerende wijziging te overschrijven.
Losgekoppelde thread
Een losgekoppelde thread geeft zijn threadbronnen automatisch vrij wanneer die eindigt en kan niet met join worden afgewacht om de retourwaarde te verzamelen.
Een achtergrondwerkthread kan worden losgekoppeld, maar terugkeren uit main beëindigt nog steeds het proces waarin de thread draait.

M

Mutex
Een mutex is een vergrendeling voor wederzijdse uitsluiting waarvan steeds één thread eigenaar is en die een gedeelde bewerking of een gedeeld gegevensgebied beschermt.
Alle saldobewerkingen verkrijgen dezelfde rekeningmutex voordat ze het beschermde saldoveld gebruiken.
Mutex met foutcontrole
Een mutex met foutcontrole meldt bepaalde fouten in eigenaarschap, waaronder opnieuw vergrendelen door de eigenaar en vrijgave door een thread die geen eigenaar is.
Tijdens de ontwikkeling kan een nieuwe vergrendelingspoging EDEADLK teruggeven in plaats van dezelfde thread ongemerkt te blokkeren.
Mutexidentiteit
Mutexidentiteit is gebonden aan het oorspronkelijke mutexobject op het geïnitialiseerde adres; een gekopieerde pthread-mutex is geen geldige alternatieve verwijzing.
Geef een gedeeld register via een pointer door in plaats van de struct te kopiëren en de gekopieerde mutex te gebruiken.

N

Niet-blokkerende vergrendelingspoging
Een niet-blokkerende vergrendelingspoging probeert direct een mutex te verkrijgen en meldt of dat is gelukt, in plaats van te wachten zolang de mutex bezet is.
Een EBUSY-resultaat laat een werkthread ander werk kiezen, maar geeft geen toegang tot beschermde velden.

O

Opruimpad
Een opruimpad is een gemeenschappelijke afsluitreeks die verkregen bronnen vrijgeeft voordat een functie na succes of een fout terugkeert.
Een zoekopdracht bewaart zijn retourwaarde, verlaat de lus en bereikt één gemeenschappelijke ontgrendeling van de mutex.
Ordening van geheugenbewerkingen
Ordening van geheugenbewerkingen bepaalt beperkingen op hoe geheugenbewerkingen zichtbaar worden, inclusief de bewerkingen rond de overdracht van vergrendelingen.
Het vrijgeven van een vergrendeling moet de beschermde schrijfbewerkingen vóór de publicatie van de nieuwe beschikbare toestand houden.
Overdrachtsdrempel
Een overdrachtsdrempel is de lokale telling waarbij een benaderende teller de verzamelde verhogingen naar de globale telling overbrengt.
Een hogere drempel vermindert het aantal globale verkrijgingen, maar laat meer verhogingen ongerapporteerd blijven.

P

Parallellisme
Parallellisme is uitvoering op hetzelfde moment op verschillende verwerkingsbronnen, zoals meerdere threads die op afzonderlijke processorkernen draaien.
Onafhankelijke delen van een afbeelding kunnen parallel worden verwerkt wanneer meerdere kernen beschikbaar zijn.
POSIX-threads
POSIX-threads vormen de gestandaardiseerde threadinterface die C-programma's op Unix-achtige systemen gebruiken om threads aan te maken en te coördineren.
Een C-programma met werkthreads neemt <pthread.h> op en wordt met de optie -pthread gecompileerd en gekoppeld.
Prioriteitsinversie
Prioriteitsinversie ontstaat wanneer werk met hogere prioriteit wacht op een eigenaar met lagere prioriteit wiens voortgang door andere planningsactiviteit wordt vertraagd.
Werk met middelhoge prioriteit kan een mutexeigenaar met lage prioriteit verhinderen vrij te geven voor een wachtende thread met hoge prioriteit.
Prioriteitsovererving
Prioriteitsovererving verhoogt de prioriteit van een eigenaar van een vergrendeling tijdelijk tot de hogere prioriteit van een wachtende thread, zodat de eigenaar kan draaien en eerder kan vrijgeven.
Door de prioriteit van de eigenaar te verhogen kan werk met middelhoge prioriteit de benodigde vrijgave niet voor onbepaalde tijd vertragen.
Publicatie van een knoop
Publicatie van een knoop maakt een voorbereide privéknoop bereikbaar via gedeelde koppelingen, zodat andere bewerkingen de geïnitialiseerde gegevens veilig kunnen vinden.
Bij invoegen vooraan wordt eerst de sleutel ingevuld en wordt de kop alleen gepubliceerd terwijl de gedeelde lijstmutex wordt vastgehouden.

R

Raceconditie
Een raceconditie ontstaat wanneer de juistheid van een bewerking afhangt van de relatieve timing van gelijktijdige handelingen.
Twee afzonderlijk beschermde goedkeuringen kunnen allebei het oude saldo gebruiken voordat een van de opnames wordt verwerkt.
Recursieve mutex
Een recursieve mutex staat herhaalde verkrijging door de eigenaar toe en wordt pas beschikbaar na het bijbehorende aantal vrijgaven.
Twee geslaagde verkrijgingen door één thread vereisen twee ontgrendelingen voordat een andere thread de mutex kan verkrijgen.

S

Sequentiële consistentie
Sequentiële consistentie geeft bewerkingen één gemeenschappelijke volgorde die de programmavolgorde van elke thread behoudt en door alle threads wordt waargenomen.
Het bewijs van Peterson gaat ervan uit dat de vlaggen en beurt deze ordening volgen en niet in een andere volgorde zichtbaar worden.
Spinlock
Een spinlock is een vergrendeling voor wederzijdse uitsluiting waarbij aanroepende threads die de vergrendeling niet krijgen herhaaldelijk controleren of proberen te verkrijgen en daarbij CPU-tijd gebruiken.
Een korte bezetting op een andere draaiende kern kan actief wachten goedkoper maken dan blokkeren en wekken.

T

Teller per processorkern
Een teller per processorkern registreert wijzigingen op een plek die aan één processorkern is gekoppeld en vermindert zo het aantal schrijfbewerkingen naar één globaal gedeelde locatie.
Threads die verschillende lokale plekken gebruiken, kunnen onder verschillende mutexen wijzigen totdat een groepsgewijze overdracht nodig is.
Telleromslag
Telleromslag ontstaat wanneer een rekenkundige bewerking zonder teken de maximaal weergeefbare waarde passeert en vanaf nul doorgaat.
Ticketgelijkheid blijft bij telleromslag alleen bruikbaar zolang uitstaande ticketnummers uniek blijven.
Test-and-set
Test-and-set slaat atomair een nieuwe waarde op en geeft de oude waarde terug, zodat één aanroepende thread zonder tussenruimte een vrije vlag kan opeisen.
Een thread die bij een uitwisseling 0 ontvangt, is eigenaar van de vlag; concurrerende threads die 1 ontvangen blijven op vrijgave wachten.
Thread
Een thread is een uitvoeringsreeks binnen een proces, met een eigen programmateller, registers, stack en threadidentiteit.
Een server kan één thread gebruiken om op een antwoord te wachten terwijl een andere thread een voltooid verzoek verwerkt.
Threadargument
Een threadargument bestaat uit gegevens die aan een nieuwe werkthread worden geleverd via de pointerparameter van de startfunctie, vaak als verwijzing naar een struct.
Een array geeft elke werkthread een afzonderlijk argumentobject dat geldig blijft totdat de join-aanroepen zijn voltooid.
Threadbesturingsblok
Een threadbesturingsblok is een record van het besturingssysteem dat de opgeslagen uitvoeringstoestand van een thread bevat voor planning en hervatting.
Bij een wisseling tussen twee threads wordt de registertoestand van de eerste thread in zijn threadbesturingsblok opgeslagen voordat de toestand van de tweede wordt hersteld.
ThreadSanitizer
ThreadSanitizer is een runtimeanalysehulpmiddel dat dataraces in geïnstrumenteerde uitvoeringen detecteert en de conflicterende toegangen meldt.
Een testbuild met -fsanitize=thread kan een leespad blootleggen dat de mutex van de wijzigende code niet gebruikte.
Threadveiligheid
Threadveiligheid betekent dat een structuur zich volgens de specificatie gedraagt wanneer bewerkingen van meerdere threads worden vervlochten.
Een teller heeft een beschermingsregel nodig voor zowel leesbewerkingen als verhogingen wanneer die bewerkingen kunnen overlappen.
Ticketvergrendeling
Een ticketvergrendeling geeft elke aanvrager atomair een uniek ticket en laat die naar binnen wanneer de beurtteller dat ticket bereikt.
Een aanroepende thread met ticket 6 wacht terwijl de beurt 5 is, ook als een thread met een later ticket al draait.
Tweefasenvergrendeling
Bij een tweefasenvergrendeling wacht een thread eerst kort actief en blokkeert daarna als de eigenaar tijdens dat korte interval niet heeft vrijgegeven.
Kort actief wachten kan bij een snelle vrijgave de kosten van wekken voorkomen zonder bij lange bezetting een kern te verspillen.

U

Uithongering
Uithongering is onbepaalde vertraging van een thread terwijl concurrerende threads voortgang blijven maken en herhaaldelijk de benodigde bron verkrijgen.
Een wachtende thread bij test-and-set kan na elke ontgrendeling de race verliezen omdat de vergrendeling geen volgorde van binnenkomst onthoudt.

V

Veel-op-een-threadmodel
Een veel-op-een-threadmodel koppelt meerdere gebruikersthreads aan één kernelthread, waardoor ze niet op hetzelfde moment op verschillende kernen kunnen worden uitgevoerd.
Eén blokkerende systeemaanroep kan elke gebruikersthread laten vastlopen die aan dat ene uitvoeringspad in de kernel is gekoppeld.
Veilig vrijgeven van geheugen
Het veilig vrijgeven van geheugen geeft toegewezen opslag pas vrij nadat geen enkele bewerking het object in die opslag nog kan benaderen.
Een knoop ontkoppelen is onvoldoende wanneer een aanroepende thread de pointer heeft behouden en die later nog kan derefereren.
Vergrendelingsgranulariteit
Vergrendelingsgranulariteit is de hoeveelheid gedeelde toestand die één vergrendeling beschermt en die bepaalt welke bewerkingen na elkaar moeten worden uitgevoerd.
Door van één tabelmutex naar mutexen per bucket over te stappen, kunnen bewerkingen in verschillende buckets overlappen.
Vergrendelingsvrije atomaire bewerking
Een vergrendelingsvrije atomaire bewerking levert haar atomaire effect zonder afhankelijk te zijn van een andere thread die een interne vergrendeling vrijgeeft.
Andere atomaire objecten kunnen een bibliotheekvergrendeling gebruiken, dus atomaire correctheid alleen stelt vergrendelingsvrijheid niet vast.
Verloren weksignaal
Een verloren weksignaal ontstaat wanneer een benodigde melding een wachttijd niet voorkomt of beëindigt, waardoor een thread ten onrechte geblokkeerd kan blijven.
Een eenvoudige wachtende thread kan gaan slapen nadat een eerdere unpark is weggegooid, zonder dat er nog een vrijgave is om de thread te wekken.
Verloren wijziging
Een verloren wijziging ontstaat wanneer een thread een resultaat opslaat dat op een eerder gelezen waarde is gebaseerd en daarmee het effect van een concurrerende wijziging overschrijft.
Twee werkthreads kunnen elk 50 uit een gedeelde teller laden en 51 opslaan, zodat na twee pogingen slechts één verhoging zichtbaar is.
Vervlechting van uitvoeringsstappen
Een vervlechting van uitvoeringsstappen is een volgorde waarin stappen van verschillende threads plaatsvinden, met behoud van de relevante uitvoeringsvolgorde binnen elke thread.
Als de volledige wijziging van één thread tussen het laden en opslaan van een andere thread wordt geplaatst, zie je hoe een verouderd resultaat die wijziging kan overschrijven.
Volatile
De C-kwalificatie volatile markeert toegangen waarvoor bijzondere regels voor waarneembare toegang gelden; die biedt noch atomiciteit, noch synchronisatie tussen threads.
Een gedeelde teller als volatile markeren combineert het laden, optellen en opslaan niet tot één wijziging.
Volgorde van binnenkomst
De volgorde van binnenkomst behandelt verzoeken in hun vastgelegde volgorde van aankomst, zodat later vastgelegde aankomsten eerdere niet kunnen inhalen.
Bij een ticketvergrendeling wordt de aankomst vastgelegd wanneer de aanroepende thread atomair zijn ticketnummer verkrijgt.
Voortgang
Voortgang vereist dat threads die een vrije kritieke sectie willen betreden een thread kunnen kiezen zonder voor onbepaalde tijd te worden gehinderd door threads die niet naar binnen willen.
Een thread die niet naar binnen probeert te gaan, mag een verder beschikbare sectie niet voor altijd gesloten houden.

W

Wachten op threadbeëindiging
Wachten op threadbeëindiging met join wacht totdat een thread waarop met join kan worden gewacht eindigt en maakt de voltooide schrijfbewerkingen zichtbaar voor de thread die met succes heeft gewacht.
De hoofdthread wacht met join op twee werkthreads die een som berekenen voordat die hun afzonderlijke resultaatvelden leest en combineert.
Wachtrij met twee vergrendelingen
Een wachtrij met twee vergrendelingen voert toevoegingen na elkaar uit met een staartvergrendeling en verwijderingen met een kopvergrendeling, zodat bewerkingen aan tegenoverliggende uiteinden kunnen overlappen.
In een C11-wachtrij met twee vergrendelingen moet de next-koppeling van de dummyknoop bij een lege wachtrij nog steeds atomair worden gepubliceerd en gelezen.
Wederzijdse uitsluiting
Wederzijdse uitsluiting is de eigenschap dat hoogstens één concurrerende thread zich tegelijk in een beschermde kritieke sectie bevindt.
Een tweede thread kan de gedeelde mutex niet verkrijgen zolang een eerste thread eigenaar is, ook niet als die eigenaar tijdelijk wordt onderbroken.
Wijzigen van de tabelgrootte
Het wijzigen van de tabelgrootte verandert de bucketindeling van een hashtabel en deelt vermeldingen opnieuw in, wat meer coördinatie vereist dan de gewone vergrendeling van één bucket biedt.
Een eenvoudige hashtabel met bucketvergrendelingen sluit gelijktijdige tabelbewerkingen uit terwijl vermeldingen naar nieuwe buckets worden verplaatst.

Over deze les

Door StudeliFout melden

Maak je eigen Studiemateriaal

Maak van je aantekeningen, slides en documenten Leermodules, Oefenvragen, Flashcards en 6 andere tools.

Aan de slag