Velnærede filosoffer eller konkurrencedygtig .NET-programmering

Velnærede filosoffer eller konkurrencedygtig .NET-programmering

Lad os se, hvordan samtidig og parallel programmering fungerer i .Net, ved at bruge Philosophers Dining Problem som eksempel. Planen er denne, fra synkronisering af tråde/processer, til aktørmodellen (i de følgende dele). Artiklen kan være nyttig for det første bekendtskab eller for at genopfriske din viden.

Hvorfor overhovedet gøre det? Transistorer når deres minimumsstørrelse, Moores lov hviler på begrænsningen af ​​lysets hastighed og derfor observeres en stigning i antallet, flere transistorer kan laves. Samtidig vokser mængden af ​​data, og brugerne forventer et øjeblikkeligt svar fra systemerne. I en sådan situation er "normal" programmering, når vi har én eksekverende tråd, ikke længere effektiv. Du skal på en eller anden måde løse problemet med samtidig eller samtidig udførelse. Desuden eksisterer dette problem på forskellige niveauer: på niveauet af tråde, på niveauet af processer, på niveauet af maskiner i netværket (distribuerede systemer). .NET har højkvalitets, gennemtestede teknologier til hurtig og effektiv løsning af sådanne problemer.

Opgave

Edsger Dijkstra stillede dette problem til sine elever allerede i 1965. Den etablerede formulering er som følger. Der er et vist (normalt fem) antal filosoffer og det samme antal gafler. De sidder ved et rundt bord, gafler sig imellem dem. Filosoffer kan spise fra deres tallerkener med endeløs mad, tænke eller vente. For at spise en filosof skal du tage to gafler (den sidste deler gaflen med den første). At tage en gaffel op og ned er to separate handlinger. Alle filosoffer tier. Opgaven er at finde sådan en algoritme, som alle ville tænke og være fyldte selv efter 54 år.

Lad os først prøve at løse dette problem ved at bruge et delt rum. Gaflerne ligger på fællesbordet, og filosofferne tager dem simpelthen, når de er, og lægger dem tilbage. Her er der problemer med synkronisering, hvornår skal man præcis tage sure bets? hvad hvis der ikke er nogen gaffel? osv. Men lad os først begynde med filosofferne.

For at starte tråde bruger vi en trådpulje igennem Task.Run metode:

var cancelTokenSource = new CancellationTokenSource();
Action<int> create = (i) => RunPhilosopher(i, cancelTokenSource.Token);
for (int i = 0; i < philosophersAmount; i++) 
{
    int icopy = i;
    // Поместить задачу в очередь пула потоков. Метод RunDeadlock не запускаеться 
    // сразу, а ждет своего потока. Асинхронный запуск.
    philosophers[i] = Task.Run(() => create(icopy), cancelTokenSource.Token);
}

Trådpuljen er designet til at optimere trådoprettelse og sletning. Denne pulje har en kø med opgaver, og CLR'en opretter eller fjerner tråde afhængigt af antallet af disse opgaver. Én pulje til alle AppDomains. Denne pool bør bruges næsten altid, fordi. ingen grund til at bøvle med at oprette, slette tråde, deres køer osv. Det er muligt uden en pulje, men så skal du bruge den direkte Thread, dette er nyttigt i tilfælde, hvor du skal ændre en tråds prioritet, når vi har en lang operation, for en forgrundstråd osv.

Med andre ord, System.Threading.Tasks.Task klasse er den samme Thread, men med alle mulige bekvemmeligheder: evnen til at køre en opgave efter en blok af andre opgaver, returnere dem fra funktioner, bekvemt afbryde dem og mere. osv. De er nødvendige for at understøtte asynkrone / afvente-konstruktioner (opgavebaseret asynkront mønster, syntaktisk sukker til at vente på IO-operationer). Vi taler om dette senere.

CancelationTokenSource her er det nødvendigt for at tråden kan afslutte sig selv ved signalet fra den kaldende tråd.

Synkroniseringsproblemer

Blokerede filosoffer

Okay, vi ved, hvordan man opretter tråde, lad os prøve at spise frokost:

// Кто какие вилки взял. К примеру: 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 venstre gaffel, og derefter højre gaffel, og hvis det lykkes, så spiser vi og sætter dem tilbage. At tage én gaffel er atomart, dvs. to tråde kan ikke tage en på samme tid (forkert: den første læser, at gaflen er fri, den anden - også den første tager, den anden tager). For det Interlocked.CompareExchange, som skal implementeres med en processorinstruktion (TSL, XCHG), som låser et stykke hukommelse til atomart sekventiel læsning og skrivning. Og SpinWait svarer til konstruktionen while(true) kun med lidt "magi" - tråden tager processoren (Thread.SpinWait), men nogle gange overfører kontrollen til en anden tråd (Thread.Yeild) eller falder i søvn (Thread.Sleep).

Men denne løsning virker ikke, pga strømmene er snart (for mig inden for et sekund) blokeret: alle filosoffer tager deres venstre gaffel, men ikke den højre. Fork-arrayet har så værdierne: 1 2 3 4 5.

Velnærede filosoffer eller konkurrencedygtig .NET-programmering

På figuren blokerende tråde (deadlock). Grøn - udførelse, rød - synkronisering, grå - tråden sover. Rhombuserne angiver starttidspunktet for Opgaver.

Filosoffernes sult

Selvom det ikke er nødvendigt at tænke særlig meget mad, men sult får enhver til at opgive filosofi. Lad os prøve at simulere situationen med udsultning af tråde i vores problem. Sult er, når en tråd løber, men uden væsentligt arbejde, med andre ord er det samme dødvande, kun nu sover tråden ikke, men søger aktivt efter noget at spise, men der er ingen mad. For at undgå hyppig blokering, sætter vi gaflen tilbage, hvis vi ikke kunne tage en 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)
    );
}

Det vigtige ved denne kode er, at to ud af fire filosoffer glemmer at lægge deres venstre gaffel. Og det viser sig, at de spiser mere mad, mens andre begynder at sulte, selvom trådene har samme prioritet. Her sulter de ikke helt, pga. dårlige filosoffer lægger nogle gange deres gafler tilbage. Det viser sig, at gode mennesker spiser omkring 5 gange mindre end dårlige. Så en lille fejl i koden fører til et fald i ydeevnen. Det er også værd at bemærke her, at en sjælden situation er mulig, når alle filosoffer tager den venstre gaffel, der er ingen højre, de sætter den venstre, venter, tager den venstre igen osv. Denne situation er også en sult, mere som et dødvande. Jeg undlod at gentage det. Nedenfor er et billede for en situation, hvor to dårlige filosoffer har taget begge gafler, og to gode sulter.

Velnærede filosoffer eller konkurrencedygtig .NET-programmering

Her kan du se, at trådene nogle gange vågner op og forsøger at få ressourcen. To af de fire kerner gør ingenting (grøn graf ovenfor).

En filosofs død

Nå, et andet problem, der kan afbryde en herlig middag af filosoffer, er, hvis en af ​​dem pludselig dør med gafler i hænderne (og de vil begrave ham sådan). Så vil naboerne stå uden frokost. Du kan selv komme med en eksempelkode til denne sag, for eksempel bliver den smidt ud NullReferenceException efter at filosoffen tager gaflerne. Og forresten vil undtagelsen ikke blive håndteret, og opkaldskoden vil ikke bare fange den (for dette AppDomain.CurrentDomain.UnhandledException og osv.). Derfor er der brug for fejlbehandlere i selve trådene og med yndefuld afslutning.

Oficiant

Okay, hvordan løser vi dette dødvande-, sult- og dødsproblem? Vi vil tillade kun én filosof at nå gaflerne, tilføje en gensidig udelukkelse af tråde til dette sted. Hvordan gør man det? Antag, at der står en tjener ved siden af ​​filosofferne, som giver tilladelse til en hvilken som helst filosof til at tage gaflerne. Hvordan laver vi denne tjener, og hvordan filosoffer vil stille ham, spørgsmålene er interessante.

Den enkleste måde er, når filosofferne simpelthen konstant vil bede tjeneren om adgang til gaflerne. De der. nu vil filosoffer ikke vente på en gaffel i nærheden, men vente eller spørge tjeneren. I første omgang bruger vi kun User Space til dette, i det bruger vi ikke interrupts til at kalde nogen procedurer fra kernen (om dem nedenfor).

Løsninger i brugerrummet

Her vil vi gøre det samme, som vi plejede at gøre med en gaffel og to filosoffer, vi vil spinde i en cyklus og vente. Men nu bliver det alle filosoffer og sådan set kun én gaffel, dvs. det kan siges, at kun den filosof, der tog denne "gyldne gaffel" fra tjeneren, vil spise. Til dette bruger vi SpinLock.

private static SpinLock spinLock = new SpinLock();  // Наш "официант"
private void RunSpinLock(int i, CancellationToken token)
{
    while (true)
    {
        // Взаимная блокировка через busy waiting. Вызываем до try, чтобы
        // выбрасить исключение в случае ошибки в самом SpinLock.
        bool hasLock = false;
        spinLock.Enter(ref hasLock);
        try
        {
            // Здесь может быть только один поток (mutual exclusion).
            forks[Left(i)] = i + 1;  // Берем вилку сразу, без ожидания.
            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();  // Избегаем проблемы со смертью философа.
        }

        Think(i);

        if (token.IsCancellationRequested)
            break;
    }
}

SpinLock dette er en blokerer, med, groft sagt, det samme while(true) { if (!lock) break; }, men med endnu mere "magi" end i SpinWait (som bruges der). Nu ved han, hvordan man tæller dem, der venter, får dem til at sove lidt og mere. osv. Gør generelt alt for at optimere. Men vi skal huske, at dette stadig er den samme aktive cyklus, som æder processorressourcer og holder flowet, hvilket kan føre til sult, hvis en af ​​filosofferne bliver mere prioriteret end andre, men ikke har en gylden gaffel (Priority Inversion problem) . Derfor bruger vi det kun til meget meget korte ændringer i delt hukommelse, uden nogen tredjepartsopkald, indlejrede låse og andre overraskelser.

Velnærede filosoffer eller konkurrencedygtig .NET-programmering

Tegning til SpinLock. Vandløbene "kæmper" konstant om den gyldne gaffel. Der er fejl - i figuren, det valgte område. Kernerne udnyttes ikke fuldt ud: kun omkring 2/3 af disse fire tråde.

En anden løsning her ville være kun at bruge Interlocked.CompareExchange med den samme aktive ventetid som vist i koden ovenfor (i de udsultede filosoffer), men dette kan som allerede sagt teoretisk føre til blokering.

Про Interlocked Det skal bemærkes, at der ikke kun er CompareExchange, men også andre metoder til atomlæsning OG skrivning. Og gennem ændringsgentagelse, hvis en anden tråd har tid til at foretage sine ændringer (læs 1, læs 2, skriv 2, skriv 1 er dårlig), kan den bruges til komplekse ændringer af en enkelt værdi (Interlocked Anything-mønster).

Kernel Mode Solutions

For at undgå at spilde ressourcer i en løkke, lad os se, hvordan vi kan blokere en tråd. Med andre ord, fortsætter vores eksempel, lad os se, hvordan tjeneren får filosoffen til at sove og kun vækker ham, når det er nødvendigt. Lad os først se på, hvordan man gør dette gennem operativsystemets kernetilstand. Alle strukturer der er ofte langsommere end dem i brugerrummet. Flere gange langsommere f.eks AutoResetEvent måske 53 gange langsommere SpinLock [Richter]. Men med deres hjælp kan du synkronisere processer i hele systemet, administreret eller ej.

Den grundlæggende konstruktion her er semaforen foreslået af Dijkstra for over et halvt århundrede siden. En semafor er, forenklet sagt, et positivt heltal, der styres af systemet, og to operationer på det, inkrement og decrement. Hvis den ikke falder, nul, så er den kaldende tråd blokeret. Når tallet øges med en anden aktiv tråd/proces, springes trådene over, og semaforen formindskes igen med det passerede antal. Man kan forestille sig tog i en flaskehals med en semafor. .NET tilbyder flere konstruktioner med lignende funktionalitet: AutoResetEvent, ManualResetEvent, Mutex og mig selv Semaphore. Vi vil bruge AutoResetEvent, dette er den enkleste af disse konstruktioner: kun to værdier 0 og 1 (falsk, sand). Hendes metode WaitOne() blokerer den kaldende tråd, hvis værdien var 0, og hvis 1, sænker den til 0 og springer den over. En metode Set() hæver til 1 og lader en tjener komme igennem, som igen sænker til 0. Fungerer som en tæller.

Lad os komplicere løsningen og bruge låsen til hver filosof, og ikke for alle på én gang. De der. nu kan der være flere filosoffer på én gang, og ikke én. Men vi blokerer igen adgangen til bordet for korrekt, at undgå løb (løbsbetingelser), tage sure bets.

// Для блокирования отдельного философа.
// Инициализируется: 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 sker her, overvej sagen, da filosoffen undlod at tage gaflerne, så vil hans handlinger være som følger. Han venter på adgang til bordet. Efter at have modtaget den, prøver han at tage gaflerne. Det lykkedes ikke. Det giver adgang til bordet (gensidig udelukkelse). Og passerer sin "turnstile" (AutoResetEvent) (de er oprindeligt åbne). Det kommer ind i kredsløbet igen, fordi han har ingen gafler. Han forsøger at tage dem og stopper ved sin "turnstile". En mere heldig nabo til højre eller venstre, efter at have spist færdig, låser vores filosof op og "åbner sit tællekors." Vores filosof passerer den (og den lukker sig bag den) for anden gang. Han forsøger for tredje gang at tage gaflerne. Held og lykke. Og han rækker sin vendekors for at spise.

Når der er tilfældige fejl i en sådan kode (de findes altid), for eksempel er en nabo forkert angivet, eller det samme objekt er oprettet AutoResetEvent for alle (Enumerable.Repeat), så vil filosofferne vente på udviklerne, fordi At finde fejl i en sådan kode er en ret vanskelig opgave. Et andet problem med denne løsning er, at den ikke garanterer, at en eller anden filosof ikke går sulten.

Hybridløsninger

Vi har set på to tilgange til timing, når vi forbliver i brugertilstand og loop, og når vi blokerer tråd gennem kernen. Den første metode er god til korte låse, den anden til lange. Det er ofte nødvendigt først kort at vente på, at en variabel ændres i en loop, og derefter blokere tråden, når ventetiden er lang. Denne tilgang er implementeret i den såkaldte. hybride strukturer. Her er de samme konstruktioner som for kernetilstand, men nu med en brugertilstandsløkke: SemaphorSlim, ManualResetEventSlim osv. Det mest populære design her er Monitor, fordi i C# er der en velkendt lock syntaks. Monitor dette er den samme semafor med en maksimal værdi på 1 (mutex), men med understøttelse af at vente i en loop, rekursion, Condition Variable-mønsteret (mere om det nedenfor) osv. Lad os se på en løsning 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 spærrer vi igen hele bordet for adgang til gaflerne, men nu spærrer vi alle tråde op på én gang, og ikke naboer, når nogen er færdige med at spise. De der. først er der nogen, der spiser og blokerer naboerne, og når denne er færdig, men vil spise igen med det samme, går han i spærring og vækker sine naboer, pga. dens ventetid er kortere.

Sådan undgår vi dødvande og en filosofs sult. Vi bruger en løkke til en kort ventetid og blokerer tråden i lang tid. Ophævelse af blokering af alle på én gang er langsommere, end hvis kun naboen blev frigjort, som i løsningen med AutoResetEvent, men forskellen bør ikke være stor, fordi tråde skal forblive i brugertilstand først.

У lock syntaks har ubehagelige overraskelser. Anbefales at bruge Monitor direkte [Richter] [Eric Lippert]. En af dem er det lock altid ude af Monitor, selvom der var en undtagelse, i hvilket tilfælde en anden tråd kunne ændre den delte hukommelsestilstand. I sådanne tilfælde er det ofte bedre at gå i dødvande eller på en eller anden måde sikkert afslutte programmet. En anden overraskelse er, at Monitor bruger synkroniseringsblokke (SyncBlock), som er til stede i alle objekter. Hvis et upassende objekt vælges, kan du derfor nemt få en dødvande (f.eks. hvis du låser på en intern streng). Vi bruger det altid skjulte objekt til dette.

Condition Variable-mønsteret giver dig mulighed for mere præcist at implementere forventningen om en kompleks tilstand. I .NET er det ufuldstændigt, efter min mening, fordi i teorien skal der være flere køer på flere variable (som i Posix Threads), og ikke på én lok. Så kunne man lave dem til alle filosoffer. Men selv i denne form giver det dig mulighed for at reducere koden.

mange filosoffer el async / await

Okay, nu kan vi effektivt blokere tråde. Men hvad nu hvis vi har mange filosoffer? 100? 10000? For eksempel modtog vi 100000 anmodninger til webserveren. Det vil være overhead at oprette en tråd for hver anmodning, fordi så mange tråde vil ikke køre parallelt. Vil kun køre så mange, som der er logiske kerner (jeg har 4). Og alle andre vil bare tage ressourcer væk. En løsning på dette problem er asynkron/afvent-mønsteret. Dens idé er, at funktionen ikke holder tråden, hvis den skal vente på, at noget fortsætter. Og når den gør noget, genoptager den sin udførelse (men ikke nødvendigvis på samme tråd!). I vores tilfælde vil vi vente på gaflen.

SemaphoreSlim har til dette WaitAsync() metode. Her er en implementering, der bruger 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();
}

Metode med async / await er oversat til en vanskelig tilstandsmaskine, der straks returnerer sin interne Task. Gennem den kan du vente på færdiggørelsen af ​​metoden, annullere den og alt andet, du kan gøre med Task. Inde i metoden styrer statsmaskinen udførelsen. Den nederste linje er, at hvis der ikke er nogen forsinkelse, så er udførelsen synkron, og hvis der er, så frigives tråden. For en bedre forståelse af dette er det bedre at se på denne tilstandsmaskine. Du kan lave kæder af disse async / await metoder.

Lad os teste. Arbejde af 100 filosoffer på en maskine med 4 logiske kerner, 8 sekunder. Den tidligere løsning med Monitor kørte kun de første 4 tråde og resten kørte slet ikke. Hver af disse 4 tråde var inaktive i omkring 2ms. Og async/wait-løsningen kørte alle 100, med en gennemsnitlig ventetid på 6.8 sekunder hver. Selvfølgelig er tomgang i 6 sekunder i rigtige systemer uacceptabelt, og det er bedre ikke at behandle så mange anmodninger som denne. Løsningen med Monitor viste sig slet ikke at være skalerbar.

Konklusion

Som du kan se fra disse små eksempler, understøtter .NET mange synkroniseringskonstruktioner. Det er dog ikke altid indlysende, hvordan man bruger dem. Jeg håber, at denne artikel var nyttig. For nu er det slut, men der er stadig en masse interessante ting tilbage, for eksempel trådsikre samlinger, TPL Dataflow, Reaktiv programmering, Software Transaction model osv.

kilder

Kilde: www.habr.com

Tilføj en kommentar