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