Dobro uhranjeni filozofi ili kompetitivno .NET programiranje

Dobro uhranjeni filozofi ili kompetitivno .NET programiranje

Pogledajmo kako paralelno i paralelno programiranje radi u .Net-u, na primjeru Philosophers Dining Problem. Plan je ovo, od sinkronizacije niti/procesa, do modela aktera (u sljedećim dijelovima). Članak može biti koristan za prvo upoznavanje ili za obnavljanje znanja.

Zašto to uopće činiti? Tranzistori dosežu svoju minimalnu veličinu, Mooreov zakon počiva na ograničenju brzine svjetlosti i stoga se uočava povećanje broja, može se napraviti više tranzistora. Istodobno raste količina podataka, a korisnici očekuju hitan odgovor sustava. U takvoj situaciji "normalno" programiranje, kada imamo jednu izvršnu nit, više nije učinkovito. Morate nekako riješiti problem simultanog ili istovremenog izvršavanja. Štoviše, ovaj problem postoji na različitim razinama: na razini niti, na razini procesa, na razini strojeva u mreži (distribuirani sustavi). .NET ima visokokvalitetne, vremenski testirane tehnologije za brzo i učinkovito rješavanje takvih problema.

Zadatak

Edsger Dijkstra postavio je ovaj problem svojim studentima još 1965. Utvrđena formulacija je sljedeća. Postoji određeni (obično pet) broj filozofa i isto toliko vilica. Sjede za okruglim stolom, između njih su vilice. Filozofi mogu jesti iz svojih tanjura beskonačnu hranu, razmišljati ili čekati. Da biste pojeli filozofa, trebate uzeti dvije vilice (posljednja dijeli vilicu s prvom). Podizanje i odlaganje vilice dvije su odvojene radnje. Svi filozofi šute. Zadatak je pronaći takav algoritam da bi svi oni razmišljali i bili siti i nakon 54 godine.

Prvo, pokušajmo riješiti ovaj problem korištenjem zajedničkog prostora. Vilice leže na zajedničkom stolu i filozofi ih jednostavno uzmu kad jesu i vrate ih natrag. Ovdje postoje problemi sa sinkronizacijom, kada točno uzeti surebets? što ako nema vilice? itd. Ali prvo, počnimo s filozofima.

Za pokretanje niti koristimo skup niti Task.Run metoda:

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

Skup niti dizajniran je za optimizaciju stvaranja i brisanja niti. Ovo spremište ima red čekanja sa zadacima i CLR stvara ili uklanja niti ovisno o broju tih zadataka. Jedan skup za sve AppDomains. Ovaj bazen treba koristiti gotovo uvijek, jer. nema potrebe zamarati se stvaranjem, brisanjem niti, njihovim redovima čekanja itd. Moguće je bez skupa, ali tada ga morate koristiti izravno Thread, ovo je korisno u slučajevima kada trebate promijeniti prioritet niti, kada imamo dugu operaciju, za nit u prvom planu itd.

Drugim riječima, System.Threading.Tasks.Task razred je isti Thread, ali sa svim vrstama pogodnosti: mogućnost pokretanja zadatka nakon bloka drugih zadataka, vraćanje iz funkcija, prikladno ih prekidanje i više. itd. Potrebni su za podršku konstrukcija async/await (Asinkroni uzorak temeljen na zadatku, sintaktički šećer za čekanje IO operacija). Razgovarat ćemo o ovome kasnije.

CancelationTokenSource ovdje je to potrebno kako bi se nit mogla sama prekinuti na signal pozivajuće niti.

Problemi sa sinkronizacijom

Blokirani filozofi

U redu, znamo kako stvarati niti, pokušajmo ručati:

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

Ovdje prvo pokušavamo uzeti lijevu, a zatim desnu vilicu, pa ako uspije, jedemo i vraćamo ih natrag. Uzimanje jedne vilice je atomsko, tj. dvije niti ne mogu uzeti jednu u isto vrijeme (netočno: prva čita da je fork slobodna, druga - također, prva uzima, druga uzima). Za ovo Interlocked.CompareExchange, koji se mora implementirati s instrukcijom procesora (TSL, XCHG), koji zaključava dio memorije za atomsko sekvencijalno čitanje i pisanje. A SpinWait je ekvivalent konstruktu while(true) samo uz malo "čarolije" - nit uzima procesor (Thread.SpinWait), ali ponekad prenosi kontrolu na drugu nit (Thread.Yeild) ili zaspi (Thread.Sleep).

Ali ovo rješenje ne funkcionira, jer tokovi su uskoro (za mene u roku od jedne sekunde) blokirani: svi filozofi hvataju lijevu vilicu, ali ne i desnu. Niz forks tada ima vrijednosti: 1 2 3 4 5.

Dobro uhranjeni filozofi ili kompetitivno .NET programiranje

Na slici, blokiranje niti (zastoj). Zeleno - izvršenje, crveno - sinkronizacija, sivo - nit spava. Rombovi označavaju vrijeme početka zadataka.

Glad filozofa

Iako nije potrebno posebno razmišljati o hrani, ali glad tjera svakoga da odustane od filozofije. Pokušajmo simulirati situaciju gladovanja niti u našem problemu. Izgladnjivanje je kada nit radi, ali bez značajnijeg rada, drugim riječima, ovo je isti zastoj, samo sada nit ne spava, već aktivno traži nešto za jelo, ali nema hrane. Kako bismo izbjegli često blokiranje, vratit ćemo vilicu ako nismo mogli uzeti drugu.

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

Važna stvar kod ovog kodeksa je da dva od četiri filozofa zaborave spustiti lijevu vilicu. I ispada da oni jedu više hrane, dok drugi počinju gladovati, iako niti imaju isti prioritet. Ovdje nisu potpuno gladni, jer. loši filozofi ponekad vrate svoje vilice. Ispostavilo se da dobri ljudi jedu oko 5 puta manje od loših. Dakle, mala greška u kodu dovodi do pada performansi. Ovdje također vrijedi napomenuti da je moguća rijetka situacija kada svi filozofi uzmu lijevu vilicu, nema desne, stave lijevu, čekaju, ponovno krenu lijevom itd. Ova situacija je također gladovanje, više nalik mrtvoj točki. Nisam uspio ponoviti. Ispod je slika za situaciju u kojoj su dva loša filozofa uzela obje vilice, a dva dobra gladuju.

Dobro uhranjeni filozofi ili kompetitivno .NET programiranje

Ovdje možete vidjeti da se niti ponekad probude i pokušaju doći do resursa. Dvije od četiri jezgre ne rade ništa (zeleni grafikon iznad).

Smrt filozofa

Pa, još jedan problem koji može prekinuti slavnu večeru filozofa je ako jedan od njih iznenada umre s vilicama u rukama (i tako će ga pokopati). Tada će susjedi ostati bez ručka. Možete sami smisliti primjer koda za ovaj slučaj, na primjer, izbačen je NullReferenceException nakon što filozof uzme vilice. I, usput, iznimka se neće obrađivati ​​i pozivni kod je neće samo uhvatiti (za ovo AppDomain.CurrentDomain.UnhandledException i tako dalje.). Stoga su rukovatelji greškama potrebni u samim nitima i s elegantnim završetkom.

konobar

U redu, kako ćemo riješiti ovaj problem bezizlazne situacije, gladovanja i smrti? Dopustit ćemo samo jednom filozofu da dosegne račve, dodati međusobno isključivanje niti za ovo mjesto. Kako to učiniti? Pretpostavimo da kraj filozofa stoji konobar, koji daje dopuštenje bilo kojem filozofu da uzme vilice. Kako napraviti ovog konobara i kako će ga pitati filozofi, pitanja su zanimljiva.

Najjednostavnije je kada će filozofi jednostavno neprestano tražiti od konobara pristup vilicama. Oni. sada filozofi neće čekati vilicu u blizini, nego čekati ili pitati konobara. Isprva za to koristimo samo korisnički prostor, u njemu ne koristimo prekide za pozivanje bilo kakvih procedura iz kernela (o njima u nastavku).

Rješenja u korisničkom prostoru

Ovdje ćemo isto kao i nekad s jednom vilicom i dva filozofa, vrtit ćemo se u ciklusu i čekati. Ali sada će to biti svi filozofi i, takoreći, samo jedna vilica, t.j. može se reći da će jesti samo onaj filozof koji je uzeo ovu “zlatnu vilicu” od konobara. Za to koristimo 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 ovo je blokator, s, grubo rečeno, istim while(true) { if (!lock) break; }, ali s još više "magije" nego u SpinWait (koji se tamo koristi). Sada zna prebrojati one koji čekaju, malo ih uspavati, i više. itd. Općenito, čini sve što je moguće za optimizaciju. Ali moramo zapamtiti da je to još uvijek isti aktivni ciklus koji jede resurse procesora i održava protok, što može dovesti do gladovanja ako jedan od filozofa postane prioritetniji od ostalih, ali nema zlatnu vilicu (problem Inverzije prioriteta) . Stoga ga koristimo samo za vrlo vrlo kratke promjene u zajedničkoj memoriji, bez ikakvih poziva trećih strana, ugniježđenih zaključavanja i drugih iznenađenja.

Dobro uhranjeni filozofi ili kompetitivno .NET programiranje

Crtanje za SpinLock. Potoci se neprestano "bore" za zlatnu vilicu. Postoje kvarovi - na slici, odabrano područje. Jezgre nisu u potpunosti iskorištene: ove četiri niti samo oko 2/3.

Drugo rješenje ovdje bi bilo korištenje samo Interlocked.CompareExchange s istim aktivnim čekanjem kao što je prikazano u gornjem kodu (u gladnim filozofima), ali to bi, kao što je već rečeno, teoretski moglo dovesti do blokiranja.

na Interlocked Treba napomenuti da ne postoji samo CompareExchange, ali i druge metode za atomsko čitanje I pisanje. I kroz ponavljanje promjene, u slučaju da druga nit ima vremena napraviti svoje promjene (čitanje 1, čitanje 2, pisanje 2, pisanje 1 je loše), može se koristiti za složene promjene jedne vrijednosti (uzorak Interlocked Anything) .

Rješenja načina rada jezgre

Kako bismo izbjegli rasipanje resursa u petlji, pogledajmo kako možemo blokirati nit. Drugim riječima, nastavljajući naš primjer, pogledajmo kako konobar uspavljuje filozofa i budi ga samo po potrebi. Prvo, pogledajmo kako to učiniti kroz kernel mod operativnog sustava. Sve su strukture tamo često sporije od onih u korisničkom prostoru. Nekoliko puta sporiji npr AutoResetEvent možda 53 puta sporije SpinLock [Richter]. Ali uz njihovu pomoć možete sinkronizirati procese u cijelom sustavu, upravljani ili ne.

Osnovni konstrukt ovdje je semafor koji je predložio Dijkstra prije više od pola stoljeća. Semafor je, jednostavno rečeno, pozitivan cijeli broj kojim upravlja sustav i dvije operacije na njemu, inkrement i dekrement. Ako se ne uspije smanjiti, nula, tada je pozivna nit blokirana. Kada se broj povećava pomoću neke druge aktivne niti/procesa, tada se niti preskaču i semafor se ponovno smanjuje za broj koji je proslijeđen. Mogu se zamisliti vlakovi u uskom grlu sa semaforom. .NET nudi nekoliko konstrukcija sa sličnim funkcijama: AutoResetEvent, ManualResetEvent, Mutex i ja Semaphore. Koristit ćemo se AutoResetEvent, ovo je najjednostavnija od ovih konstrukcija: samo dvije vrijednosti 0 i 1 (false, true). Njezina metoda WaitOne() blokira pozivnu nit ako je vrijednost bila 0, a ako je 1, spušta je na 0 i preskače. Metoda Set() diže na 1 i propušta jednog konobara, koji opet spušta na 0. Ponaša se kao okretište podzemne željeznice.

Zakomplicirajmo rješenje i koristimo bravu za svakog filozofa, a ne za sve odjednom. Oni. sada može biti nekoliko filozofa odjednom, a ne jedan. Ali ponovno blokiramo pristup stolu kako bismo ispravno, izbjegavajući utrke (uvjete utrke), uzimali sigurnosne oklade.

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

Da biste razumjeli što se ovdje događa, razmotrite slučaj kada filozof nije uspio uzeti vilice, tada će njegovi postupci biti sljedeći. Čeka pristup stolu. Primivši ga, pokušava uzeti vilice. Nije uspjelo. Omogućuje pristup tablici (međusobno isključivanje). I prolazi pored njegove "okretnice" (AutoResetEvent) (u početku su otvoreni). Ponovno ulazi u ciklus, jer on nema vilice. Pokušava ih uzeti i staje kod svoje "okretnice". Neki sretniji susjed s desne ili lijeve strane, nakon što je završio s jelom, otključa našeg filozofa, "otvarajući mu okretnicu". Naš ga filozof prolazi (i zatvara se za njim) po drugi put. Treći put pokušava uzeti vilice. Sretno. I prolazi kraj svoje okretnice da večera.

Kada postoje nasumične pogreške u takvom kodu (one uvijek postoje), na primjer, susjed je netočno naveden ili je kreiran isti objekt AutoResetEvent za sve (Enumerable.Repeat), tada će filozofi čekati programere, jer Pronalaženje pogrešaka u takvom kodu prilično je težak zadatak. Još jedan problem s ovim rješenjem je što ne jamči da neki filozof neće ostati gladan.

Hibridna rješenja

Pogledali smo dva pristupa vremenskom određivanju, kada ostajemo u korisničkom načinu rada i petlji, i kada blokiramo nit kroz kernel. Prva metoda je dobra za kratke pramenove, druga za duge. Često je potrebno prvo kratko pričekati da se varijabla promijeni u petlji, a zatim blokirati nit kada je čekanje dugo. Ovaj pristup se provodi u tzv. hibridne strukture. Ovdje su iste konstrukcije kao za način rada jezgre, ali sada s petljom korisničkog načina rada: SemaphorSlim, ManualResetEventSlim itd. Ovdje je najpopularniji dizajn Monitor, jer u C# postoji dobro poznata lock sintaksa. Monitor ovo je isti semafor s maksimalnom vrijednošću 1 (mutex), ali s podrškom za čekanje u petlji, rekurziju, uzorak varijable uvjeta (više o tome u nastavku), itd. Pogledajmo rješenje s njim.

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

Ovdje ponovno blokiramo cijeli stol za pristup vilicama, ali sada deblokiramo sve niti odjednom, a ne susjede kada netko završi s jelom. Oni. prvo netko jede i blokira susjede, pa kad ovaj netko završi, ali hoće odmah opet jesti, ide u blokadu i budi susjede, jer. njegovo vrijeme čekanja je manje.

Tako izbjegavamo zastoje i izgladnjivanje nekog filozofa. Koristimo petlju za kratko čekanje i blokiramo nit za dugo. Deblokiranje svih odjednom je sporije nego da je samo susjed deblokiran, kao u rješenju sa AutoResetEvent, ali razlika ne bi trebala biti velika, jer niti prvo moraju ostati u korisničkom načinu rada.

У lock sintaksa ima neugodnih iznenađenja. Preporuka za korištenje Monitor izravno [Richter] [Eric Lippert]. Jedan od njih je taj lock uvijek izvan Monitor, čak i ako postoji iznimka, u kojem slučaju druga nit može promijeniti stanje zajedničke memorije. U takvim slučajevima često je bolje otići u mrtvu blokadu ili nekako sigurno prekinuti program. Još jedno iznenađenje je da Monitor koristi blokove sinkronizacije (SyncBlock), koji su prisutni u svim objektima. Stoga, ako je odabran neodgovarajući objekt, možete lako doći do zastoja (na primjer, ako zaključate interni niz). Za to koristimo uvijek skriveni predmet.

Uzorak varijable uvjeta omogućuje koncizniju implementaciju očekivanja nekog složenog uvjeta. U .NET-u je, po mom mišljenju, nepotpun jer u teoriji bi trebalo postojati nekoliko redova čekanja na nekoliko varijabli (kao u Posix Threads), a ne na jednom mjestu. Tada bi ih netko mogao napraviti za sve filozofe. Ali čak iu ovom obliku, omogućuje vam smanjenje koda.

mnogi filozofi ili async / await

U redu, sada možemo učinkovito blokirati niti. Ali što ako imamo mnogo filozofa? 100? 10000? Na primjer, primili smo 100000 zahtjeva za web poslužitelj. Stvaranje niti za svaki zahtjev bit će naporno jer tako mnogo niti neće raditi paralelno. Pokretat će se samo onoliko koliko ima logičkih jezgri (imam 4). A svi ostali će samo oduzeti resurse. Jedno rješenje za ovaj problem je uzorak async/await. Njegova ideja je da funkcija ne zadržava nit ako treba čekati da se nešto nastavi. A kada nešto učini, nastavlja s izvođenjem (ali ne nužno na istoj niti!). U našem slučaju, čekat ćemo fork.

SemaphoreSlim ima za ovo WaitAsync() metoda. Ovdje je implementacija koja koristi ovaj obrazac.

// Запуск такой же, как раньше. Где-нибудь в программе:
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 s async / await se prevodi u lukavi državni stroj koji odmah vraća svoje unutarnje Task. Preko njega možete čekati završetak metode, otkazati ju i sve ostalo što možete raditi s Taskom. Unutar metode, stroj stanja kontrolira izvršenje. Suština je da ako nema kašnjenja, onda je izvršenje sinkrono, a ako postoji, onda se nit oslobađa. Za bolje razumijevanje ovoga, bolje je pogledati ovaj državni stroj. Od njih možete stvoriti lance async / await metode.

Idemo testirati. Rad 100 filozofa na stroju s 4 logičke jezgre, 8 sekundi. Prethodno rješenje s Monitorom pokrenulo je samo prve 4 niti, a ostatak se uopće nije pokrenuo. Svaka od ove 4 niti bila je u mirovanju oko 2 ms. A async/await rješenje pokrenulo je svih 100, s prosječnim čekanjem od 6.8 ​​sekundi po svakom. Naravno, u stvarnim sustavima mirovanje od 6 sekundi je neprihvatljivo i bolje je ne obrađivati ​​toliki broj zahtjeva kao što je ovaj. Pokazalo se da rješenje s Monitorom uopće nije skalabilno.

Zaključak

Kao što možete vidjeti iz ovih malih primjera, .NET podržava mnoge konstrukcije sinkronizacije. Međutim, nije uvijek jasno kako ih koristiti. Nadam se da je ovaj članak bio od pomoći. Za sada je ovo kraj, ali ostalo je još puno zanimljivih stvari, na primjer, thread-safe kolekcije, TPL Dataflow, Reactive programming, Software Transaction model itd.

izvori

Izvor: www.habr.com

Dodajte komentar