Välmatade filosofer eller konkurrenskraftig programmering i .NET

Välmatade filosofer eller konkurrenskraftig programmering i .NET

Låt oss titta på hur samtidig och parallell programmering fungerar i .Net, med hjälp av exemplet med lunchfilosofernas problem. Planen är följande, från tråd/processsynkronisering till aktörsmodellen (i följande delar). Artikeln kan vara användbar för en första bekantskap eller för att fräscha upp dina kunskaper.

Varför ens veta hur man gör detta? Transistorer når sin minimistorlek, Moores lag når gränsen för ljusets hastighet, och därför observeras tillväxt i antal, fler transistorer kan tillverkas. Samtidigt växer mängden data och användarna förväntar sig omedelbar respons från systemen. I en sådan situation är "normal" programmering, när vi har en exekverande tråd, inte längre effektiv. Vi måste på något sätt lösa problemet med samtidig eller samtidig utförande. Dessutom finns detta problem på olika nivåer: på trådnivå, på processnivå, på nivån för maskiner i nätverket (distribuerade system). .NET har högkvalitativa, beprövade tekniker för att snabbt och effektivt lösa sådana problem.

Uppgift

Edsger Dijkstra ställde detta problem till sina elever redan 1965. Den etablerade formuleringen är följande. Det finns ett visst (vanligtvis fem) antal filosofer och samma antal gafflar. De sitter vid ett runt bord och kliver mellan sig. Filosofer kan äta från sina tallrikar med oändlig mat, tänka eller vänta. För att äta måste en filosof ta två gafflar (den senare delar en gaffel med den förra). Att plocka upp och sätta ner en gaffel är två separata åtgärder. Alla filosofer är tysta. Uppgiften är att hitta en sådan algoritm så att de alla tänker och är välmatade även efter 54 år.

Låt oss först försöka lösa det här problemet genom att använda delat utrymme. Gafflarna ligger på det gemensamma bordet och filosoferna tar dem helt enkelt när de är där och lägger tillbaka dem. Det är här problem med synkronisering uppstår, när exakt ska man ta gafflar? vad ska man göra om det inte finns någon kontakt? etc. Men låt oss först börja med filosoferna.

För att starta trådar använder vi en trådpool via Task.Run metod:

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 är designad för att optimera skapandet och avlägsnandet av trådar. Denna pool har en kö av uppgifter och CLR skapar eller tar bort trådar beroende på antalet av dessa uppgifter. En pool för alla AppDomains. Denna pool bör användas nästan alltid, eftersom... du behöver inte bry dig om att skapa och ta bort trådar, deras köer etc. Du kan göra det utan pool, men då måste du använda det direkt Thread, detta är användbart för fall då vi behöver ändra prioritet för en tråd, när vi har en lång operation, för en förgrundstråd, etc.

Med andra ord, System.Threading.Tasks.Task klass är densamma Thread, men med alla möjliga bekvämligheter: möjligheten att starta en uppgift efter ett block av andra uppgifter, returnera dem från funktioner, bekvämt avbryta dem och mycket mer. etc. De behövs för att stödja async/wait-konstruktioner (Task-based Asynchronous Pattern, syntaktisk socker för att vänta på IO-operationer). Vi ska prata om detta senare.

CancelationTokenSource här är det nödvändigt att tråden kan avsluta sig själv på en signal från den anropande tråden.

Synkroniseringsproblem

Blockerade filosofer

Okej, vi vet hur man skapar trådar, låt oss prova lunch:

// Кто какие вилки взял. К примеру: 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();
    }
}

Här försöker vi först ta vänster och sedan höger gaffel, och om det fungerar äter vi och lägger tillbaka dem. Att ta en gaffel är atomärt, d.v.s. två trådar kan inte ta en samtidigt (fel: den första läser att gaffeln är ledig, den andra gör detsamma, den första tar, den andra tar). För detta Interlocked.CompareExchange, som måste implementeras med hjälp av en processorinstruktion (TSL, XCHG), som låser en bit minne för atomär sekventiell läsning och skrivning. Och SpinWait motsvarar konstruktionen while(true) bara med lite "magi" - tråden tar upp processorn (Thread.SpinWait), men ibland överför kontrollen till en annan tråd (Thread.Yeild) eller somnar (Thread.Sleep).

Men den här lösningen fungerar inte, eftersom... trådarna snart (inom en sekund för mig) blockeras: alla filosofer tar sin vänstra gaffel, men det finns ingen höger. Gaffelarrayen har då värdena: 1 2 3 4 5.

Välmatade filosofer eller konkurrenskraftig programmering i .NET

På bilden, blockerande trådar (deadlock). Grönt indikerar exekvering, rött indikerar synkronisering och grått indikerar att tråden sover. Diamanter indikerar starttiden för Tasks.

Filosofernas hunger

Även om du inte behöver mycket mat för att tänka, kan hunger tvinga vem som helst att ge upp filosofin. Låt oss försöka simulera situationen med trådsvält i vårt problem. Svält är när en tråd fungerar, men utan nämnvärt arbete, med andra ord, det är samma dödläge, bara nu sover tråden inte, utan letar aktivt efter något att äta, men det finns ingen mat. För att undvika frekvent blockering kommer vi att lägga tillbaka gaffeln om vi inte kunde ta en till.

// То же что и в 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 som är viktigt med den här koden är att två av fyra filosofer glömmer att lägga ner sin vänstra gaffel. Och det visar sig att de äter mer mat, och andra börjar svälta, även om trådarna har samma prioritet. Här svälter de inte helt, för... dåliga filosofer lägger ibland tillbaka gafflarna. Det visar sig att de goda äter cirka 5 gånger mindre än de dåliga. Så ett litet fel i koden leder till att prestanda sjunker. Här är det också värt att notera att en sällsynt situation är möjlig när alla filosofer tar den vänstra gaffeln, det finns ingen höger, de lägger ner den vänstra, väntar, tar den vänstra igen, etc. Denna situation är också svält, mer som en ömsesidig blockering. Jag kunde inte upprepa det. Nedan är en bild för en situation där två dåliga filosofer har tagit båda gafflarna, och två goda svälter.

Välmatade filosofer eller konkurrenskraftig programmering i .NET

Här kan man se att trådar ibland vaknar och försöker få en resurs. Två av fyra kärnor gör ingenting (grön graf ovan).

En filosofs död

Nåväl, ytterligare ett problem som skulle kunna avbryta filosofernas härliga middag är om en av dem plötsligt dör med gafflar i händerna (och han kommer att begravas på det sättet). Då blir grannarna utan lunch. Du kan själv komma på en exempelkod för detta fall, till exempel slängs den NullReferenceException efter att filosofen tar gafflarna. Och förresten, undantaget kommer inte att hanteras och anropskoden kommer inte bara att fånga det (för detta AppDomain.CurrentDomain.UnhandledException och så vidare.). Därför behövs felhanterare i själva trådarna och med graciös avslutning.

servitör

Okej, hur löser vi detta problem med dödlägen, svält och dödsfall? Vi kommer att tillåta endast en filosof till gafflarna, och vi kommer att lägga till ömsesidig uteslutning av trådar för denna plats. Hur man gör det? Anta att bredvid filosoferna finns en servitör som ger tillstånd till en filosof att ta gafflarna. Hur vi ska göra denna servitör och hur filosofer kommer att ställa honom är intressanta frågor.

Det enklaste sättet är för filosofer att helt enkelt ständigt be servitören om tillgång till gafflarna. De där. Nu kommer filosofer inte att vänta på en gaffel i närheten, utan vänta eller fråga servitören. Först använder vi bara User Space för detta; i det använder vi inte avbrott för att anropa några procedurer från kärnan (mer om dem nedan).

Användarutrymmeslösningar

Här ska vi göra samma sak som vi gjorde tidigare med en gaffel och två filosofer, vi ska snurra i en slinga och vänta. Men nu blir det alla filosofer och liksom bara en gaffel, d.v.s. vi kan säga att bara filosofen som tog denna "gyllene gaffel" från servitören kommer att äta. För att göra detta använder 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 detta är en blockerare, med ungefär samma sak while(true) { if (!lock) break; }, men med ännu mer "magi" än i SpinWait (som används där). Nu vet han hur han ska räkna de som väntar, söva dem lite och mycket mer. etc. I allmänhet gör den allt för att optimera. Men vi måste komma ihåg att detta fortfarande är samma aktiva loop som äter upp processorresurser och håller en tråd, vilket kan leda till svält om en av filosoferna blir mer prioriterad än de andra, men inte har en gyllene gaffel (Priority Inversion problem ). Därför använder vi det bara för mycket mycket korta ändringar i delat minne, utan några tredjepartssamtal, kapslade lås eller andra överraskningar.

Välmatade filosofer eller konkurrenskraftig programmering i .NET

Ritning för SpinLock. Strömmar "kämpar" ständigt om den gyllene gaffeln. Fel uppstår - det markerade området i figuren. Kärnorna är inte helt använda: endast cirka 2/3 av dessa fyra trådar.

En annan lösning här skulle vara att endast använda Interlocked.CompareExchange med samma aktiva väntan som visas i koden ovan (hos svältande filosofer), men detta kan, som redan sagt, teoretiskt leda till blockering.

Про Interlocked det är värt att säga att det inte bara finns CompareExchange, men även andra metoder för atomär läsning OCH skrivning. Och genom att upprepa ändringen, om en annan tråd lyckas göra sina ändringar (läs 1, läs 2, skriv 2, skriv 1 är dåligt), kan den användas för komplexa ändringar av ett värde (Interlocked Anything-mönster).

Kärnlägeslösningar

För att undvika att slösa resurser i en loop, låt oss titta på hur man blockerar en tråd. Med andra ord, för att fortsätta vårt exempel, låt oss se hur servitören sövar filosofen och väcker honom endast när det behövs. Låt oss först titta på hur man gör detta genom operativsystemets kärnläge. Alla strukturer där blir ofta långsammare än de i användarutrymmet. Långsammare flera gånger, till exempel AutoResetEvent kanske 53 gånger långsammare SpinLock [Richter]. Men med deras hjälp kan du synkronisera processer över hela systemet, hanterade eller inte.

Den grundläggande designen här är en semafor, föreslog av Dijkstra för mer än ett halvt sekel sedan. En semafor är, enkelt uttryckt, ett positivt heltal som kontrolleras av systemet, och två operationer på det - öka och minska. Om det inte är möjligt att reducera noll, blockeras anropstråden. När siffran ökas med någon annan aktiv tråd/process, passerar trådarna och semaforen minskas igen med antalet passerade. Du kan föreställa dig tåg i en flaskhals med semafor. .NET erbjuder flera konstruktioner med liknande funktionalitet: AutoResetEvent, ManualResetEvent, Mutex och mig själv Semaphore. Vi kommer använda AutoResetEvent, detta är den enklaste av dessa konstruktioner: endast två värden 0 och 1 (falskt, sant). Hennes metod WaitOne() blockerar den anropande tråden om värdet var 0, och om 1, sänks den till 0 och hoppar över den. En metod Set() ökar till 1 och släpper igenom en person, som återigen minskar till 0. Fungerar som ett vändkors i tunnelbanan.

Låt oss komplicera lösningen och använda blockering för varje filosof, och inte för alla på en gång. De där. Nu kan flera filosofer äta samtidigt, och inte bara en. Men vi blockerar återigen åtkomsten till bordet för att ta gafflar korrekt och undvika tävlingsförhållanden.

// Для блокирования отдельного философа.
// Инициализируется: 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();
}

För att förstå vad som händer här, överväg fallet när filosofen misslyckades med att ta gafflarna, då kommer hans handlingar att vara som följer. Han väntar på tillgång till bordet. Efter att ha fått den försöker han ta gafflarna. Det funkade inte. Det ger bort tillgång till tabellen (ömsesidig uteslutning). Och han passerar sin "vändkors" (AutoResetEvent) (först är de öppna). Det faller in i cykeln igen, eftersom han har inga gafflar. Han försöker ta dem och stannar vid sin "vändkors". Någon lyckligare granne till höger eller vänster, efter att ha ätit klart, kommer att låsa upp vår filosof genom att "öppna sitt vändkors." Vår filosof går igenom det (och det sluter sig bakom honom) en andra gång. Försöker för tredje gången att ta gafflarna. Framgångsrik. Och han går igenom sin vändkors för att äta lunch.

När det finns slumpmässiga fel i sådan kod (de finns alltid), till exempel kommer en granne att anges felaktigt eller samma objekt skapas AutoResetEvent för alla (Enumerable.Repeat), då kommer filosoferna att vänta på utvecklarna, eftersom Att hitta fel i sådan kod är en ganska svår uppgift. Ett annat problem med denna lösning är att den inte garanterar att någon filosof inte kommer att svälta.

Hybridlösningar

Vi tittade på två tillvägagångssätt för synkronisering, när vi stannar i användarläge och snurrar i en loop och när vi blockerar tråden genom kärnan. Den första metoden är bra för korta block, den andra för långa. Ofta behöver du först vänta kort på att en variabel ska ändras i en loop, och sedan blockera tråden när väntan är lång. Detta tillvägagångssätt implementeras i den sk. hybriddesigner. Den har samma konstruktioner som för kärnläge, men nu med en loop i användarläge: SemaphorSlim, ManualResetEventSlim etc. Den mest populära designen här är Monitor, därför att i C# finns en välkänd lock syntax. Monitor detta är samma semafor med ett maximalt värde på 1 (mutex), men med stöd för att vänta i en loop, rekursion, Condition Variable pattern (mer om det nedan), etc. Låt oss titta 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);
    }
}

Här blockerar vi återigen hela bordet från att komma åt gafflarna, men nu låser vi upp alla trådar på en gång, snarare än grannar när någon äter färdigt. De där. först, någon äter och blockerar grannarna, och när denne slutar, men vill äta igen direkt, går han in i kvarteret och väcker sina grannar, eftersom dess väntetid är kortare.

På så sätt undviker vi dödlägen och en filosofs svält. Vi använder en slinga för att vänta en kort stund och blockera tråden länge. Att avblockera alla på en gång går långsammare än om bara grannen var avblockerad, som i lösningen med AutoResetEvent, men skillnaden bör inte vara stor, eftersom trådar måste vara i användarläge först.

У lock syntax har några obehagliga överraskningar. Rekommenderas att använda Monitor direkt [Richter] [Eric Lippert]. En av dem är det lock kommer alltid ut Monitor, även om det fanns ett undantag, och då kan en annan tråd ändra tillståndet för det delade minnet. I sådana fall är det ofta bättre att gå in i ett dödläge eller på något sätt säkert avsluta programmet. En annan överraskning är att Monitor använder klockblock (SyncBlock), som finns i alla objekt. Därför, om ett olämpligt objekt väljs, kan du enkelt få ett dödläge (till exempel om du låser på en intern sträng). Vi använder alltid ett dolt föremål för detta.

Konditionsvariabelmönstret låter dig mer koncist implementera förväntan på något komplext tillstånd. I .NET är det ofullständigt, enligt min mening, eftersom... I teorin bör det finnas flera köer på flera variabler (som i Posix Threads), och inte på ett lås. Då skulle det vara möjligt att göra dem för alla filosofer. Men även i denna form låter den dig förkorta koden.

Många filosofer eller async / await

Okej, nu kan vi effektivt blockera trådar. Men tänk om vi har många filosofer? 100? 10000 100000? Till exempel fick vi 4 XNUMX förfrågningar till webbservern. Att skapa en tråd för varje förfrågan kommer att bli dyrt, eftersom så många trådar kommer inte att köras parallellt. Endast så många logiska kärnor kommer att exekveras (jag har XNUMX). Och alla andra kommer helt enkelt att ta bort resurser. En lösning på detta problem är det asynkrona / vänta-mönstret. Dess idé är att en funktion inte håller en tråd om den behöver vänta på att något ska fortsätta. Och när något händer återupptar det sitt exekvering (men inte nödvändigtvis i samma tråd!). I vårt fall kommer vi att vänta på en gaffel.

SemaphoreSlim har för detta WaitAsync() metod. Här är en implementering som använder detta 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();
}

Metod med async / await översätts till en listig finita tillståndsmaskin, som omedelbart returnerar sin inre Task. Genom det kan du vänta på att metoden ska slutföras, avbryta den och allt annat du kan göra med Task. Inuti metoden styr en tillståndsmaskin exekveringen. Summan av kardemumman är att om det inte finns någon fördröjning, så är exekveringen synkron, och om det finns, släpps tråden. För en bättre förståelse av detta är det bättre att titta på denna tillståndsmaskin. Du kan skapa kedjor från dessa async / await metoder.

Låt oss testa det. Arbete av 100 filosofer på en maskin med 4 logiska kärnor, 8 sekunder. Den tidigare lösningen med Monitor körde bara de första 4 trådarna och körde inte resten alls. Var och en av dessa 4 trådar var inaktiva i cirka 2ms. Och lösningen async/wait klarade alla 100, med ett genomsnitt på 6.8 sekunder varje väntan. Naturligtvis, i verkliga system, är det oacceptabelt att vara inaktiv i 6 sekunder och det är bättre att inte behandla så många förfrågningar på detta sätt. Lösningen med Monitor visade sig inte alls vara skalbar.

Slutsats

Som du kan se från dessa små exempel stöder .NET många synkroniseringskonstruktioner. Det är dock inte alltid självklart hur man använder dem. Jag hoppas att den här artikeln var till hjälp. Vi avslutar det här för tillfället, men det finns fortfarande mycket intressant kvar, till exempel trådsäkra samlingar, TPL Dataflow, Reaktiv programmering, Software Transaction-modell, etc.

källor

Källa: will.com

Lägg en kommentar