Filosofi bine hrăniți sau programare .NET competitivă

Filosofi bine hrăniți sau programare .NET competitivă

Să vedem cum funcționează programarea simultană și paralelă în .Net, folosind Philosophers Dining Problem ca exemplu. Planul este acesta, de la sincronizarea firelor/proceselor, până la modelul actorului (în următoarele părți). Articolul poate fi util pentru prima cunoștință sau pentru a vă reîmprospăta cunoștințele.

De ce să o faci deloc? Tranzistorii ating dimensiunea minimă, legea lui Moore se bazează pe limitarea vitezei luminii și de aceea se observă o creștere a numărului, se pot realiza mai multe tranzistoare. În același timp, cantitatea de date este în creștere, iar utilizatorii se așteaptă la un răspuns imediat din partea sistemelor. Într-o astfel de situație, programarea „normală”, atunci când avem un fir de execuție, nu mai este eficientă. Trebuie să rezolvați cumva problema execuției simultane sau concurente. Mai mult, această problemă există la diferite niveluri: la nivel de fire, la nivel de procese, la nivelul mașinilor din rețea (sisteme distribuite). .NET are tehnologii de înaltă calitate, testate în timp, pentru a rezolva rapid și eficient astfel de probleme.

Sarcină

Edsger Dijkstra a pus această problemă studenților săi încă din 1965. Formularea stabilită este următoarea. Există un anumit număr (de obicei cinci) de filozofi și același număr de furculițe. Ei stau la o masă rotundă, cu furculițe între ei. Filosofii pot mânca din farfuriile lor cu mâncare nesfârșită, pot gândi sau aștepta. Pentru a mânca un filozof, trebuie să iei două furculițe (ultima împarte furculița cu prima). Ridicarea și lăsarea unei furculițe sunt două acțiuni separate. Toți filozofii tac. Sarcina este de a găsi un astfel de algoritm încât toți să gândească și să fie plini chiar și după 54 de ani.

În primul rând, să încercăm să rezolvăm această problemă prin utilizarea unui spațiu comun. Furculițele stau pe masa comună, iar filosofii pur și simplu le iau când sunt și le pun la loc. Aici sunt probleme cu sincronizarea, când anume să luăm surebets? ce daca nu exista furca? etc. Dar mai întâi, să începem filozofii.

Pentru a începe firele, folosim un pool de fire prin intermediul 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);
}

Pool-ul de fire este conceput pentru a optimiza crearea și ștergerea firelor. Acest pool are o coadă cu sarcini, iar CLR creează sau elimină fire în funcție de numărul acestor sarcini. Un singur pool pentru toate AppDomains. Această piscină ar trebui folosită aproape întotdeauna, deoarece. nu trebuie să vă deranjați cu crearea, ștergerea firelor de execuție, a cozilor lor etc. Este posibil fără un pool, dar apoi trebuie să îl utilizați direct Thread, acest lucru este util pentru cazurile în care trebuie să schimbați prioritatea unui thread, când avem o operațiune lungă, pentru un thread Foreground etc.

Cu alte cuvinte, System.Threading.Tasks.Task clasa este aceeași Thread, dar cu tot felul de facilități: capacitatea de a rula o sarcină după un bloc de alte sarcini, de a le returna din funcții, de a le întrerupe convenabil și multe altele. etc. Sunt necesare pentru a susține construcții asincrone/await (Task-based Asynchronous Pattern, zahăr sintactic pentru așteptarea operațiunilor IO). Vom vorbi despre asta mai târziu.

CancelationTokenSource aici este nevoie pentru ca firul să se poată termina singur la semnalul firului de apelare.

Probleme de sincronizare

Filosofii blocați

Bine, știm cum să creăm fire, să încercăm să luăm prânzul:

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

Aici încercăm mai întâi să luăm furculița din stânga, apoi cea din dreapta, iar dacă merge, atunci mâncăm și le punem înapoi. A lua o furculiță este atomică, adică. două fire nu pot lua unul în același timp (incorect: primul scrie că furculița este liberă, al doilea - de asemenea, prima ia, a doua ia). Pentru aceasta Interlocked.CompareExchange, care trebuie implementat cu o instrucțiune procesor (TSL, XCHG), care blochează o bucată de memorie pentru citirea și scrierea secvențială atomică. Și SpinWait este echivalent cu constructul while(true) doar cu puțină „magie” - firul ia procesorul (Thread.SpinWait), dar uneori transferă controlul către alt fir (Thread.Yeild) sau adoarme (Thread.Sleep).

Dar această soluție nu funcționează, pentru că curgele curand (pentru mine intr-o secunda) sunt blocate: toti filozofii isi iau furca din stanga, dar nu pe cea dreapta. Matricea forks are atunci valorile: 1 2 3 4 5.

Filosofi bine hrăniți sau programare .NET competitivă

În figură, fire de blocare (blocare). Verde - execuție, roșu - sincronizare, gri - firul este adormit. Romburi indică ora de începere a sarcinilor.

Foamea filozofilor

Deși nu este necesar să se gândească în special la multă mâncare, dar foamea face pe oricine să renunțe la filozofie. Să încercăm să simulăm situația de înfometare a firelor în problema noastră. Înfometarea este atunci când un fir rulează, dar fără muncă semnificativă, cu alte cuvinte, acesta este același blocaj, doar că acum firul nu doarme, ci caută activ ceva de mâncare, dar nu există mâncare. Pentru a evita blocarea frecventă, vom pune furca înapoi dacă nu am putea lua alta.

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

Lucrul important la acest cod este că doi din patru filosofi uită să pună jos furculița din stânga. Și se dovedește că ei mănâncă mai multă mâncare, în timp ce alții încep să moară de foame, deși firele au aceeași prioritate. Aici nu mor complet de foame, pentru că. filozofii răi își pun furculița înapoi uneori. Se dovedește că oamenii buni mănâncă de aproximativ 5 ori mai puțin decât cei răi. Deci, o mică eroare în cod duce la o scădere a performanței. De asemenea, este de remarcat aici că o situație rară este posibilă când toți filozofii iau bifurcația din stânga, nu există una dreaptă, pun stânga, așteaptă, iau din nou stânga etc. Această situație este, de asemenea, o înfometare, mai mult ca un impas. Nu am reușit să o repet. Mai jos este o imagine pentru o situație în care doi filozofi răi au luat ambele furculițe și doi buni mor de foame.

Filosofi bine hrăniți sau programare .NET competitivă

Aici puteți vedea că firele se trezesc uneori și încearcă să obțină resursa. Două dintre cele patru nuclee nu fac nimic (graficul verde de mai sus).

Moartea unui filozof

Ei bine, o altă problemă care poate întrerupe o cină glorioasă a filozofilor este dacă unul dintre ei moare brusc cu furculițele în mâini (și îl vor îngropa așa). Apoi vecinii vor rămâne fără cină. Puteți veni singur cu un exemplu de cod pentru acest caz, de exemplu, acesta este aruncat NullReferenceException după ce filosoful ia furcile. Și, apropo, excepția nu va fi gestionată și codul de apel nu o va prinde pur și simplu (pentru aceasta AppDomain.CurrentDomain.UnhandledException si etc.). Prin urmare, manipulatorii de erori sunt necesari în firele în sine și cu terminare grațioasă.

chelner

Bine, cum rezolvăm această problemă de blocaj, foamete și moarte? Vom permite doar unui singur filozof să ajungă la furci, adăugați o excludere reciprocă a firelor pentru acest loc. Cum să o facă? Să presupunem că lângă filozofi stă un chelner, care dă permis oricărui filosof să ia furculițele. Cum îl facem pe acest chelner și cum îl vor pune filozofii, întrebările sunt interesante.

Cel mai simplu mod este atunci când filosofii vor cere în mod constant chelnerului accesul la furculițe. Acestea. acum filozofii nu vor aștepta o furculiță în apropiere, ci vor aștepta sau îl vor întreba pe chelner. La început, folosim doar spațiul utilizator pentru aceasta, în el nu folosim întreruperi pentru a apela nicio procedură din kernel (despre ele mai jos).

Soluții în spațiul utilizatorului

Aici vom face la fel cum făceam obișnuit cu o furculiță și doi filozofi, ne vom învârti într-un ciclu și ne vom aștepta. Dar acum vor fi toți filozofi și, parcă, doar o furculiță, adică. se poate spune că doar filosoful care a luat această „furculiță de aur” de la chelner va mânca. Pentru aceasta folosim 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 acesta este un blocant, cu, aproximativ, același lucru while(true) { if (!lock) break; }, dar cu și mai multă „magie” decât în SpinWait (care este folosit acolo). Acum știe să numere cei care așteaptă, să-i adoarmă puțin și nu numai. etc. În general, face tot posibilul pentru a optimiza. Dar trebuie să ne amintim că acesta este în continuare același ciclu activ care consumă resursele procesorului și menține fluxul, ceea ce poate duce la înfometare dacă unul dintre filozofi devine mai prioritar decât alții, dar nu are o furcă de aur (problema inversării priorității) . Prin urmare, îl folosim doar pentru modificări foarte foarte scurte în memoria partajată, fără apeluri terță parte, blocări imbricate și alte surprize.

Filosofi bine hrăniți sau programare .NET competitivă

Desen pentru SpinLock. Pârâurile se „luptează” în permanență pentru furculița de aur. Există eșecuri - în figură, zona selectată. Miezurile nu sunt utilizate pe deplin: doar aproximativ 2/3 din aceste patru fire.

O altă soluție aici ar fi folosirea numai Interlocked.CompareExchange cu aceeași așteptare activă ca cea arătată în codul de mai sus (la filozofii înfometați), dar acest lucru, așa cum s-a spus deja, ar putea duce teoretic la blocare.

despre Interlocked Trebuie remarcat faptul că nu există numai CompareExchange, dar și alte metode de citire ȘI scriere atomică. Și prin repetarea modificării, în cazul în care un alt thread are timp să-și facă modificările (citește 1, citește 2, scrie 2, scrie 1 este rău), poate fi folosit pentru modificări complexe la o singură valoare (model Interlocked Anything) .

Soluții pentru modul Kernel

Pentru a evita irosirea resurselor într-o buclă, să vedem cum putem bloca un fir. Cu alte cuvinte, continuând exemplul nostru, să vedem cum chelnerul adoarme filosoful și îl trezește doar când este nevoie. În primul rând, să vedem cum să faceți acest lucru prin modul kernel al sistemului de operare. Toate structurile de acolo sunt adesea mai lente decât cele din spațiul utilizatorului. De câteva ori mai încet, de exemplu AutoResetEvent poate de 53 de ori mai lent SpinLock [Richter]. Dar cu ajutorul lor, puteți sincroniza procese în întregul sistem, gestionate sau nu.

Construcția de bază aici este semaforul propus de Dijkstra acum peste jumătate de secol. Un semafor este, pur și simplu, un număr întreg pozitiv gestionat de sistem și două operații asupra acestuia, creștere și decrementare. Dacă nu reușește să scadă, zero, atunci firul de apelare este blocat. Când numărul este incrementat de un alt fir/proces activ, atunci firele sunt sărite și semaforul este din nou decrementat cu numărul trecut. Ne putem imagina trenuri într-un blocaj cu un semafor. .NET oferă mai multe constructe cu funcționalități similare: AutoResetEvent, ManualResetEvent, Mutex si eu Semaphore. Noi vom folosi AutoResetEvent, aceasta este cea mai simplă dintre aceste construcții: doar două valori 0 și 1 (fals, adevărat). Metoda ei WaitOne() blochează firul de apel dacă valoarea a fost 0, iar dacă 1, îl coboară la 0 și îl omite. O metodă Set() ridică la 1 și lasă un chelner să treacă, care din nou coboară la 0. Acționează ca un turnichet de metrou.

Să complicăm soluția și să folosim lacătul pentru fiecare filosof și nu pentru toți deodată. Acestea. acum pot fi mai mulți filozofi deodată și nu unul. Dar blocăm din nou accesul la masă pentru a putea corect, evitând cursele (condiții de cursă), să luăm surebets.

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

Pentru a înțelege ce se întâmplă aici, luați în considerare cazul în care filozoful nu a reușit să ia furculițele, atunci acțiunile sale vor fi după cum urmează. El așteaptă accesul la masă. După ce l-a primit, încearcă să ia furculițele. Nu a funcționat. Oferă acces la tabel (excludere reciprocă). Și trece pe lângă „turnichetul” lui (AutoResetEvent) (sunt deschise inițial). Intră din nou în ciclu, pentru că nu are furculițe. Încearcă să le ia și se oprește la „turnichetul lui”. Un vecin mai norocos din dreapta sau din stânga, după ce a terminat de mâncat, descuie filosoful nostru, „deschizându-și turnichetul”. Filosoful nostru trece pe lângă ea (și se închide în urma ei) pentru a doua oară. Încearcă pentru a treia oară să ia furcile. Noroc. Și își trece turnichetul să ia masa.

Când există erori aleatorii într-un astfel de cod (există întotdeauna), de exemplu, un vecin este specificat incorect sau este creat același obiect AutoResetEvent pentru toți (Enumerable.Repeat), atunci filosofii îi vor aștepta pe dezvoltatori, pentru că Găsirea erorilor într-un astfel de cod este o sarcină destul de dificilă. O altă problemă cu această soluție este că nu garantează că vreun filosof nu va fi foame.

Soluții hibride

Am analizat două abordări ale sincronizarii, când rămânem în modul utilizator și buclă și când blocăm thread-ul prin nucleu. Prima metodă este bună pentru încuietori scurte, a doua pentru cele lungi. Este adesea necesar să așteptați pentru scurt timp ca o variabilă să se schimbe într-o buclă și apoi să blocați firul de execuție când așteptarea este lungă. Această abordare este implementată în așa-numitul. structuri hibride. Iată aceleași construcții ca pentru modul kernel, dar acum cu o buclă în modul utilizator: SemaphorSlim, ManualResetEventSlim etc. Cel mai popular design aici este Monitor, deoarece în C# există un binecunoscut lock sintaxă. Monitor acesta este același semafor cu o valoare maximă de 1 (mutex), dar cu suport pentru așteptarea într-o buclă, recursiunea, modelul Variabila condiției (mai multe despre asta mai jos), etc. Să ne uităm la o soluție cu el.

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

Aici blocăm din nou întreaga masă pentru accesul la furculițe, dar acum deblochăm toate firele deodată, și nu vecinii când cineva termină de mâncat. Acestea. mai întâi, cineva mănâncă și îi blochează pe vecini, iar când acesta termină cineva, dar vrea să mănânce din nou imediat, intră în blocaj și își trezește vecinii, pentru că. timpul său de așteptare este mai mic.

Așa evităm blocajele și foametea unui filosof. Folosim o buclă pentru o scurtă așteptare și blocăm firul pentru unul lung. Deblocarea tuturor deodată este mai lent decât dacă ar fi deblocat doar vecinul, ca în soluția cu AutoResetEvent, dar diferența nu ar trebui să fie mare, pentru că firele trebuie să rămână mai întâi în modul utilizator.

У lock sintaxa are surprize urâte. Recomand de utilizat Monitor direct [Richter] [Eric Lippert]. Una dintre ele este că lock mereu din Monitor, chiar dacă a existat o excepție, caz în care un alt fir ar putea schimba starea memoriei partajate. În astfel de cazuri, este adesea mai bine să treceți în impas sau să încheiați programul în siguranță. O altă surpriză este că Monitor folosește blocuri de sincronizare (SyncBlock), care sunt prezente în toate obiectele. Prin urmare, dacă este selectat un obiect neadecvat, puteți obține cu ușurință un impas (de exemplu, dacă blocați un șir internat). Pentru aceasta folosim obiectul mereu ascuns.

Modelul variabilă de condiție vă permite să implementați mai concis așteptările unei anumite condiții complexe. În .NET, este incomplet, după părerea mea, pentru că în teorie, ar trebui să existe mai multe cozi pe mai multe variabile (ca în Posix Threads), și nu pe un singur loc. Atunci s-ar putea să le facă pentru toți filozofii. Dar chiar și în această formă, vă permite să reduceți codul.

multi filozofi sau async / await

Bine, acum putem bloca în mod eficient firele. Dar dacă avem mulți filozofi? 100? 10000? De exemplu, am primit 100000 de solicitări către serverul web. Crearea unui fir pentru fiecare solicitare va fi peste măsură, deoarece atât de multe fire nu vor rula în paralel. Va rula doar câte nuclee logice există (am 4). Și toți ceilalți vor lua doar resurse. O soluție la această problemă este modelul asincron / așteptare. Ideea sa este că funcția nu reține firul dacă trebuie să aștepte ca ceva să continue. Și când face ceva, își reia execuția (dar nu neapărat pe același fir!). În cazul nostru, vom aștepta furculița.

SemaphoreSlim are pentru asta WaitAsync() metodă. Iată o implementare folosind acest model.

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

Metoda cu async / await este tradus într-o mașină de stări complicată care își returnează imediat interiorul Task. Prin intermediul acestuia, puteți aștepta finalizarea metodei, o anulați și tot ce puteți face cu Task. În cadrul metodei, mașina de stat controlează execuția. Concluzia este că, dacă nu există întârziere, atunci execuția este sincronă, iar dacă există, atunci firul este eliberat. Pentru o mai bună înțelegere a acestui lucru, este mai bine să vă uitați la această mașină de stare. Puteți crea lanțuri din acestea async / await metode.

Să testăm. Lucrare a 100 de filozofi pe o mașină cu 4 nuclee logice, 8 secunde. Soluția anterioară cu Monitor rula doar primele 4 fire și restul nu rula deloc. Fiecare dintre aceste 4 fire a fost inactiv timp de aproximativ 2 ms. Iar soluția async / await a rulat toate cele 100, cu o așteptare medie de 6.8 secunde fiecare. Desigur, în sistemele reale, inactiv timp de 6 secunde este inacceptabil și este mai bine să nu procesezi atât de multe cereri de acest fel. Soluția cu Monitor s-a dovedit a nu fi deloc scalabilă.

Concluzie

După cum puteți vedea din aceste exemple mici, .NET acceptă multe constructe de sincronizare. Cu toate acestea, nu este întotdeauna evident cum să le folosiți. Sper că acest articol a fost de ajutor. Deocamdată, acesta este sfârșitul, dar au mai rămas o mulțime de lucruri interesante, de exemplu, colecții thread-safe, TPL Dataflow, programare reactivă, model de tranzacție software etc.

surse

Sursa: www.habr.com

Adauga un comentariu