Dobře živení filozofové nebo konkurenční .NET programování

Dobře živení filozofové nebo konkurenční .NET programování

Podívejme se, jak funguje souběžné a paralelní programování v .Net, na příkladu Philosophers Dining Problem. Plán je takový, od synchronizace vláken / procesů až po model aktéra (v následujících částech). Článek může být užitečný pro první seznámení nebo pro osvěžení vašich znalostí.

Proč to vůbec dělat? Tranzistory dosahují své minimální velikosti, Moorův zákon spočívá na omezení rychlosti světla a proto je pozorován nárůst počtu, lze vyrobit více tranzistorů. Zároveň roste množství dat a uživatelé očekávají okamžitou odezvu systémů. V takové situaci už není „normální“ programování, kdy máme jedno spouštějící vlákno, efektivní. Musíte nějak vyřešit problém současného nebo souběžného provádění. Navíc tento problém existuje na různých úrovních: na úrovni vláken, na úrovni procesů, na úrovni strojů v síti (distribuované systémy). .NET má kvalitní, časem prověřené technologie pro rychlé a efektivní řešení takových problémů.

Úkol

Edsger Dijkstra položil tento problém svým studentům již v roce 1965. Zavedená formulace je následující. Existuje určitý (většinou pět) počet filozofů a stejný počet fórků. Sedí u kulatého stolu, mezi nimi vidličky. Filozofové mohou jíst ze svých talířů nekonečné jídlo, přemýšlet nebo čekat. Chcete-li jíst filozofa, musíte si vzít dvě vidličky (poslední sdílí vidličku s první). Zvednutí a položení vidlice jsou dvě samostatné akce. Všichni filozofové mlčí. Úkolem je najít takový algoritmus, aby všichni mysleli a byli plní i po 54 letech.

Nejprve se pokusme tento problém vyřešit pomocí sdíleného prostoru. Vidličky leží na společném stole a filozofové je prostě vezmou, když jsou, a položí je zpátky. Zde jsou problémy se synchronizací, kdy přesně brát surebets? co když tam není vidlička? atd. Nejprve ale začněme filozofy.

Ke spuštění vláken používáme fond vláken skrz 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);
}

Fond vláken je navržen tak, aby optimalizoval vytváření a odstraňování vláken. Tento fond má frontu s úlohami a CLR vytváří nebo odebírá vlákna v závislosti na počtu těchto úloh. Jeden fond pro všechny domény AppDomains. Tento bazén by měl být používán téměř vždy, protože. není třeba se obtěžovat vytvářením, mazáním vláken, jejich front atd. Jde to i bez fondu, ale pak ho musíte použít přímo Thread, je to užitečné v případech, kdy potřebujete změnit prioritu vlákna, když máme dlouhou operaci, u vlákna v popředí atd.

Jinými slovy, System.Threading.Tasks.Task třída je stejná Thread, ale s nejrůznějšími vymoženostmi: možnost spustit úlohu po bloku jiných úloh, vrátit je z funkcí, pohodlně je přerušit a další. atd. Jsou potřeba pro podporu asynchronních /čekacích konstrukcí (task-based asynchronous pattern, syntaktický cukr pro čekání na IO operace). Promluvíme si o tom později.

CancelationTokenSource zde je potřeba, aby se vlákno mohlo ukončit na signál volajícího vlákna.

Problémy se synchronizací

Blokovaní filozofové

Dobře, víme, jak vytvářet vlákna, zkusme si dát oběd:

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

Tady nejdřív zkoušíme vzít levou vidličku a pak pravou, a když to vyjde, tak sníme a dáme je zpátky. Odběr jedné vidličky je atomový, tzn. dvě vlákna nemohou vzít jedno současně (nesprávně: první čte, že vidlice je volná, druhá - taky, první bere, druhá bere). Pro tohle Interlocked.CompareExchange, který musí být implementován pomocí instrukce procesoru (TSL, XCHG), který uzamkne část paměti pro atomické sekvenční čtení a zápis. A SpinWait je ekvivalentní konstruktu while(true) jen s trochou "kouzla" - vlákno zabere procesor (Thread.SpinWait), ale někdy přenese řízení na jiné vlákno (Thread.Yeild) nebo usne (Thread.Sleep).

Ale toto řešení nefunguje, protože toky se brzy (pro mě během vteřiny) zablokují: všichni filozofové berou levou vidličku, ale ne pravou. Pole vidlic pak má hodnoty: 1 2 3 4 5.

Dobře živení filozofové nebo konkurenční .NET programování

Na obrázku blokování vláken (zablokování). Zelená - provedení, červená - synchronizace, šedá - vlákno spí. Kosočtverce označují čas zahájení úkolů.

Hlad filozofů

Sice není nutné myslet hlavně na jídlo, ale hlad nutí každého vzdát filozofii. Pokusme se nasimulovat situaci hladovění vláken v našem problému. Hladovění je, když vlákno běží, ale bez významné práce, jinými slovy, toto je stejné zablokování, pouze vlákno nyní nespí, ale aktivně hledá něco k jídlu, ale není žádné jídlo. Abychom předešli častému zablokování, vložíme vidlici zpět, pokud jsme nemohli vzít jinou.

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

Důležité na tomto kódu je, že dva ze čtyř filozofů zapomenou položit levou vidličku. A ukázalo se, že jedí více jídla, zatímco jiní začnou hladovět, ačkoli vlákna mají stejnou prioritu. Tady nehladoví úplně, protože. špatní filozofové někdy odkládají vidličky zpět. Ukazuje se, že dobří lidé jedí asi 5x méně než ti špatní. Malá chyba v kódu tedy vede k poklesu výkonu. Zde také stojí za zmínku, že je možná ojedinělá situace, kdy všichni filozofové vezmou vidličku levou, neexistuje žádná pravá, dají levou, počkají, znovu vezmou levou atd. Tato situace je také hladověním, spíše mrtvou situací. Nepodařilo se mi to zopakovat. Níže je obrázek pro situaci, kdy dva špatní filozofové vzali obě vidličky a dva dobří hladoví.

Dobře živení filozofové nebo konkurenční .NET programování

Zde můžete vidět, že vlákna se někdy probudí a pokusí se získat zdroj. Dvě ze čtyř jader nedělají nic (zelený graf výše).

Smrt filozofa

No a dalším problémem, který může přerušit slavnou večeři filozofů, je, když jeden z nich náhle zemře s vidličkami v rukou (a oni ho tak pohřbí). Sousedé pak zůstanou bez oběda. Vzorový kód pro tento případ si můžete vymyslet sami, například je vyhozen NullReferenceException poté, co filozof vezme vidličky. A mimochodem, výjimka nebude zpracována a volací kód ji jen tak nezachytí (pro toto AppDomain.CurrentDomain.UnhandledException atd.). Proto jsou potřebné obslužné rutiny chyb v samotných vláknech a s elegantním ukončením.

Číšník

Dobře, jak vyřešíme tuto patovou situaci, hladovění a smrt? Dovolíme pouze jednomu filozofovi, aby dosáhl na vidličky, přidáme vzájemné vyloučení vláken pro toto místo. Jak to udělat? Předpokládejme, že vedle filozofů stojí číšník, který dává povolení kterémukoli filozofovi vzít vidličky. Jak toho číšníka uděláme a jak se ho budou ptát filozofové, otázky jsou zajímavé.

Nejjednodušší způsob je, když se filozofové prostě budou neustále ptát číšníka na přístup k vidličkám. Tito. nyní filozofové nebudou čekat na vidličku poblíž, ale čekat nebo se zeptat číšníka. Nejprve k tomu používáme pouze User Space, v něm nepoužíváme přerušení k volání žádných procedur z jádra (o nich níže).

Řešení v uživatelském prostoru

Tady uděláme to samé, co jsme dělali s jednou vidličkou a dvěma filozofy, budeme se točit v cyklu a čekat. Ale teď to budou všichni filozofové a jakoby jen jeden fórek, tzn. dá se říci, že jíst bude jen filozof, který tuto „zlatou vidličku“ od číšníka vzal. K tomu používáme 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 toto je blokátor, zhruba řečeno stejný while(true) { if (!lock) break; }, ale s ještě větším „kouzlem“ než v SpinWait (který se tam používá). Nyní ví, jak spočítat čekající, trochu je uspat a další. atd. Obecně dělá vše pro optimalizaci. Musíme si ale pamatovat, že je to stále stejný aktivní cyklus, který spotřebovává zdroje procesoru a udržuje tok, což může vést k hladovění, pokud se jeden z filozofů stane prioritnějším než ostatní, ale nemá zlatou vidlici (problém s inverzí priority) . Proto jej používáme pouze pro velmi krátké změny ve sdílené paměti, bez jakýchkoli volání třetích stran, vnořených zámků a dalších překvapení.

Dobře živení filozofové nebo konkurenční .NET programování

Kreslení pro SpinLock. Potoky neustále „bojují“ o zlatou vidličku. Existují poruchy - na obrázku vybraná oblast. Jádra nejsou plně využita: pouze asi ze 2/3 těmito čtyřmi vlákny.

Dalším řešením by zde bylo použít pouze Interlocked.CompareExchange se stejným aktivním čekáním, jako je uvedeno v kódu výše (u hladovějících filozofů), ale to, jak již bylo řečeno, by teoreticky mohlo vést k zablokování.

O Interlocked Je třeba poznamenat, že neexistuje pouze CompareExchange, ale také další metody pro atomické čtení A zápis. A díky opakování změny v případě, že má jiné vlákno čas provést své změny (čtení 1, čtení 2, zápis 2, zápis 1 je špatný), lze jej použít pro komplexní změny jedné hodnoty (vzor Interlocked Anything) .

Řešení režimu jádra

Abychom se vyhnuli plýtvání zdroji ve smyčce, podívejme se, jak můžeme zablokovat vlákno. Jinými slovy, pokračujme v našem příkladu, podívejme se, jak číšník uspí filozofa a probudí ho, jen když je to nutné. Nejprve se podívejme, jak to udělat prostřednictvím režimu jádra operačního systému. Všechny struktury tam jsou často pomalejší než ty v uživatelském prostoru. Například několikrát pomaleji AutoResetEvent možná 53krát pomalejší SpinLock [Richter]. Ale s jejich pomocí můžete synchronizovat procesy v celém systému, ať už spravované nebo ne.

Základním konstruktem je zde semafor navržený Dijkstrou před více než půl stoletím. Semafor je zjednodušeně řečeno kladné celé číslo spravované systémem a dvě operace s ním, inkrementace a dekrementace. Pokud se nepodaří snížit, nula, pak je volající vlákno zablokováno. Když je číslo zvýšeno nějakým jiným aktivním vláknem/procesem, pak jsou vlákna přeskočena a semafor se opět sníží o předané číslo. Lze si představit vlaky v úzkém hrdle se semaforem. .NET nabízí několik konstrukcí s podobnou funkčností: AutoResetEvent, ManualResetEvent, Mutex a já Semaphore. budeme používat AutoResetEvent, jedná se o nejjednodušší z těchto konstrukcí: pouze dvě hodnoty 0 a 1 (false, true). Její metoda WaitOne() blokuje volající vlákno, pokud byla hodnota 0, a pokud 1, sníží jej na 0 a přeskočí. Metoda Set() zvýší na 1 a nechá projít jednoho číšníka, který opět sníží na 0. Funguje jako turniket v metru.

Zkomplikujme řešení a použijme zámek pro každého filozofa, a ne pro všechny najednou. Tito. nyní může být několik filozofů najednou a ne jeden. Ale opět zablokujeme přístup ke stolu, abychom správně, vyhýbání se závodům (podmínkám závodu), brali 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();
}

Abyste pochopili, co se zde děje, zvažte případ, kdy se filozofovi nepodařilo vzít vidličky, pak budou jeho činy následující. Čeká na přístup ke stolu. Když ji obdrží, pokusí se vzít vidličky. Nevyšlo to. Umožňuje přístup k tabulce (vzájemné vyloučení). A projde svým "turniketem" (AutoResetEvent) (jsou zpočátku otevřeny). Znovu se dostává do koloběhu, protože nemá vidličky. Snaží se je vzít a zastaví se u svého „turniketu“. Nějaký šťastnější soused vpravo nebo vlevo, když dojedl, odemkne našeho filozofa a „otevře svůj turniket“. Náš filozof ji míjí (a zavře se za ní) podruhé. Potřetí se pokouší vzít vidličky. Hodně štěstí. A předá svůj turniket na večeři.

Pokud jsou v takovém kódu náhodné chyby (existují vždy), například je nesprávně zadán soused nebo je vytvořen stejný objekt AutoResetEvent pro všechny (Enumerable.Repeat), pak budou filozofové čekat na vývojáře, protože Hledání chyb v takovém kódu je poměrně obtížný úkol. Dalším problémem tohoto řešení je, že nezaručuje, že nějaký filozof nebude hladovět.

Hybridní řešení

Podívali jsme se na dva přístupy k časování, kdy zůstáváme v uživatelském režimu a smyčce a kdy blokujeme vlákno v jádře. První metoda je dobrá pro krátké zámky, druhá pro dlouhé. Často je nutné nejprve krátce počkat, až se proměnná změní ve smyčce, a poté vlákno zablokovat, když je čekání dlouhé. Tento přístup je implementován v tzv. hybridní struktury. Zde jsou stejné konstrukce jako pro režim jádra, ale nyní se smyčkou uživatelského režimu: SemaphorSlim, ManualResetEventSlim atd. Zde je nejoblíbenější design Monitor, protože v C# je dobře známý lock syntax. Monitor jedná se o stejný semafor s maximální hodnotou 1 (mutex), ale s podporou čekání ve smyčce, rekurze, vzor Condition Variable (více níže) atd. Podívejme se na řešení s ním.

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

Zde opět blokujeme celý stůl pro přístup k vidličkám, ale nyní odblokujeme všechna vlákna najednou a ne sousedy, když někdo dojí. Tito. nejprve někdo sní a zablokuje sousedy, a když tento někdo dojede, ale chce hned zase jíst, jde do blokování a probudí sousedy, protože. jeho čekací doba je kratší.

Tak se vyhneme patové situaci a vyhladovění nějakého filozofa. Na krátké čekání použijeme smyčku a na dlouhé zablokujeme vlákno. Odblokování všech najednou je pomalejší, než kdyby se odblokoval jen soused, jako v řešení s AutoResetEvent, ale rozdíl by neměl být velký, protože vlákna musí nejprve zůstat v uživatelském režimu.

У lock syntaxe přináší nemilá překvapení. Doporučuji používat Monitor přímo [Richter] [Eric Lippert]. Jedním z nich je to lock vždy mimo Monitor, i když došlo k výjimce, v takovém případě by jiné vlákno mohlo změnit stav sdílené paměti. V takových případech je často lepší přejít do mrtvého bodu nebo nějakým způsobem bezpečně ukončit program. Dalším překvapením je, že Monitor používá synchronizační bloky (SyncBlock), které jsou přítomny ve všech objektech. Pokud je tedy vybrán nevhodný objekt, můžete se snadno dostat do mrtvého bodu (například když zamknete vložený řetězec). K tomu používáme vždy skrytý objekt.

Vzor Condition Variable umožňuje stručněji implementovat očekávání nějaké složité podmínky. V .NET je to podle mého názoru neúplné, protože teoreticky by mělo existovat několik front na několika proměnných (jako v Posix Threads), a ne na jednom lokaci. Pak je mohl udělat pro všechny filozofy. Ale i v této podobě umožňuje zmenšit kód.

mnoho filozofů popř async / await

Dobře, teď můžeme efektivně blokovat vlákna. Ale co když máme hodně filozofů? 100? 10000 100000? Například jsme obdrželi 4 XNUMX požadavků na webový server. Vytvoření vlákna pro každý požadavek bude režie, protože tolik vláken nepoběží paralelně. Poběží pouze tolik, kolik je logických jader (mám XNUMX). A všichni ostatní budou jen brát zdroje. Jedním z řešení tohoto problému je vzor async / wait. Jeho myšlenkou je, že funkce nedrží vlákno, pokud potřebuje čekat, až něco bude pokračovat. A když něco udělá, obnoví své provádění (ale ne nutně ve stejném vlákně!). V našem případě počkáme na vidličku.

SemaphoreSlim má na to WaitAsync() metoda. Zde je implementace pomocí tohoto vzoru.

// Запуск такой же, как раньше. Где-нибудь в программе:
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 je přeložen do složitého stavového automatu, který okamžitě vrátí své vnitřní Task. Prostřednictvím něj můžete čekat na dokončení metody, zrušit ji a vše ostatní, co můžete s Task dělat. Uvnitř metody stavový automat řídí provádění. Pointa je, že pokud nedojde k žádné prodlevě, pak je provádění synchronní, a pokud ano, pak se vlákno uvolní. Pro lepší pochopení je lepší podívat se na tento stavový automat. Z nich můžete vytvořit řetězy async / await metody.

Pojďme otestovat. Práce 100 filozofů na stroji se 4 logickými jádry, 8 sekund. Předchozí řešení s Monitorem spouštělo pouze první 4 vlákna a zbytek neběžel vůbec. Každé z těchto 4 vláken bylo nečinné asi 2 ms. A řešení async / wait běželo všech 100, s průměrným čekáním každého 6.8 sekundy. Samozřejmě v reálných systémech je nečinnost po dobu 6 sekund nepřijatelná a je lepší takové množství požadavků nezpracovávat. Ukázalo se, že řešení s Monitorem není vůbec škálovatelné.

Závěr

Jak můžete vidět z těchto malých příkladů, .NET podporuje mnoho synchronizačních konstrukcí. Ne vždy je však zřejmé, jak je používat. Doufám, že vám tento článek pomohl. Pro tuto chvíli je to konec, ale stále zbývá spousta zajímavých věcí, například thread-safe kolekce, TPL Dataflow, Reactive programming, Software Transaction model atd.

zdroje

Zdroj: www.habr.com

Přidat komentář