Velfødde filosofer eller konkurransedyktig .NET-programmering

Velfødde filosofer eller konkurransedyktig .NET-programmering

La oss se hvordan samtidig og parallell programmering fungerer i .Net, med Philosophers Dining Problem som eksempel. Planen er dette, fra synkronisering av tråder/prosesser, til aktørmodellen (i de følgende delene). Artikkelen kan være nyttig for det første bekjentskapet eller for å friske opp kunnskapen din.

Hvorfor gjøre det i det hele tatt? Transistorer når sin minimumsstørrelse, Moores lov hviler på begrensning av lyshastigheten og derfor observeres en økning i antallet, flere transistorer kan lages. Samtidig vokser datamengden, og brukerne forventer en umiddelbar respons fra systemene. I en slik situasjon er "normal" programmering, når vi har én utførende tråd, ikke lenger effektiv. Du må på en eller annen måte løse problemet med samtidig eller samtidig utførelse. Dessuten eksisterer dette problemet på forskjellige nivåer: på trådnivå, på prosessnivå, på maskinnivå i nettverket (distribuerte systemer). .NET har høykvalitets, tidstestede teknologier for raskt og effektivt å løse slike problemer.

Oppgave

Edsger Dijkstra stilte dette problemet til studentene sine allerede i 1965. Den etablerte formuleringen er som følger. Det er et visst (vanligvis fem) antall filosofer og det samme antall gafler. De sitter ved et rundt bord, deler seg mellom dem. Filosofer kan spise fra tallerkenene sine med endeløs mat, tenke eller vente. For å spise en filosof, må du ta to gafler (den siste deler gaffelen med den første). Å plukke opp og sette ned en gaffel er to separate handlinger. Alle filosofer tier. Oppgaven er å finne en slik algoritme at alle vil tenke og være fulle selv etter 54 år.

Først, la oss prøve å løse dette problemet ved å bruke en delt plass. Gaflene ligger på fellesbordet og filosofene tar dem rett og slett når de er og legger dem tilbake. Her er det problemer med synkronisering, når skal man ta surebets? hva om det ikke er noen gaffel? osv. Men først, la oss starte filosofene.

For å starte tråder bruker vi en trådpool gjennom 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ådpoolen er designet for å optimalisere trådoppretting og sletting. Dette bassenget har en kø med oppgaver og CLR oppretter eller fjerner tråder avhengig av antall disse oppgavene. Ett basseng for alle AppDomains. Dette bassenget bør brukes nesten alltid, fordi. du trenger ikke bry deg med å lage, slette tråder, deres køer osv. Det er mulig uten en pool, men da må du bruke den direkte Thread, dette er nyttig for tilfeller der du trenger å endre prioritet til en tråd, når vi har en lang operasjon, for en forgrunnstråd osv.

Med andre ord, System.Threading.Tasks.Task klasse er den samme Thread, men med alle slags bekvemmeligheter: muligheten til å kjøre en oppgave etter en blokk med andre oppgaver, returnere dem fra funksjoner, enkelt avbryte dem og mer. osv. De er nødvendige for å støtte asynkrone / avvente-konstruksjoner (Oppgavebasert asynkront mønster, syntaktisk sukker for å vente på IO-operasjoner). Vi snakker om dette senere.

CancelationTokenSource her trengs det slik at tråden kan avslutte seg selv ved signalet fra den kallende tråden.

Synkroniseringsproblemer

Blokkerte filosofer

Ok, vi vet hvordan vi lager tråder, la oss prøve å spise lunsj:

// Кто какие вилки взял. К примеру: 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 prøver vi først å ta venstre gaffel, og deretter høyre gaffel, og hvis det går, så spiser vi og legger dem tilbake. Å ta en gaffel er atomært, dvs. to tråder kan ikke ta en samtidig (feil: den første leser at gaffelen er fri, den andre - også den første tar, den andre tar). For dette Interlocked.CompareExchange, som må implementeres med en prosessorinstruksjon (TSL, XCHG), som låser et stykke minne for atomisk sekvensiell lesing og skriving. Og SpinWait tilsvarer konstruksjonen while(true) bare med litt "magi" - tråden tar prosessoren (Thread.SpinWait), men noen ganger overfører kontrollen til en annen tråd (Thread.Yeild) eller sovner (Thread.Sleep).

Men denne løsningen fungerer ikke, pga strømmene er snart (for meg innen et sekund) blokkert: alle filosofer tar sin venstre gaffel, men ikke den høyre. Fork-arrayet har da verdiene: 1 2 3 4 5.

Velfødde filosofer eller konkurransedyktig .NET-programmering

På figuren, blokkerende tråder (stopp). Grønn - utførelse, rød - synkronisering, grå - tråden sover. Rombene indikerer starttidspunktet for Oppgaver.

Filosofenes sult

Selv om det ikke er nødvendig å tenke spesielt mye mat, men sult får noen til å gi opp filosofi. La oss prøve å simulere situasjonen med sulting av tråder i problemet vårt. Sult er når en tråd løper, men uten nevneverdig arbeid, med andre ord, dette er den samme fastlåsen, bare nå sover ikke tråden, men leter aktivt etter noe å spise, men det er ingen mat. For å unngå hyppig blokkering, setter vi gaffelen tilbake hvis vi ikke kunne ta en til.

// То же что и в 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 viktige med denne koden er at to av fire filosofer glemmer å legge fra seg venstre gaffel. Og det viser seg at de spiser mer mat, mens andre begynner å sulte, selv om trådene har samme prioritet. Her sulter de ikke helt, pga. dårlige filosofer legger noen ganger gaflene tilbake. Det viser seg at gode mennesker spiser omtrent 5 ganger mindre enn dårlige. Så en liten feil i koden fører til et fall i ytelse. Det er også verdt å merke seg her at en sjelden situasjon er mulig når alle filosofer tar venstre gaffel, det er ingen høyre, de legger til venstre, venter, tar til venstre igjen, etc. Denne situasjonen er også en sult, mer som en fastlåst situasjon. Jeg klarte ikke å gjenta det. Nedenfor er et bilde for en situasjon der to dårlige filosofer har tatt begge gaflene og to gode sulter.

Velfødde filosofer eller konkurransedyktig .NET-programmering

Her kan du se at trådene våkner av og til og prøver å få tak i ressursen. To av de fire kjernene gjør ingenting (grønn graf over).

En filosofs død

Vel, et annet problem som kan avbryte en herlig middag med filosofer er hvis en av dem plutselig dør med gafler i hendene (og de vil begrave ham slik). Da blir naboene stående uten lunsj. Du kan selv komme med en eksempelkode for denne saken, for eksempel blir den kastet ut NullReferenceException etter at filosofen tar gaflene. Og forresten, unntaket vil ikke bli håndtert, og ringekoden vil ikke bare fange det (for dette AppDomain.CurrentDomain.UnhandledException og så videre.). Derfor trengs feilbehandlere i selve trådene og med grasiøs avslutning.

servitør

Ok, hvordan løser vi dette vranglås-, sult- og dødsproblemet? Vi vil tillate bare en filosof å nå gaflene, legge til en gjensidig utelukkelse av tråder for dette stedet. Hvordan gjøre det? Anta at det står en kelner ved siden av filosofene, som gir tillatelse til en hvilken som helst filosof til å ta gaflene. Hvordan lager vi denne servitøren og hvordan filosofer vil stille ham, spørsmålene er interessante.

Den enkleste måten er når filosofene rett og slett hele tiden vil be servitøren om tilgang til gaflene. De. nå vil ikke filosofer vente på en gaffel i nærheten, men vente eller spørre servitøren. Til å begynne med bruker vi bare brukerplass til dette, i det bruker vi ikke avbrudd for å kalle noen prosedyrer fra kjernen (om dem nedenfor).

Løsninger i brukerrommet

Her skal vi gjøre det samme som vi pleide å gjøre med en gaffel og to filosofer, vi skal spinne i en syklus og vente. Men nå blir det alle filosofer og så å si bare én gaffel, d.v.s. det kan sies at bare filosofen som tok denne "gylne gaffelen" fra servitøren vil spise. Til dette bruker 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 blokker, med grovt sett det samme while(true) { if (!lock) break; }, men med enda mer "magi" enn i SpinWait (som brukes der). Nå vet han hvordan han skal telle de som venter, få dem til å sove litt og mer. etc. Gjør generelt alt for å optimalisere. Men vi må huske at dette fortsatt er den samme aktive syklusen som spiser opp prosessorressurser og holder flyten, noe som kan føre til sult hvis en av filosofene blir mer prioritert enn andre, men ikke har en gyllen gaffel (Priority Inversion problem) . Derfor bruker vi det bare for veldig, veldig korte endringer i delt minne, uten tredjepartsoppkall, nestede låser og andre overraskelser.

Velfødde filosofer eller konkurransedyktig .NET-programmering

Tegning for SpinLock. Bekkene «kjemper» hele tiden om gullgaffelen. Det er feil - i figuren, det valgte området. Kjernene er ikke fullt utnyttet: bare ca. 2/3 av disse fire trådene.

En annen løsning her ville være å bare bruke Interlocked.CompareExchange med den samme aktive ventetiden som vist i koden ovenfor (i de sultende filosofene), men dette kan, som allerede sagt, teoretisk sett føre til blokkering.

Про Interlocked Det skal bemerkes at det ikke bare er CompareExchange, men også andre metoder for atomlesing OG skriving. Og gjennom gjentakelse av endring, i tilfelle en annen tråd har tid til å gjøre endringer (les 1, les 2, skriv 2, skriv 1 er dårlig), kan den brukes til komplekse endringer i en enkelt verdi (Interlocked Anything-mønster).

Løsninger for kjernemodus

For å unngå å kaste bort ressurser i en løkke, la oss se hvordan vi kan blokkere en tråd. Med andre ord, for å fortsette vårt eksempel, la oss se hvordan servitøren får filosofen til å sove og vekker ham bare når det er nødvendig. La oss først se på hvordan du gjør dette gjennom kjernemodusen til operativsystemet. Alle strukturer der er ofte tregere enn de i brukerområdet. Flere ganger tregere, for eksempel AutoResetEvent kanskje 53 ganger langsommere SpinLock [Richter]. Men med deres hjelp kan du synkronisere prosesser i hele systemet, administrert eller ikke.

Den grunnleggende konstruksjonen her er semaforen foreslått av Dijkstra for over et halvt århundre siden. En semafor er, enkelt sagt, et positivt heltall administrert av systemet, og to operasjoner på det, inkrement og dekrement. Hvis den ikke reduseres, null, blokkeres anropstråden. Når tallet økes med en annen aktiv tråd/prosess, hoppes trådene over og semaforen reduseres igjen med antallet passerte. Man kan tenke seg tog i en flaskehals med semafor. .NET tilbyr flere konstruksjoner med lignende funksjonalitet: AutoResetEvent, ManualResetEvent, Mutex og meg selv Semaphore. Vi vil bruke AutoResetEvent, dette er den enkleste av disse konstruksjonene: bare to verdier 0 og 1 (false, sann). Hennes metode WaitOne() blokkerer den kallende tråden hvis verdien var 0, og hvis 1, senker den til 0 og hopper over den. En metode Set() hever til 1 og slipper en kelner gjennom, som igjen senker til 0. Fungerer som en tunnelbane.

La oss komplisere løsningen og bruke låsen for hver filosof, og ikke for alle på en gang. De. nå kan det være flere filosofer samtidig, og ikke én. Men vi blokkerer igjen tilgangen til bordet for på riktig måte å unngå løp (løpsforhold), ta 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 å forstå hva som skjer her, vurder saken da filosofen ikke klarte å ta gaflene, så vil handlingene hans være som følger. Han venter på tilgang til bordet. Etter å ha mottatt den prøver han å ta gaflene. Det gikk ikke. Det gir tilgang til tabellen (gjensidig utestenging). Og passerer sin "turstile" (AutoResetEvent) (de er i utgangspunktet åpne). Det kommer inn i syklusen igjen, fordi han har ingen gafler. Han prøver å ta dem og stopper ved sin "turnstile". En eller annen heldig nabo til høyre eller venstre, etter å ha spist ferdig, låser opp filosofen vår og "åpner svingkorken". Vår filosof passerer den (og den lukker seg bak den) for andre gang. Han prøver for tredje gang å ta gaflene. Lykke til. Og han passerer snurren for å spise.

Når det er tilfeldige feil i en slik kode (de finnes alltid), for eksempel er en nabo feil spesifisert eller det samme objektet opprettes AutoResetEvent for alle (Enumerable.Repeat), så vil filosofene vente på utviklerne, fordi Å finne feil i slik kode er en ganske vanskelig oppgave. Et annet problem med denne løsningen er at den ikke garanterer at noen filosofer ikke går sultne.

Hybridløsninger

Vi har sett på to tilnærminger til timing, når vi holder oss i brukermodus og loop, og når vi blokkerer tråden gjennom kjernen. Den første metoden er bra for korte låser, den andre for lange. Det er ofte nødvendig å først kort vente på at en variabel endres i en loop, og deretter blokkere tråden når ventetiden er lang. Denne tilnærmingen er implementert i den såkalte. hybride strukturer. Her er de samme konstruksjonene som for kjernemodus, men nå med en brukermodusløkke: SemaphorSlim, ManualResetEventSlim etc. Det mest populære designet her er Monitor, fordi i C# er det en velkjent lock syntaks. Monitor dette er den samme semaforen med en maksimal verdi på 1 (mutex), men med støtte for å vente i en loop, rekursjon, Condition Variable-mønsteret (mer om det nedenfor), etc. La oss se på en løsning med den.

// Спрячем объект для Монитора от всех, чтобы без дедлоков.
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 sperrer vi igjen hele bordet for tilgang til gaflene, men nå sperrer vi opp alle tråder på en gang, og ikke naboer når noen er ferdige med å spise. De. først er det noen som spiser og blokkerer naboene, og når denne er ferdig, men vil spise igjen med en gang, går han inn i blokkering og vekker naboene, pga. ventetiden er kortere.

Slik unngår vi fastlåste situasjoner og sulten til en filosof. Vi bruker en løkke for en kort ventetid og blokkerer tråden for en lang. Å fjerne blokkeringen av alle på en gang går tregere enn om bare naboen var opphevet, som i løsningen med AutoResetEvent, men forskjellen bør ikke være stor, fordi tråder må være i brukermodus først.

У lock syntaks har ubehagelige overraskelser. Anbefales å bruke Monitor direkte [Richter] [Eric Lippert]. En av dem er det lock alltid ute av Monitor, selv om det var et unntak, i så fall kan en annen tråd endre tilstanden for delt minne. I slike tilfeller er det ofte bedre å gå i vranglås eller på en eller annen måte trygt avslutte programmet. En annen overraskelse er at Monitor bruker synkroniseringsblokker (SyncBlock), som finnes i alle objekter. Derfor, hvis et upassende objekt velges, kan du enkelt få en vranglås (for eksempel hvis du låser på en internert streng). Vi bruker det alltid skjulte objektet til dette.

Condition Variable-mønsteret lar deg mer konsist implementere forventningen om en kompleks tilstand. I .NET er det ufullstendig, etter min mening, fordi i teorien skal det være flere køer på flere variabler (som i Posix Threads), og ikke på en lok. Da kunne man lage dem for alle filosofer. Men selv i denne formen lar den deg redusere koden.

mange filosofer eller async / await

Ok, nå kan vi effektivt blokkere tråder. Men hva om vi har mange filosofer? 100? 10000 100000? For eksempel mottok vi 4 XNUMX forespørsler til webserveren. Det vil være overhead å opprette en tråd for hver forespørsel, fordi så mange tråder vil ikke gå parallelt. Vil bare kjøre så mange som det er logiske kjerner (jeg har XNUMX). Og alle andre vil bare ta fra seg ressurser. En løsning på dette problemet er asynkron/avvent-mønsteret. Ideen er at funksjonen ikke holder tråden hvis den må vente på at noe skal fortsette. Og når den gjør noe, gjenopptar den utførelsen (men ikke nødvendigvis på samme tråd!). I vårt tilfelle vil vi vente på gaffelen.

SemaphoreSlim har for dette WaitAsync() metode. Her er en implementering som bruker dette mønsteret.

// Запуск такой же, как раньше. Где-нибудь в программе:
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 oversatt til en vanskelig tilstandsmaskin som umiddelbart returnerer sin interne Task. Gjennom den kan du vente på fullføringen av metoden, kansellere den og alt annet du kan gjøre med Task. Inne i metoden styrer statsmaskinen utførelsen. Poenget er at hvis det ikke er noen forsinkelse, er utførelsen synkron, og hvis det er det, frigjøres tråden. For en bedre forståelse av dette er det bedre å se på denne tilstandsmaskinen. Du kan lage kjeder fra disse async / await metoder.

La oss teste. Arbeid av 100 filosofer på en maskin med 4 logiske kjerner, 8 sekunder. Den forrige løsningen med Monitor kjørte bare de første 4 trådene og resten kjørte ikke i det hele tatt. Hver av disse 4 trådene var inaktive i ca. 2ms. Og async/wait-løsningen kjørte alle 100, med en gjennomsnittlig ventetid på 6.8 sekunder hver. Selvfølgelig, i virkelige systemer, er tomgang i 6 sekunder uakseptabelt, og det er bedre å ikke behandle så mange forespørsler som dette. Løsningen med Monitor viste seg ikke å være skalerbar i det hele tatt.

Konklusjon

Som du kan se fra disse små eksemplene, støtter .NET mange synkroniseringskonstruksjoner. Det er imidlertid ikke alltid åpenbart hvordan de skal brukes. Jeg håper denne artikkelen var nyttig. Foreløpig er dette slutten, men det er fortsatt mye interessant igjen, for eksempel trådsikre samlinger, TPL Dataflow, Reaktiv programmering, Software Transaction modell, etc.

kilder

Kilde: www.habr.com

Legg til en kommentar