
Lad os se på, hvordan konkurrence- og parallelprogrammering fungerer i .Net, ved at tage udgangspunkt i problemet med de spisende filosoffer. Planen er at dække alt fra synkronisering af tråde/processer til aktørmodellen (i de følgende dele). Artiklen kan være nyttig til en første introduktion eller til at opfriske dine kundskaber.
Hvorfor skal man overhovedet kunne dette? Transistorer når deres mindste størrelse, Moores lov støder mod lysets hastighed, og derfor ses væksten i antallet; der kan laves flere transistorer. Samtidig vokser datamængden, og brugerne forventer øjeblikkelig respons fra systemerne. I denne situation er "normalt" programmering, hvor vi har én kørende tråd, ikke effektivt. Der skal findes en løsning på problemet med samtidighed eller konkurrencekørsel. Dette problem findes på forskellige niveauer: på trådniveau, procesniveau og maskinniveau i netværk (fordelte systemer). I .NET findes der kvalitetsmæssige, tidsafprøvede teknologier til hurtig og effektiv løsning af sådanne opgaver.
Regler for godkendelse af merge-requests
Edsger Dijkstra præsenterede dette problem for sine studerende allerede i 1965. Den etablerede formulering er som følger. Der er et vist antal (normalt fem) filosoffer og lige så mange gaffel. De sidder omkring et rundt bord, gaffelerne imellem dem. Filosoferne kan spise fra deres tallerkener med uendelig mad, tænke eller vente. For en filosof at spise, skal han tage to gafler (den sidste deler en gaffel med den første). At tage og lægge en gaffel er to separate handlinger. Alle filosoffer er stille. Opgaven er at finde en algoritme, så alle tænker og er mætte, selv efter 54 år.
Lad os først forsøge at løse denne opgave ved at bruge et delt rum. Gaflerne ligger på et fælles bord, og filosofferne tager dem, når de er der, og lægger dem tilbage. Her opstår der problemer med synkroniseringen: hvornår skal gaflerne tages? hvad skal der gøres, hvis der ikke er nogen gafler? osv. Men lad os først starte filosofferne.
Til at starte trådene bruger vi en trådpool via Task.Run metoden:
var cancelTokenSource = new CancellationTokenSource();
Action create = (i) => RunPhilosopher(i, cancelTokenSource.Token);
for (int i = 0; i create(icopy), cancelTokenSource.Token);
}Thread-pool er oprettet for at optimere oprettelse og sletning af tråde. Denne pulje har en opgavekø, og CLR opretter eller sletter tråde afhængigt af antallet af disse opgaver. Der er én pulje for alle AppDomain’er. Denne pulje bør næsten altid bruges, da man ikke behøver at bekymre sig om oprettelse, sletning af tråde, deres køer osv. Man kan godt undvære puljen, men så skal man bruge Tråd, hvilket er hensigtsmæssigt i situationer, hvor man skal ændre prioriteten af en tråd, når vi har en lang operation, for Foreground-tråden osv.
Med andre ord, System.Threading.Tasks.Task klassen — det er den samme Tråd, men med diverse nyttige funktioner: muligheden for at starte en opgave efter et blok af andre opgaver, returnere dem fra funktioner, nemt at annullere dem osv. De er nødvendige for at understøtte async/await konstruktioner (Task-baseret asynkron mønster, syntaktisk sukker til at vente på IO-operationer). Vi vil tale mere om dette senere.
CancelationTokenSource er nødt til her, så tråden kan afslutte sig selv ved signal fra den kaldende tråd.
Problemer med synkronisering
Blokerede filosofer
Godt, vi kan oprette tråde, lad os prøve at spise middag:
// Кто какие вилки взял. К примеру: 1 1 3 3 - 1й и 3й взяли первые две пары.
private int[] forks = Enumerable.Repeat(0, philosophersAmount).ToArray();
// То же, что RunPhilosopher()
private void RunDeadlock(int i, CancellationToken token)
{
// Ждать вилку, взять её. Эквивалентно:
// while(true)
// if forks[fork] == 0
// forks[fork] = i+1
// break
// Thread.Sleep() или Yield() или SpinWait()
void TakeFork(int fork) =>
SpinWait.SpinUntil(() =>
Interlocked.CompareExchange(ref forks[fork], i+1, 0) == 0);
// Для простоты, но можно с Interlocked.Exchange:
void PutFork(int fork) => forks[fork] = 0;
while (true)
{
TakeFork(Left(i));
TakeFork(Right(i));
eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
PutFork(Left(i));
PutFork(Right(i));
Think(i);
// Завершить работу по-хорошему.
token.ThrowIfCancellationRequested();
}
}Her forsøger vi først at tage den venstre gaffel, og derefter den højre, og hvis det lykkes, så spiser vi og lægger dem tilbage. At tage en gaffel er atomar, dvs. to tråde kan ikke tage den samme på samme tid (forkert: den første læser, at gaffelen er ledig, den anden også, den første tager, den anden tager). For dette Interlocked.CompareExchange, som skal implementeres ved hjælp af processorinstruktionen (TSL, XCHG), som låser et hukommelsesområde for atomare sekventielle læsninger og skrivninger. Og SpinWait svarer til konstruktionen while(true) , kun med lidt "magik" — tråden optager processoren (Thread.SpinWait), men nogle gange giver den kontrollen videre til en anden tråd (Thread.Yeild) eller sover (Thread.Sleep).
Men denne løsning fungerer ikke, da trådene snart (for mig inden for et sekund) låses: alle filosoffer tager deres venstre gaffel, men ikke den højre. Arrayet forks har så værdierne: 1 2 3 4 5.

På tegningen, låsning af tråde (deadlock). Grønt er udførelse, rødt er synkronisering, gråt er tråden sover. Romberne angiver tidspunktet for opstart af Task’erne.
Filosofernes sult
Selv om man ikke behøver meget mad for at tænke, kan sult få enhver til at opgive filosofien. Lad os prøve at simulere en situation med sultende tråde i vores opgave. Sult indebærer, at en tråd arbejder, men uden væsentligt arbejde; med andre ord er det det samme som deadlock, bare nu sover tråden ikke, men leder aktivt efter noget at spise, men der er ingen mad. For at undgå hyppig blokeringsproblematik vil vi lægge gaffelen tilbage, hvis vi ikke kan tage den anden.
// То же что и в RunDeadlock, но теперь кладем вилку назад и добавляем плохих философов.
private void RunStarvation(int i, CancellationToken token)
{
while (true)
{
bool hasTwoForks = false;
var waitTime = TimeSpan.FromMilliseconds(50);
// Плохой философов может уже иметь вилку:
bool hasLeft = forks[Left(i)] == i + 1;
if (hasLeft || TakeFork(Left(i), i + 1, waitTime))
{
if (TakeFork(Right(i), i + 1, TimeSpan.Zero))
hasTwoForks = true;
else
PutFork(Left(i)); // Иногда плохой философ отдает вилку назад.
}
if (!hasTwoForks)
{
if (token.IsCancellationRequested) break;
continue;
}
eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
bool goodPhilosopher = i % 2 == 0;
// А плохой философ забывает положить свою вилку обратно:
if (goodPhilosopher)
PutFork(Left(i));
// А если и правую не положит, то хорошие будут вообще без еды.
PutFork(Right(i));
Think(i);
if (token.IsCancellationRequested)
break;
}
}
// Теперь можно ждать определенное время.
bool TakeFork(int fork, int philosopher, TimeSpan? waitTime = null)
{
return SpinWait.SpinUntil(
() => Interlocked.CompareExchange(ref forks[fork], philosopher, 0) == 0,
waitTime ?? TimeSpan.FromMilliseconds(-1)
);
}I denne kode er det vigtigt, at to af de fire filosoffer glemmer at lægge deres venstre gaffel. Det resulterer i, at de spiser mere mad, mens de andre begynder at sulte, selvom trådene har samme prioritet. Her sulter de ikke helt, da de dårlige filosoffer nogle gange lægger deres gafler tilbage. Det viser sig, at de gode spiser cirka fem gange mindre end de dårlige. En lille fejl i koden fører til, at ydeevnen falder. Det er også værd at bemærke, at der kan opstå en sjælden situation, hvor alle filosoffer tager den venstre gaffel og ingen har den højre, hvorefter de lægger den venstre tilbage, venter, tager den venstre igen, osv. Denne situation er også sult, men mere lig den gensidige blokering. Jeg har ikke kunnet gentage den. Nedenfor er et billede af situationen, hvor to dårlige filosoffer har taget begge gafler, mens to gode sulter.

Her kan vi se, at trådene nogle gange vågner og forsøger at få ressourcer. To kerner ud af fire laver ingenting (den grønne graf øverst).
Filosoffens død
Og endnu et problem, der kan afbryde de filosofers gode middag, er, hvis en af dem pludselig dør med gaflerne i hånden (og begraves sådan). Så vil naboerne blive efterladt uden middag. Du kan selv finde på et eksempel på kode til denne situation, for eksempel kastes NullReferenceException efter at filosoffen tager gaflerne. Og for resten, vil undtagelsen ikke blive fanget, og den kaldende kode vil ikke fange den blot sådan (for dette AppDomain.CurrentDomain.UnhandledException osv.). Derfor er fejlhåndterere nødvendige i selve trådene og for korrekt afslutning.
Tjener
Hvordan kan vi løse dette problem med gensidig blokering, sult og død? Vi tillader kun én filosof ad gangen at bruge gaflerne, og vi tilføjer gensidig udelukkelse (mutual exclusion) for denne ressource. Hvordan gør vi det? Antag, at der står en tjener ved siden af filosofene, som giver tilladelse til kun én filosof til at tage gaflerne. Hvordan skaber vi denne tjener, og hvordan vil filosofene anmode om hans hjælp? Det er interessante spørgsmål.
Den simpleste løsning er, at filosofene bare konstant vil anmode tjeneren om adgang til gaflerne. Det vil sige, at filosofene ikke venter på gaflen ved siden af, men venter på eller beder tjeneren. I første omgang bruger vi kun User Space, hvor vi ikke anvender afbrydelser til at kalde nogen procedurer fra kernen (mere om det senere).
Løsninger i brugerens rum
Her vil vi gøre det samme som tidligere med én gaffel og to filosoffer; vi vil køre i en cyklus og vente. Men nu vil det være alle filosofene, og der vil kun være én gaffel, hvilket vil sige, at kun den filosof, der har taget denne "gyldne gaffel" fra tjeneren, kan spise. Til dette bruger vi SpinLock.
private static SpinLock spinLock = new SpinLock(); // Vores "tjener"
private void RunSpinLock(int i, CancellationToken token)
{
while (true)
{
// Gensidig blokering gennem busy waiting. Vi kalder før try, for at
// kaste en undtagelse i tilfælde af fejl i selve SpinLock.
bool hasLock = false;
spinLock.Enter(ref hasLock);
try
{
// Her kan kun én tråd være aktiv (mutual exclusion).
forks[Left(i)] = i + 1; // Tag gaflen med det samme, uden at vente.
forks[Right(i)] = i + 1;
eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
forks[Left(i)] = 0;
forks[Right(i)] = 0;
}
finally
{
if(hasLock) spinLock.Exit(); // Undgå problemer med philosopher døden.
}
Think(i);
if (token.IsCancellationRequested)
break;
}
}SpinLock det er en låsemekanisme med, groft sagt, det samme while(true) { if (!lock) break; }, men med endnu mere "magisk" end i SpinWait (som bruges der). Nu kan det tælle ventende, lidt berolige dem og meget mere. Kort sagt, det gør alt for at optimere. Men man må huske, at det stadig er den samme aktive cyklus, som bruger CPU-ressourcer og opretholder en strøm, som kan føre til sult, hvis en af filosoferne får højere prioritet end de andre, men ikke har en gylden gaffel (Priority Inversion problem). Derfor bruger vi det kun til meget, meget korte ændringer i den delte hukommelse, uden nogen sideopkald, indlejrede låse og andre overraskelser.

Illustration for SpinLock. Strømmene "kriger" konstant om den gyldne gaffel. Der opstår nedbrud - det markerede område på illustrationen. Kjernerne bruges ikke fuldt ud: kun omkring 2/3 af disse fire strømme.
En anden løsning her ville være kun at bruge Interlocked.CompareExchange med den samme aktive venten, som vist i koden ovenfor (i sultne filosofer), men det kan som nævnt teoretisk føre til blokering.
Om Interlocked det skal siges, at der ikke kun er CompareExchange, men også andre metoder til atomisk læsning og skrivning. Og gennem gentagne ændringer i tilfælde af, hvis en anden strøm når at foretage sine ændringer (læse 1, læse 2, skrive 2, skrive 1 er dårligt), kan den anvendes til komplekse ændringer af en enkelt værdi (Interlocked Anything mønster).
Løsninger i kernel-tilstand
For at undgå tab af ressourcer i cyklussen, lad os se, hvordan vi kan låse strømmen. Med andre ord, fortsætte vores eksempel, lad os se, hvordan tjeneren beroliger filosofen og vækker ham kun når det er nødvendigt. Lad os først se, hvordan dette kan gøres gennem operativsystemets kernel-tilstand. Alle strukturer der er ofte langsommere end dem i brugerens rum. Langsommere i flere gange, for eksempel AutoResetEvent kan være 53 gange langsommere SpinLock [Richter]. Men med dem kan man synkronisere processer på tværs af systemet, uanset om de er styrede eller ej.
Den grundlæggende konstruktion her er et semaphore, foreslået af Dijkstra for mere end et halvt århundrede siden. Et semaphore er, forenklet sagt, et positivt heltal, der administreres af systemet, og dets to operationer — at øge og mindske det. Hvis det ikke er muligt at mindske det til nul, blokeres den kaldende tråd. Når tallet øges af en anden aktiv tråd/process, bliver trådene givet adgang, og semaphore tælles igen ned med antallet af passerende. Man kan forestille sig tog i en smal passage med et semaphore. .NET tilbyder flere konstruktioner med lignende funktioner: AutoResetEvent, ManualResetEvent, Mutex og selve Semaphore. Vi vil bruge AutoResetEvent, dette er den simpleste af disse konstruktioner: kun to værdier 0 og 1 (false, true). Dens metode WaitOne() blokerer den kaldende tråd, hvis værdien var 0, mens den, hvis værdien er 1, sænker den til 0 og giver adgang. Metoden Set() øger den til 1 og giver adgang til én, der venter, som igen sænker den til 0. Det fungerer som en drejesnolog i metroen.
Lad os komplicere løsningen og bruge en lås for hver filosof, i stedet for for alle på én gang. Dvs. nu kan flere filosoffer være aktive samtidig, ikke bare én. Men vi blokerer igen adgangen til bordet for korrekt at tage gaflerne og undgå race conditions.
// Для блокирования отдельного философа.
// Инициализируется: new AutoResetEvent(true) для каждого.
private AutoResetEvent[] philosopherEvents;
// Для доступа к вилкам / доступ к столу.
private AutoResetEvent tableEvent = new AutoResetEvent(true);
// Рождение философа.
public void Run(int i, CancellationToken token)
{
while (true)
{
TakeForks(i); // Ждет вилки.
// Обед. Может быть и дольше.
eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
PutForks(i); // Отдать вилки и разблокировать соседей.
Think(i);
if (token.IsCancellationRequested) break;
}
}
// Ожидать вилки в блокировке.
void TakeForks(int i)
{
bool hasForks = false;
while (!hasForks) // Попробовать еще раз (блокировка не здесь).
{
// Исключающий доступ к столу, без гонок за вилками.
tableEvent.WaitOne();
if (forks[Left(i)] == 0 && forks[Right(i)] == 0)
forks[Left(i)] = forks[Right(i)] = i + 1;
hasForks = forks[Left(i)] == i + 1 && forks[Right(i)] == i + 1;
if (hasForks)
// Теперь философ поест, выйдет из цикла. Если Set
// вызван дважды, то значение true.
philosopherEvents[i].Set();
// Разблокировать одного ожидающего. После него значение tableEvent в false.
tableEvent.Set();
// Если имеет true, не блокируется, а если false, то будет ждать Set от соседа.
philosopherEvents[i].WaitOne();
}
}
// Отдать вилки и разблокировать соседей.
void PutForks(int i)
{
tableEvent.WaitOne(); // Без гонок за вилками.
forks[Left(i)] = 0;
// Пробудить левого, а потом и правого соседа, либо AutoResetEvent в true.
philosopherEvents[LeftPhilosopher(i)].Set();
forks[Right(i)] = 0;
philosopherEvents[RightPhilosopher(i)].Set();
tableEvent.Set();
}For at forstå, hvad der foregår her, lad os overveje et tilfælde, hvor filosofen ikke kunne tage gaflerne, så hans handlinger ville være sådanne. Han venter på adgang til bordet. Når han får den, prøver han at tage gaflerne. Det virkede ikke. Han giver adgang til bordet (gensidig undtagelse). Og passerer sin "drejesnolog" (AutoResetEvent) (de er åbne i starten). Han havner igen i cyklus, da han ikke har gaflerne. Han prøver at tage dem og stopper ved sin "drejesnolog". En mere heldig nabo til højre eller venstre, der er færdig med at spise, låser vores filosof op og "åbner hans drejesnolog". Vores filosof passerer den (og den lukker sig bag ham) for anden gang. Han prøver for tredje gang at tage gaflerne. Heldigt. Og han passerer sin drejesnolog for at spise frokost.
Når der i sådan en kode opstår tilfældige fejl (de er altid til stede), for eksempel hvis naboen er forkert angivet eller hvis den samme genstand er oprettet AutoResetEvent for alle (Enumerable.Repeat), så vil filosofferne vente på udviklerne, da det kan være en udfordring at finde fejl i sådan kode. Endnu et problem med denne løsning er, at den ikke garanterer, at nogen filosof ikke begynder at sulte.
Hybride løsninger
Vi har undersøgt to tilgange til synkronisering, når vi forbliver i brugermode og kører i en løkke, samt når vi blokerer tråden gennem kernen. Den første metode er god til korte blokeringer, den anden til længerevarende. Ofte er det nødvendigt først kort at vente på en ændring af variablen i løkken, og derefter at blokere tråden, når ventetiden er lang. Denne tilgang implementeres i såkaldte hybride konstruktioner. Her er der de samme konstruktioner som i kernemoden, men nu med en løkke i brugermode: SemaphoreSlim, ManualResetEventSlim og andre. Den mest populære konstruktion her er Monitor, da der i C# er en velkendt lock syntaksis. Monitor det er den samme semafor med et maksimalt værdig 1 (mutex), men med støtte til at vente i løkken, rekursion, Condition Variable mønsteret (mere om det senere) og andre. Lad os se på løsningen med det.
// Спрячем объект для Монитора от всех, чтобы без дедлоков.
private readonly object _lock = new object();
// Время ожидания потока.
private DateTime?[] _waitTimes = new DateTime?[philosophersAmount];
public void Run(int i, CancellationToken token)
{
while (true)
{
TakeForks(i);
eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
PutForks(i);
Think(i);
if (token.IsCancellationRequested) break;
}
}
// Наше сложное условие для Condition Variable паттерна.
bool CanIEat(int i)
{
// Если есть вилки:
if (forks[Left(i)] != 0 && forks[Right(i)] != 0)
return false;
var now = DateTime.Now;
// Может, если соседи не более голодные, чем текущий.
foreach(var p in new int[] {LeftPhilosopher(i), RightPhilosopher(i)})
if (_waitTimes[p] != null && now - _waitTimes[p] > now - _waitTimes[i])
return false;
return true;
}
void TakeForks(int i)
{
// Зайти в Монитор. То же самое: lock(_lock) {..}.
// Вызываем вне try, чтобы возможное исключение выбрасывалось выше.
bool lockTaken = false;
Monitor.Enter(_lock, ref lockTaken);
try
{
_waitTimes[i] = DateTime.Now;
// Condition Variable паттерн. Освобождаем лок, если не выполненно
// сложное условие. И ждем пока кто-нибудь сделает Pulse / PulseAll.
while (!CanIEat(i))
Monitor.Wait(_lock);
forks[Left(i)] = i + 1;
forks[Right(i)] = i + 1;
_waitTimes[i] = null;
}
finally
{
if (lockTaken) Monitor.Exit(_lock);
}
}
void PutForks(int i)
{
// То же самое: lock (_lock) {..}.
bool lockTaken = false;
Monitor.Enter(_lock, ref lockTaken);
try
{
forks[Left(i)] = 0;
forks[Right(i)] = 0;
// Освободить все потоки в очереди ПОСЛЕ вызова Monitor.Exit.
Monitor.PulseAll(_lock);
}
finally
{
if (lockTaken) Monitor.Exit(_lock);
}
}Her blokerer vi igen hele bordet for adgangen til gaflerne, men nu frigiver vi alle tråde på én gang, ikke kun naboerne, når nogen er færdig med at spise. Det vil sige, først spiser nogen og blokerer naboerne, men når denne person er færdig, men straks vil spise igen, går han i blokering og vækker sine naboer, da hans ventetid er kortere.
Sådan undgår vi deadlocks og sulten for en hvilken som helst filosof. Vi bruger en løkke til kort ventetid og blokerer tråden for lang ventetid. At frigive alle på én gang fungerer langsommere end hvis der kun blev frigivet naboen, som i løsningen med AutoResetEvent, men forskellen bør ikke være stor, da trådene først skal forblive i brugermode.
Ved lock syntaksen har ubehagelige overraskelser. Det anbefales at bruge Monitor direkte [Richter][Erik Lippert]. En af dem er, at lock aldrig forlader Monitor, selvom der opstod en undtagelse, og så kan en anden tråd ændre tilstanden for den delte hukommelse. I sådanne tilfælde er det ofte bedre at gå ind i en deadlock eller på en sikker måde afslutte programmet. En anden overraskelse er, at Monitor bruger synkroniseringsblokke (SyncBlock), som findes i alle objekter. Derfor, hvis der vælges et uhensigtsmæssigt objekt, kan man nemt få en deadlock (for eksempel, hvis man laver en lock på en internær streng). Vi bruger altid et skjult objekt til dette.
Condition Variable-mønsteret tillader en mere kompakt implementering af venting på en kompleks betingelse. I .NET er det ikke helt fuldendt, efter min mening, da der ideelt set skulle være flere køer baseret på flere variabler (som i Posix Threads) i stedet for én lås. Så kunne vi planlægge dem for alle filosoferne. Men i denne form muliggør det stadig at forkorte koden.
Mange filosoffer eller async / await
Godt, nu kan vi effektivt blokere tråde. Men hvad hvis vi har mange filosoffer? 100? 10000? For eksempel, vi modtager 100000 anmodninger til webserveren. At oprette en tråd til hver anmodning ville være belastende, da så mange tråde ikke vil kunne køre parallelt. Kun så mange som der er logiske kerner (jeg har 4). Og alle de andre vil bare tage ressourcer. En løsning på dette problem er async / await-mønsteret. Ideen er, at funktionen ikke holder tråden, hvis der er noget, den skal vente på, før den kan fortsætte. Når dette noget sker, genoptager den sin udførelse (men ikke nødvendigvis på den samme tråd!). I vores tilfælde skal vi vente på gaffelen.
SemaphoreSlim tilbyder dette WaitAsync() metode. Her er en implementering ved brug af dette mønster.
// Запуск такой же, как раньше. Где-нибудь в программе:
Task.Run(() => Run(i, cancelTokenSource.Token));
// Запуск философа.
// Ключевое слово async -- компилятор транслирует этот метот в асинхронный.
public async Task Run(int i, CancellationToken token)
{
while (true)
{
// await -- будем ожидать какого-то события.
await TakeForks(i);
// После await, продолжение возможно в другом потоке.
eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
// Может быть несколько событий для ожидания.
await PutForks(i);
Think(i);
if (token.IsCancellationRequested) break;
}
}
async Task TakeForks(int i)
{
bool hasForks = false;
while (!hasForks)
{
// Взаимоисключающий доступ к столу:
await _tableSemaphore.WaitAsync();
if (forks[Left(i)] == 0 && forks[Right(i)] == 0)
{
forks[Left(i)] = i+1;
forks[Right(i)] = i+1;
hasForks = true;
}
_tableSemaphore.Release();
// Будем ожидать, чтобы сосед положил вилки:
if (!hasForks)
await _philosopherSemaphores[i].WaitAsync();
}
}
// Ждем доступа к столу и кладем вилки.
async Task PutForks(int i)
{
await _tableSemaphore.WaitAsync();
forks[Left(i)] = 0;
// "Пробудить" соседей, если они "спали".
_philosopherSemaphores[LeftPhilosopher(i)].Release();
forks[Right(i)] = 0;
_philosopherSemaphores[RightPhilosopher(i)].Release();
_tableSemaphore.Release();
}Metoden med async / await oversættes til en smart endelig automat, der hurtigt returnerer sin interne Task. Gennem den kan man vente på afslutningen af metoden, afbryde den, og alt andet man kan gøre med Task. Inden i metoden kontrollerer den endelige automat udførelsen. Essensen er, at hvis der ikke er nogen forsinkelse, er udførelsen synkron, og hvis der er, frigives tråden. For bedre forståelse af dette er det bedre at se på denne endelige automat. Man kan lave kæder af disse async / await metoder.
Lad os teste. Kørsel af 100 filosoffer på en maskine med 4 logiske kerner, 8 sekunder. Den tidligere løsning med Monitor udførte kun de første 4 tråde, mens de andre slet ikke kørte. Hver af disse 4 tråde var inaktiv i cirka 2 ms. Og løsningen med async / await udførte alle 100, hvoraf hver i gennemsnit ventede 6,8 sekunder. Selvfølgelig er det ikke acceptabelt med en inaktivitet på 6 sekunder i reelle systemer, og det er bedre ikke at behandle så mange anmodninger sådan. Løsningen med Monitor viste sig at være helt uholdbar.
Konklusion
Som det fremgår af disse små eksempler, understøtter .NET mange synkroniseringskonstruktioner. Det er dog ikke altid indlysende, hvordan man bruger dem. Jeg håber, denne artikel har været nyttig. Vi slutter her for nu, men der er stadig meget interessant at komme ind på, såsom tråd-sikre samlinger, TPL Dataflow, reaktiv programmering, software-transaktionsmodellen mv.
Kilder
- Visualisering af tråde:
- MSDN: , og mange andre.
- [Richter] — CLR via C#, Jeffrey Richter
- [Eric Lippert] —
- Billede — "Dansen blandt sværd", H. Semiradsky
Kilde: habr.com
