Dobro hranjeni filozofi ili konkurentno .NET programiranje

Dobro hranjeni filozofi ili konkurentno .NET programiranje

Pogledajmo kako istovremeno i paralelno programiranje funkcionira u .Net-u, na primjeru Problema objedovanja filozofa. Plan je ovakav, od sinhronizacije niti/procesa, do modela aktera (u narednim delovima). Članak može biti koristan za prvo upoznavanje ili da osvježite svoje znanje.

Zašto to uopšte raditi? Tranzistori dostiž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. Istovremeno, količina podataka raste, a korisnici očekuju trenutnu reakciju sistema. U takvoj situaciji, "normalno" programiranje, kada imamo jednu izvršnu nit, više nije efikasno. Morate nekako riješiti problem istovremenog ili istovremenog izvršavanja. Štaviše, ovaj problem postoji na različitim nivoima: na nivou niti, na nivou procesa, na nivou mašina u mreži (distribuisani sistemi). .NET ima visokokvalitetne, vremenski testirane tehnologije za brzo i efikasno rješavanje takvih problema.

Cilj

Edsger Dijkstra je postavio ovaj problem svojim studentima još 1965. Utvrđena formulacija je sljedeća. Postoji određeni (obično pet) broj filozofa i isti broj viljuški. Sjede za okruglim stolom, između njih su viljuške. Filozofi mogu jesti iz svojih tanjira beskrajnu hranu, razmišljati ili čekati. Da biste pojeli filozofa, trebate uzeti dvije viljuške (posljednja dijeli viljušku s prvom). Podizanje i spuštanje viljuške su dvije odvojene radnje. Svi filozofi ćute. Zadatak je pronaći takav algoritam da bi svi razmišljali i bili puni i nakon 54 godine.

Prvo, pokušajmo riješiti ovaj problem korištenjem zajedničkog prostora. Viljuške leže na zajedničkom stolu i filozofi ih jednostavno uzimaju kada jesu i vraćaju ih nazad. Ovdje postoje problemi sa sinhronizacijom, kada tačno uzeti sigurne opklade? šta ako nema viljuške? itd. Ali prvo, hajde da počnemo sa 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 je dizajniran da optimizira kreiranje i brisanje niti. Ovo spremište ima red sa zadacima i CLR kreira ili uklanja niti ovisno o broju ovih zadataka. Jedan bazen za sve domene aplikacija. Ovaj bazen treba koristiti gotovo uvijek, jer. nema potrebe da se mučite sa kreiranjem, brisanjem niti, njihovim redovima itd. Moguće je i bez bazena, ali onda ga morate direktno koristiti Thread, ovo je korisno za slučajeve kada trebate promijeniti prioritet niti, kada imamo dugu operaciju, za prednju nit itd.

Drugim riječima, System.Threading.Tasks.Task klasa je ista Thread, ali sa svim vrstama pogodnosti: mogućnost pokretanja zadatka nakon bloka drugih zadataka, vraćanja iz funkcija, prikladnog prekidanja i još mnogo toga. itd. Potrebni su za podršku async/await konstrukcijama (Asinhroni obrazac zasnovan na zadatku, sintaktički šećer za čekanje IO operacija). Pričaćemo o ovome kasnije.

CancelationTokenSource ovdje je to potrebno kako bi nit mogla sama da se završi na signal pozivajuće niti.

Problemi sa sinhronizacijom

Blocked Philosophers

Dobro, znamo da kreiramo teme, hajde da probamo da rucamo:

// Кто какие вилки взял. К примеру: 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 viljušku, pa desnu viljušku, pa ako uspije, onda jedemo i vraćamo ih nazad. Uzimanje jedne vilice je atomsko, tj. dvije niti ne mogu uzeti jednu u isto vrijeme (netačno: prva glasi da je viljuška slobodna, druga - također, prva uzima, druga uzima). Za ovo Interlocked.CompareExchange, koji se mora implementirati sa instrukcijom procesora (TSL, XCHG), koji zaključava dio memorije za atomsko sekvencijalno čitanje i pisanje. A SpinWait je ekvivalentan konstrukciji while(true) samo sa malo "magije" - nit preuzima 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 sekunde) blokirani: svi filozofi uzimaju lijevu viljušku, ali ne i desnu. Niz viljuški tada ima vrijednosti: 1 2 3 4 5.

Dobro hranjeni filozofi ili konkurentno .NET programiranje

Na slici, blokiranje niti (zastoj). Zelena - izvršenje, crvena - sinhronizacija, siva - nit spava. Rombovi označavaju vrijeme početka zadataka.

Glad filozofa

Iako ne treba posebno razmišljati o hrani, ali glad tjera svakoga da odustane od filozofije. Pokušajmo simulirati situaciju izgladnjivanja niti u našem problemu. Gladovanje je kada nit radi, ali bez značajnog rada, drugim riječima, ovo je isti zastoj, samo što sada nit ne spava, već aktivno traži nešto za jelo, ali nema hrane. Kako bismo izbjegli česta blokiranja, vratit ćemo viljušku 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 u vezi s ovim kodom je da dva od četiri filozofa zaborave spustiti lijevu viljušku. I ispostavilo se da jedu više hrane, dok drugi počinju da gladuju, iako niti imaju isti prioritet. Ovdje nisu potpuno gladni, jer. loši filozofi ponekad vraćaju viljuške. Ispostavilo se da dobri ljudi jedu oko 5 puta manje od loših. Dakle, mala greška u kodu dovodi do pada performansi. Ovdje je također vrijedno napomenuti da je moguća rijetka situacija kada svi filozofi zauzmu lijevo račvanje, nema desnog, stave lijevo, čekaju, opet lijevo, itd. Ova situacija je također gladovanje, više kao ćorsokak. Nisam uspeo da to ponovim. Ispod je slika situacije u kojoj su dva loša filozofa uzela obje vilice, a dva dobra gladuju.

Dobro hranjeni filozofi ili konkurentno .NET programiranje

Ovdje možete vidjeti da se niti ponekad probude i pokušavaju dobiti resurs. 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 sa viljuškama u rukama (i tako će ga sahraniti). Tada će komšije ostati bez večere. Možete sami smisliti primjer koda za ovaj slučaj, na primjer, izbačen je NullReferenceException nakon što filozof uzme rašlje. I, usput, izuzetak neće biti obrađen i pozivni kod ga neće tek tako uhvatiti (za ovo AppDomain.CurrentDomain.UnhandledException i sl.). Stoga su potrebni rukovaoci greškama u samim nitima i sa gracioznim završetkom.

Konobar

U redu, kako da riješimo ovaj ćorsokak, problem gladovanja i smrti? Dozvolićemo samo jednom filozofu da dođe do račva, dodati međusobno isključivanje niti za ovo mesto. Kako uraditi? Pretpostavimo da pored filozofa stoji konobar, koji svakom filozofu daje dozvolu da uzme viljuške. Kako da napravimo ovog konobara i kako će ga filozofi postaviti, pitanja su zanimljiva.

Najjednostavniji način je kada će filozofi jednostavno stalno tražiti od konobara pristup viljuškama. One. sada filozofi neće čekati viljušku u blizini, već čekati ili pitati konobara. U početku koristimo samo korisnički prostor za ovo, u njemu ne koristimo prekide za pozivanje bilo koje procedure iz kernela (o njima u nastavku).

Rješenja u korisničkom prostoru

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

Dobro hranjeni filozofi ili konkurentno .NET programiranje

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

Drugo rješenje ovdje bi bilo korištenje samo Interlocked.CompareExchange sa istim aktivnim čekanjem kao što je prikazano u kodu iznad (kod gladnih filozofa), ali to, kao što je već rečeno, teoretski može 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 da izvrši svoje promjene (čitaj 1, čitaj 2, piši 2, piši 1 je loš), može se koristiti za složene promjene jedne vrijednosti (interlocked Anything obrazac) .

Kernel Mode Solutions

Da bismo izbjegli trošenje resursa u petlji, hajde da vidimo kako možemo blokirati nit. Drugim riječima, nastavljajući naš primjer, da vidimo kako konobar uspavljuje filozofa i budi ga samo kada je potrebno. Prvo, pogledajmo kako to učiniti kroz kernel mod operativnog sistema. Sve strukture tamo su često sporije od onih u korisničkom prostoru. Nekoliko puta sporije, na primjer AutoResetEvent možda 53 puta sporije SpinLock [Richter]. Ali uz njihovu pomoć možete sinkronizirati procese u cijelom sistemu, 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 sistem i dvije operacije na njemu, inkrement i dekrement. Ako ne uspije da se smanji, nula, tada je pozivna nit blokirana. Kada se broj poveća nekom drugom aktivnom niti/procesom, tada se niti preskaču i semafor se ponovo smanjuje za proslijeđeni broj. Mogu se zamisliti vozovi u uskom grlu sa semaforom. .NET nudi nekoliko konstrukcija sa sličnim funkcijama: AutoResetEvent, ManualResetEvent, Mutex i sebe Semaphore. Koristićemo AutoResetEvent, ovo je najjednostavnija od ovih konstrukcija: samo dvije vrijednosti 0 i 1 (netačno, istinito). Njen metod WaitOne() blokira pozivnu nit ako je vrijednost bila 0, a ako je 1, snižava je na 0 i preskače. Metoda Set() podiže na 1 i pušta jednog konobara da prođe, koji opet spušta na 0. Djeluje kao okretnica podzemne željeznice.

Zakomplikujmo rješenje i koristimo bravu za svakog filozofa, a ne za sve odjednom. One. sada može postojati nekoliko filozofa odjednom, a ne jedan. Ali ponovo blokiramo pristup stolu kako bismo ispravno, izbjegavajući utrke (uslove utrke), preuzeli opklade.

// Для блокирования отдельного философа.
// Инициализируется: 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 šta se ovdje događa, razmotrite slučaj kada filozof nije uspio uzeti viljuške, tada će njegovi postupci biti sljedeći. Čeka pristup stolu. Pošto ga primi, pokušava da uzme viljuške. Nije išlo. Omogućuje pristup tabeli (međusobno isključivanje). I prolazi pored svoje "okretnice" (AutoResetEvent) (u početku su otvoreni). Ponovo ulazi u ciklus, jer on nema viljuške. Pokušava da ih uzme i zaustavlja se na njegovom "okretu". Neki sretniji komšija s desne ili lijeve strane, nakon što je jeo, otključava našeg filozofa, "otvarajući mu okretnu kapiju". Naš filozof ga prolazi (i zatvara se iza njega) po drugi put. Treći put pokušava da uzme viljuške. Sretno. I on prolazi pored svoje okretnice da večera.

Kada postoje slučajne greške u takvom kodu (one uvijek postoje), na primjer, susjed je pogrešno naveden ili se kreira isti objekat AutoResetEvent za sve (Enumerable.Repeat), onda će filozofi čekati programere, jer Pronalaženje grešaka u takvom kodu je prilično težak zadatak. Drugi problem s ovim rješenjem je taj što ne garantuje da neki filozof neće umrijeti od gladi.

Hybrid Solutions

Pogledali smo dva pristupa mjerenju vremena, kada ostajemo u korisničkom modu i petlji, i kada blokiramo nit kroz kernel. Prva metoda je dobra za kratke pramenove, druga za dugačke. Često je potrebno prvo kratko pričekati da se varijabla promijeni u petlji, a zatim blokirati nit kada je čekanje dugo. Ovaj pristup je implementiran u tzv. hibridne strukture. Evo istih konstrukcija kao i za kernel mod, ali sada s petljom korisničkog načina: SemaphorSlim, ManualResetEventSlim itd. Najpopularniji dizajn ovdje je Monitor, jer u C# postoji dobro poznata lock sintaksa. Monitor ovo je isti semafor sa maksimalnom vrednošću 1 (mutex), ali sa podrškom za čekanje u petlji, rekurziju, obrazac Condition Variable (više o tome u nastavku), itd. Pogledajmo rešenje sa 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 ponovo blokiramo cijelu tabelu za pristup viljuškama, ali sada deblokiramo sve teme odjednom, a ne susjede kada neko završi s jelom. One. prvo neko pojede i blokira komsije, a kada ovaj neko zavrsi, ali hoce odmah da jede, ide u blokadu i budi komsije, jer. vrijeme čekanja je kraće.

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 deblokiran samo komšija, kao u rešenju sa AutoResetEvent, ali razlika ne bi trebala biti velika, jer niti moraju prvo ostati u korisničkom modu.

У lock sintaksa ima gadna iznenađenja. Preporuka za upotrebu Monitor direktno [Richter] [Eric Lippert]. Jedan od njih je to lock uvek van Monitor, čak i ako je postojao izuzetak, u kom slučaju bi druga nit mogla promijeniti stanje dijeljene memorije. U takvim slučajevima je često bolje otići u zastoj ili nekako sigurno prekinuti program. Još jedno iznenađenje je da Monitor koristi blokove sinhronizacije (SyncBlock), koji su prisutni u svim objektima. Stoga, ako je odabran neprikladan objekt, lako možete doći do zastoja (na primjer, ako zaključate internirani niz). Za ovo koristimo uvijek skriveni objekt.

Obrazac Condition Variable vam omogućava da konciznije implementirate očekivanje nekog složenog stanja. U .NET-u je nepotpuna, po mom mišljenju, jer u teoriji, trebalo bi da postoji nekoliko redova na nekoliko varijabli (kao u Posix Threads), a ne na jednom lok. Tada bi se mogli napraviti za sve filozofe. Ali čak i u ovom obliku, omogućava vam da smanjite kod.

mnogi filozofi ili async / await

U redu, sada možemo efikasno blokirati niti. Ali šta ako imamo puno filozofa? 100? 10000? Na primjer, primili smo 100000 zahtjeva ka web serveru. Kreiranje niti za svaki zahtjev bit će prekomjerno, jer toliko niti neće raditi paralelno. Pokrenut će se onoliko koliko ima logičkih jezgara (imam 4). A svi ostali će samo oduzimati resurse. Jedno rješenje za ovaj problem je async/await obrazac. Njegova ideja je da funkcija ne zadržava nit ako treba čekati da se nešto nastavi. I kada nešto uradi, nastavlja sa izvršavanjem (ali ne nužno na istoj niti!). U našem slučaju, sačekaćemo viljušku.

SemaphoreSlim ima za ovo WaitAsync() metoda. Evo implementacije 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 sa async / await je preveden u lukavu državnu mašinu koja odmah vraća svoj interni Task. Preko njega možete čekati završetak metode, otkazati je i sve ostalo što možete učiniti sa Task-om. Unutar metode, državni stroj kontrolira izvršenje. Suština je da ako nema kašnjenja, onda je izvršenje sinhrono, a ako postoji, onda se nit oslobađa. Za bolje razumijevanje ovoga, bolje je pogledati ovaj državni stroj. Od njih možete kreirati lance async / await metode.

Hajde da testiramo. Rad 100 filozofa na mašini sa 4 logička jezgra, 8 sekundi. Prethodno rješenje sa Monitorom je pokretalo samo prve 4 niti, a ostale se uopće nisu pokretale. Svaka od ove 4 niti je bila neaktivna oko 2 ms. A rješenje async/await je pokrenulo svih 100, sa prosječnim čekanjem od 6.8 ​​sekundi svaki. Naravno, u stvarnim sistemima, mirovanje od 6 sekundi je neprihvatljivo i bolje je ne obrađivati ​​ovoliki broj zahtjeva. Pokazalo se da rješenje sa Monitorom uopće nije skalabilno.

zaključak

Kao što možete vidjeti iz ovih malih primjera, .NET podržava mnoge sinkronizacijske konstrukcije. 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š dosta zanimljivih stvari, na primjer, thread-safe kolekcije, TPL Dataflow, reaktivno programiranje, model softverske transakcije itd.

Izvori

izvor: www.habr.com

Dodajte komentar