Goed gevoede filosofen of competitieve .NET-programmering

Goed gevoede filosofen of competitieve .NET-programmering

Laten we eens kijken hoe gelijktijdig en parallel programmeren werkt in .Net, waarbij we het Philosophers Dining Problem als voorbeeld gebruiken. Het plan is dit, van de synchronisatie van threads/processen, tot het actormodel (in de volgende delen). Het artikel kan nuttig zijn voor de eerste kennismaking of om uw kennis op te frissen.

Waarom zou je het überhaupt doen? Transistors bereiken hun minimale grootte, de wet van Moore berust op de beperking van de lichtsnelheid en daarom wordt een toename in het aantal waargenomen, er kunnen meer transistors worden gemaakt. Tegelijkertijd groeit de hoeveelheid data en verwachten gebruikers direct reactie van de systemen. In een dergelijke situatie is "normaal" programmeren, wanneer we één uitvoerende thread hebben, niet langer effectief. U moet op de een of andere manier het probleem van gelijktijdige of gelijktijdige uitvoering oplossen. Bovendien bestaat dit probleem op verschillende niveaus: op het niveau van threads, op het niveau van processen, op het niveau van machines in het netwerk (gedistribueerde systemen). .NET beschikt over hoogwaardige, beproefde technologieën om dergelijke problemen snel en efficiënt op te lossen.

Taak

Edsger Dijkstra legde dit probleem al in 1965 voor aan zijn studenten. De gangbare formulering is als volgt. Er is een bepaald (meestal vijf) aantal filosofen en hetzelfde aantal vorken. Ze zitten aan een ronde tafel, vorken tussen hen in. Filosofen kunnen van hun bord met eindeloos eten eten, nadenken of wachten. Om een ​​filosoof te eten, moet je twee vorken nemen (de laatste deelt de vork met de eerste). Een vork oppakken en neerleggen zijn twee afzonderlijke handelingen. Alle filosofen zwijgen. De taak is om zo'n algoritme te vinden dat ze allemaal zouden denken en zelfs na 54 jaar vol zouden zijn.

Laten we eerst proberen dit probleem op te lossen door een gedeelde ruimte te gebruiken. De vorken liggen op de gemeenschappelijke tafel en de filosofen pakken ze gewoon als ze liggen en leggen ze weer terug. Hier zijn er problemen met synchronisatie, wanneer precies surebets nemen? wat als er geen vork is? enz. Maar laten we eerst beginnen met de filosofen.

Om threads te starten, gebruiken we een thread pool through Task.Run methode:

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);
}

De threadpool is ontworpen om het maken en verwijderen van threads te optimaliseren. Deze pool heeft een wachtrij met taken en de CLR maakt of verwijdert threads afhankelijk van het aantal van deze taken. Eén pool voor alle AppDomains. Dit zwembad moet bijna altijd worden gebruikt, omdat. u hoeft zich geen zorgen te maken over het maken, verwijderen van threads, hun wachtrijen, enz. Het is mogelijk zonder een pool, maar dan moet u deze direct gebruiken Thread, dit is handig voor gevallen waarin u de prioriteit van een thread moet wijzigen, wanneer we een lange operatie hebben, voor een voorgrondthread, enz.

Met andere woorden, System.Threading.Tasks.Task klasse is hetzelfde Thread, maar met allerlei gemakken: de mogelijkheid om een ​​taak uit te voeren na een blok met andere taken, ze terug te halen uit functies, ze gemakkelijk te onderbreken en meer. enz. Ze zijn nodig om asynchrone / wachtende constructies te ondersteunen (taakgebaseerd asynchroon patroon, syntactische suiker voor wachten op IO-bewerkingen). We zullen hier later over praten.

CancelationTokenSource hier is het nodig zodat de thread zichzelf kan beëindigen op het signaal van de oproepende thread.

Synchronisatieproblemen

Geblokkeerde filosofen

Oké, we weten hoe we threads moeten maken, laten we proberen te lunchen:

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

Hier proberen we eerst de linkervork te pakken, en dan de rechtervork, en als het lukt, dan eten we en leggen ze terug. Het nemen van één vork is atomair, d.w.z. twee threads kunnen er niet tegelijkertijd één nemen (onjuist: de eerste leest dat de vork vrij is, de tweede ook, de eerste neemt, de tweede neemt). Voor deze Interlocked.CompareExchange, die moet worden geïmplementeerd met een processorinstructie (TSL, XCHG), die een stukje geheugen vergrendelt voor atomair sequentieel lezen en schrijven. En SpinWait is gelijk aan de constructie while(true) alleen met een beetje "magie" - de thread neemt de processor (Thread.SpinWait), maar draagt ​​soms de controle over aan een andere thread (Thread.Yeild) of valt in slaap (Thread.Sleep).

Maar deze oplossing werkt niet, omdat de stromen zijn al snel (voor mij binnen een seconde) geblokkeerd: alle filosofen nemen hun linkervork, maar niet de rechter. De forks-array heeft dan de waarden: 1 2 3 4 5.

Goed gevoede filosofen of competitieve .NET-programmering

In de figuur, threads blokkeren (deadlock). Groen - uitvoering, rood - synchronisatie, grijs - de draad slaapt. De ruiten geven de starttijd van Taken aan.

De honger van de filosofen

Hoewel het niet nodig is om vooral veel aan eten te denken, zorgt honger ervoor dat iedereen de filosofie opgeeft. Laten we proberen de situatie van uithongering van threads in ons probleem te simuleren. Uithongering is wanneer een draad loopt, maar zonder noemenswaardig werk, met andere woorden, dit is dezelfde impasse, alleen nu slaapt de draad niet, maar is actief op zoek naar iets om te eten, maar er is geen eten. Om frequente blokkering te voorkomen, plaatsen we de vork terug als we geen andere kunnen nemen.

// То же что и в 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)
    );
}

Het belangrijkste aan deze code is dat twee op de vier filosofen vergeten hun linkervork neer te leggen. En het blijkt dat ze meer voedsel eten, terwijl anderen beginnen te verhongeren, hoewel de draden dezelfde prioriteit hebben. Hier verhongeren ze niet helemaal, want. slechte filosofen zetten hun vorken soms terug. Het blijkt dat goede mensen ongeveer 5 keer minder eten dan slechte. Dus een kleine fout in de code leidt tot een daling van de prestaties. Het is ook vermeldenswaard dat een zeldzame situatie mogelijk is wanneer alle filosofen de linkervork nemen, er is geen rechter, ze zetten de linker, wachten, nemen weer de linker, enz. Deze situatie is ook een uithongering, meer een impasse. Ik heb het niet herhaald. Hieronder is een foto voor een situatie waarin twee slechte filosofen beide vorken hebben genomen en twee goede verhongeren.

Goed gevoede filosofen of competitieve .NET-programmering

Hier kun je zien dat de threads soms wakker worden en proberen de bron te krijgen. Twee van de vier kernen doen niets (groene grafiek hierboven).

Dood van een filosoof

Welnu, een ander probleem dat een glorieus diner van filosofen kan verstoren, is als een van hen plotseling sterft met een vork in zijn handen (en ze zullen hem zo begraven). Dan zitten de buren zonder lunch. U kunt zelf een voorbeeldcode voor deze zaak bedenken, deze wordt bijvoorbeeld weggegooid NullReferenceException nadat de filosoof de vorken heeft genomen. En trouwens, de uitzondering wordt niet afgehandeld en de aanroepende code zal het niet zomaar opvangen (voor dit AppDomain.CurrentDomain.UnhandledException en etc.). Daarom zijn foutafhandelaars nodig in de threads zelf en met sierlijke beëindiging.

ober

Oké, hoe lossen we dit probleem van impasse, uithongering en dood op? We laten slechts één filosoof toe om de vorken te bereiken, voegen een wederzijdse uitsluiting van threads toe voor deze plek. Hoe je dat doet? Stel dat er naast de filosofen een ober staat, die aan een filosoof toestemming geeft om de vorken aan te nemen. Hoe maken we deze ober en hoe filosofen hem zullen stellen, de vragen zijn interessant.

De eenvoudigste manier is wanneer de filosofen de ober gewoon constant om toegang tot de vorken vragen. Die. nu wachten filosofen niet op een vork in de buurt, maar wachten of vragen de ober. In eerste instantie gebruiken we hiervoor alleen User Space, daarin gebruiken we geen interrupts om procedures uit de kernel aan te roepen (hierover hieronder).

Oplossingen in gebruikersruimte

Hier doen we hetzelfde als vroeger met één vork en twee filosofen, we draaien in een cyclus en wachten. Maar nu zullen het allemaal filosofen zijn en als het ware slechts één vork, d.w.z. men kan zeggen dat alleen de filosoof die deze "gouden vork" van de ober heeft genomen, zal eten. Hiervoor gebruiken we 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 dit is een blocker, met grofweg hetzelfde while(true) { if (!lock) break; }, maar met nog meer "magie" dan in SpinWait (die daar wordt gebruikt). Nu weet hij de wachtenden te tellen, ze een beetje te laten inslapen en meer. etc. Doet er in het algemeen alles aan om te optimaliseren. Maar we moeten niet vergeten dat dit nog steeds dezelfde actieve cyclus is die processorbronnen opslokt en de stroom vasthoudt, wat kan leiden tot uithongering als een van de filosofen meer prioriteit krijgt dan andere, maar geen gouden vork heeft (Priority Inversion-probleem) . Daarom gebruiken we het alleen voor zeer korte wijzigingen in het gedeelde geheugen, zonder oproepen van derden, geneste vergrendelingen en andere verrassingen.

Goed gevoede filosofen of competitieve .NET-programmering

Tekenen voor SpinLock. De streams "vechten" constant om de gouden vork. Er zijn mislukkingen - in de figuur, het geselecteerde gebied. De kernen worden niet volledig benut: slechts ongeveer 2/3 door deze vier threads.

Een andere oplossing hier zou zijn om alleen te gebruiken Interlocked.CompareExchange met dezelfde actieve wacht als getoond in de bovenstaande code (in de uitgehongerde filosofen), maar dit zou, zoals reeds gezegd, theoretisch kunnen leiden tot blokkering.

Про Interlocked Opgemerkt moet worden dat er niet alleen CompareExchange, maar ook andere methoden voor atomisch lezen EN schrijven. En door wijzigingsherhaling, in het geval dat een andere thread tijd heeft om zijn wijzigingen aan te brengen (lees 1, lees 2, schrijf 2, schrijf 1 is slecht), kan deze worden gebruikt voor complexe wijzigingen in een enkele waarde (Interlocked Anything-patroon).

Oplossingen voor kernelmodus

Laten we eens kijken hoe we een thread kunnen blokkeren om te voorkomen dat we middelen in een lus verspillen. Met andere woorden, als we ons voorbeeld voortzetten, laten we eens kijken hoe de ober de filosoof in slaap brengt en hem alleen wakker maakt als dat nodig is. Laten we eerst eens kijken hoe we dit kunnen doen via de kernelmodus van het besturingssysteem. Alle structuren daar zijn vaak langzamer dan die in de gebruikersruimte. Meerdere keren langzamer bijvoorbeeld AutoResetEvent misschien 53 keer langzamer SpinLock [Richter]. Maar met hun hulp kunt u processen in het hele systeem synchroniseren, beheerd of niet.

De basisconstructie hier is de semafoor die Dijkstra meer dan een halve eeuw geleden voorstelde. Een semafoor is, simpel gezegd, een positief geheel getal dat door het systeem wordt beheerd, en twee bewerkingen daarop, verhogen en verlagen. Als het niet afneemt, nul, dan is de aanroepende thread geblokkeerd. Wanneer het nummer wordt verhoogd door een andere actieve thread/proces, worden de threads overgeslagen en wordt de semafoor opnieuw verlaagd met het doorgegeven nummer. Men kan zich treinen voorstellen in een knelpunt met een semafoor. .NET biedt verschillende constructies met vergelijkbare functionaliteit: AutoResetEvent, ManualResetEvent, Mutex en mezelf Semaphore. We zullen gebruiken AutoResetEvent, dit is de eenvoudigste van deze constructies: slechts twee waarden 0 en 1 (false, true). Haar methode WaitOne() blokkeert de aanroepende thread als de waarde 0 was, en als 1, verlaagt deze naar 0 en slaat deze over. Een methode Set() verhoogt naar 1 en laat één ober door, die weer verlaagt naar 0. Gedraagt ​​zich als een tourniquet in de metro.

Laten we de oplossing ingewikkelder maken en het slot voor elke filosoof gebruiken, en niet voor allemaal tegelijk. Die. nu kunnen er meerdere filosofen tegelijk zijn, en niet één. Maar we blokkeren opnieuw de toegang tot de tafel om op de juiste manier races te vermijden (raceomstandigheden), surebets te nemen.

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

Om te begrijpen wat hier gebeurt, overweeg het geval waarin de filosoof de vorken niet nam, dan zullen zijn acties als volgt zijn. Hij wacht op toegang tot de tafel. Nadat hij het heeft ontvangen, probeert hij de vorken te pakken. Niet gelukt. Het geeft toegang tot de tafel (wederzijdse uitsluiting). En passeert zijn "tourniquet" (AutoResetEvent) (ze zijn aanvankelijk open). Het komt weer in de cyclus, omdat hij heeft geen vorken. Hij probeert ze te pakken en stopt bij zijn "tourniquet". Een meer fortuinlijke buurman rechts of links, die klaar is met eten, ontgrendelt onze filosoof en opent zijn tourniquet. Onze filosoof passeert het (en het sluit erachter) voor de tweede keer. Hij probeert voor de derde keer de vorken te pakken. Succes. En hij passeert zijn tourniquet om te dineren.

Wanneer er willekeurige fouten in dergelijke code zitten (ze bestaan ​​altijd), bijvoorbeeld een buur is onjuist gespecificeerd of hetzelfde object is gemaakt AutoResetEvent voor iedereen (Enumerable.Repeat), dan wachten de filosofen op de ontwikkelaars, want Het vinden van fouten in dergelijke code is een vrij moeilijke taak. Een ander probleem met deze oplossing is dat het niet garandeert dat een of andere filosoof niet zal verhongeren.

Hybride oplossingen

We hebben gekeken naar twee benaderingen van timing, wanneer we in de gebruikersmodus en lus blijven, en wanneer we thread door de kernel blokkeren. De eerste methode is goed voor korte lokken, de tweede voor lange. Het is vaak nodig om eerst even te wachten tot een variabele verandert in een lus, en dan de thread te blokkeren als het wachten lang is. Deze aanpak is geïmplementeerd in de zgn. hybride structuren. Hier zijn dezelfde constructies als voor de kernelmodus, maar nu met een lus in de gebruikersmodus: SemaphorSlim, ManualResetEventSlim enz. Het meest populaire ontwerp hier is Monitor, omdat in C# is er een bekende lock syntaxis. Monitor dit is dezelfde semafoor met een maximale waarde van 1 (mutex), maar met ondersteuning voor wachten in een lus, recursie, het Conditie Variabele patroon (meer hierover hieronder), enz. Laten we eens kijken naar een oplossing ermee.

// Спрячем объект для Монитора от всех, чтобы без дедлоков.
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);
    }
}

Hier blokkeren we opnieuw de hele tafel voor toegang tot de vorken, maar nu deblokkeren we alle threads tegelijk, en niet de buren als iemand klaar is met eten. Die. eerst eet iemand en blokkeert de buren, en als deze iemand klaar is, maar meteen weer wil eten, gaat hij in de blokkering en maakt zijn buren wakker, omdat. de wachttijd is minder.

Zo vermijden we impasses en de uithongering van een of andere filosoof. We gebruiken een lus voor een korte wachttijd en blokkeren de draad voor een lange. Iedereen tegelijk deblokkeren gaat langzamer dan wanneer alleen de buurman werd gedeblokkeerd, zoals in de oplossing met AutoResetEvent, maar het verschil mag niet groot zijn, omdat threads moeten eerst in de gebruikersmodus blijven.

У lock syntaxis heeft nare verrassingen. Aanraden om te gebruiken Monitor direct [Richter] [Eric Lippert]. Een daarvan is dat lock altijd uit Monitor, zelfs als er een uitzondering was, in welk geval een andere thread de status van het gedeelde geheugen zou kunnen wijzigen. In dergelijke gevallen is het vaak beter om in een impasse te raken of het programma op de een of andere manier veilig te beëindigen. Een andere verrassing is dat Monitor synchronisatieblokken gebruikt (SyncBlock), die aanwezig zijn in alle objecten. Daarom kunt u, als een ongepast object is geselecteerd, gemakkelijk een impasse krijgen (bijvoorbeeld als u vergrendelt op een interne tekenreeks). Hiervoor gebruiken we het altijd verborgen object.

Met het patroon Conditievariabele kunt u de verwachting van een complexe aandoening beknopter implementeren. In .NET is het naar mijn mening onvolledig omdat in theorie zouden er meerdere wachtrijen moeten zijn op verschillende variabelen (zoals in Posix Threads), en niet op één lok. Dan zou je ze voor alle filosofen kunnen maken. Maar zelfs in deze vorm kunt u de code verkleinen.

veel filosofen of async / await

Oké, nu kunnen we threads effectief blokkeren. Maar wat als we veel filosofen hebben? 100? 10000? Zo ontvingen we 100000 verzoeken aan de webserver. Het zal overhead zijn om voor elk verzoek een thread te maken, omdat zoveel threads zullen niet parallel lopen. Zal slechts zoveel uitvoeren als er logische kernen zijn (ik heb er 4). En alle anderen zullen gewoon middelen wegnemen. Een oplossing voor dit probleem is het async/wait-patroon. Het idee is dat de functie de draad niet vasthoudt als hij moet wachten tot er iets doorgaat. En wanneer het iets doet, hervat het zijn uitvoering (maar niet noodzakelijk op dezelfde thread!). In ons geval wachten we op de vork.

SemaphoreSlim hiervoor heeft WaitAsync() methode. Hier is een implementatie die dit patroon gebruikt.

// Запуск такой же, как раньше. Где-нибудь в программе:
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();
}

Methode met async / await wordt vertaald in een lastige toestandsmachine die onmiddellijk zijn interne teruggeeft Task. Hierdoor kunt u wachten op de voltooiing van de methode, deze annuleren en al het andere dat u met Task kunt doen. Binnen de methode bestuurt de toestandsmachine de uitvoering. Het komt erop neer dat als er geen vertraging is, de uitvoering synchroon is en als die er is, wordt de thread vrijgegeven. Voor een beter begrip hiervan kun je beter naar deze toestandsmachine kijken. Hiervan kun je ketens maken async / await methoden.

Laten we testen. Werk van 100 filosofen op een machine met 4 logische kernen, 8 seconden. De vorige oplossing met Monitor draaide alleen de eerste 4 threads en de rest helemaal niet. Elk van deze 4 threads was ongeveer 2 ms inactief. En de asynchrone / wait-oplossing draaide alle 100, met een gemiddelde wachttijd van elk 6.8 seconden. In echte systemen is 6 seconden inactiviteit natuurlijk onaanvaardbaar en het is beter om niet zoveel van dit soort verzoeken te verwerken. De oplossing met Monitor bleek helemaal niet schaalbaar.

Conclusie

Zoals je aan deze kleine voorbeelden kunt zien, ondersteunt .NET veel synchronisatieconstructies. Het is echter niet altijd duidelijk hoe ze te gebruiken. Ik hoop dat dit artikel nuttig was. Voor nu is dit het einde, maar er is nog veel interessants over, bijvoorbeeld thread-safe collecties, TPL Dataflow, Reactive programming, Software Transaction model, etc.

bronnen

Bron: www.habr.com

Voeg een reactie