
Pojďme se podívat na to, jak funguje konkurentní a paralelní programování v .Net na příkladu problému obědvajících filozofů. Plán je takový, že začneme synchronizací vláken/procesů a přejdeme k modelu actorů (v následujících částech). Článek může být užitečný pro první seznámení nebo pro osvěžení znalostí.
Proč se to vůbec učit? Tranzistory dosahují své minimální velikosti, Mooreův zákon se dostává do omezení rychlosti světla a proto růst pozorujeme v počtu tranzistorů, kterých lze vyrobit více. Zároveň roste objem dat a uživatelé očekávají okamžité reakce systémů. V takové situaci je "běžné" programování, kdy máme pouze jeden vykonávající tok, již neefektivní. Je potřeba nějak řešit problém souběžného nebo konkurentního provádění. Tento problém existuje na různých úrovních: na úrovni vláken, na úrovni procesů a na úrovni strojů v síti (distribuované systémy). V .NET existují kvalitní, časem prověřené technologie pro rychlé a efektivní řešení takových úloh.
Úkol
Edsger Dijkstra tuto problematiku kladl svým studentům už v roce 1965. Ustanovená formulace je následující. Existuje určité (obvykle pět) množství filozofů a stejný počet vidlic. Sedí kolem kulatého stolu, vidlice jsou mezi nimi. Filozofové mohou jíst ze svých talířů s nekonečným množstvím jídla, přemýšlet nebo čekat. Aby filozof mohl jíst, musí vzít dvě vidlice (poslední sdílí vidlici s prvním). Vzít a položit vidlici jsou dvě samostatné akce. Všichni filozofové jsou tichí. Úkolem je najít takový algoritmus, aby všichni přemýšleli a byli sytí i po 54 letech.
Nejprve se pokusíme vyřešit tento problém pomocí sdíleného prostoru. Vidlice leží na společném stole a filozofové je jednoduše berou, když jsou k dispozici, a pokládají zpět. Zde se objevují problémy se synchronizací, kdy přesně vzít vidlice? Co dělat, když vidlice nejsou k dispozici? atd. Ale nejprve spusťme filozofy.
Pro spuštění vláken použijeme pool vláken přes Task.Run metodu:
var cancelTokenSource = new CancellationTokenSource();
Action create = (i) => RunPhilosopher(i, cancelTokenSource.Token);
for (int i = 0; i create(icopy), cancelTokenSource.Token);
}Pool vláken byl vytvořen pro optimalizaci vytváření a mazání vláken. Tento pool má frontu úloh a CLR vytváří nebo maže vlákna podle počtu těchto úloh. Jeden pool pro všechny AppDomainy. Tento pool by měl být používán téměř vždy, protože se nemusíte zabývat vytvářením, mazáním vláken, jejich frontami atd. Můžete fungovat i bez poolu, ale musíte pak používat přímo Vlákno, to je rozumné pro případy, kdy potřebujete změnit prioritu vláknu, když máme dlouhou operaci, pro foreground vlákno atd.
Jinými slovy, System.Threading.Tasks.Task třída — to je stejné Vlákno, ale se všemi možnými výhodami: možnost spouštět úkol po bloku dalších úkolů, vracet je z funkcí, pohodlně je přerušovat atd. Jsou potřebné pro podporu async/await konstrukcí (Task-based Asynchronous Pattern, syntaktický cukr pro čekání na IO operaci). O tom si ještě promluvíme.
CancelationTokenSource je zde potřebný, aby vlákno mohlo samo skončit na signál vyvolávajícího vlákna.
Problémy se synchronizací
Zablokovaní filozofové
Dobře, umíme vytvářet vlákna, pojďme si zkusit posnídat:
// Кто какие вилки взял. К примеру: 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();
}
}Nejprve se pokusíme vzít levou a poté pravou vidlici a pokud se to podaří, tak jíme a vracíme je zpět. Vzít jednu vidlici je atomární, t.j. dvě vlákna ji nemohou vzít současně (chybně: první čte, že vidlice je volná, druhý — také, první bere, druhý bere). K tomu Interlocked.CompareExchange, který by měl být implementován pomocí instrukce procesoru (TSL, XCHG), která zablokuje část paměti pro atomární sekvenční čtení a zápis. A SpinWait je ekvivalentním konstrukci while(true) pouze s malou „magie“ — vlákno zabírá procesor (Thread.SpinWait), ale někdy předává řízení jinému vláknu (Thread.Yeild) nebo usíná (Thread.Sleep).
Ale toto řešení nefunguje, protože se vlákna brzy (u mě během vteřiny) zablokují: všichni filozofové berou svou levou vidlici, ale pravou ne. Pole forks pak má hodnoty: 1 2 3 4 5.

Na obrázku je blokování vláken (deadlock). Zelenou barvou — provádění, červenou — synchronizace, šedou — vlákno spí. Kosočtverci označují čas spuštění úloh.
Hlad filozofů
Abychom přemýšleli, nepotřebujeme až tolik jídla, avšak hlad může kohokoliv donutit opustit filozofii. Zkusme si modelovat situaci hladu vláken v našem úkolu. Hlad je situace, kdy vlákno běží, ale neprovádí žádnou podstatnou práci; jinými slovy jde o stejný deadlock, jen tentokrát vlákno nespí, ale aktivně se snaží najít něco k jídlu, ale nic není. Abychom se vyhnuli častému blokování, budeme vracet vidličku zpět, pokud se nám nepodaří vzít další.
// То же что и в 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)
);
}V tomto kódu je důležité, že dva ze čtyř filozofů zapomínají vrátit svou levou vidličku. A tak se stává, že jedí více, zatímco ostatní začínají hladovět, i když mají vlákna stejnou prioritu. Zde vlastně nehladoví, protože špatní filozofové občas vrací své vidličky zpět. Mám pocit, že dobří jedí asi pětkrát méně než špatní. Takže malá chyba v kódu způsobuje pokles výkonu. Je také třeba poznamenat, že může nastat vzácná situace, kdy všichni filozofové vezmou levou vidličku, pravou ne, položí levou, čekají, znovu vezmou levou atd. Tato situace je také hladovění, více připomínající vzájemné blokování. Nepodařilo se mi ji zopakovat. Níže je obrázek pro situaci, kdy dva špatní filozofové vzali obě vidličky, zatímco dva dobří hladoví.

Zde je vidět, že se občas vlákna probouzejí a pokoušejí se získat zdroj. Dva jádra ze čtyř nic nedělají (zelená křivka nahoře).
Smrt filozofa
A ještě jeden problém, který může přerušit slavný oběd filozofů — je to, pokud jeden z nich náhle umře s vidličkami v rukou (a tak ho i pohřbí). Potom sousedé zůstanou bez oběda. Můžete si sami vymyslet příklad kódu pro tuto situaci, například vyhazuje NullReferenceException poté, co filozof vezme vidličky. A mimochodem, tato výjimka bude neošetřená a volající kód ji jednoduše nezachytí (k tomu AppDomain.CurrentDomain.UnhandledException a další). Proto jsou zpracovatelé chyb nezbytní uvnitř samotných vláken a s korektním ukončením.
Číšník
Dobře, jak vyřešíme tento problém s vzájemným blokováním, hladem a úmrtími? Budeme povolovat pouze jednoho filozofa k vidlicím, přidáme vzájemné vyloučení (mutual exclusion) vláken pro toto místo. Jak to udělat? Předpokládejme, že vedle filozofů je číšník, který dává povolení nějakému jednomu filozofovi vzít vidlice. Jak vytvoříme tohoto číšníka a jak budou filozofové žádat o jeho pomoc, to jsou zajímavé otázky.
Nejjednodušší způsob je, když filozofové budou neustále žádat číšníka, aby jim dal přístup k vidlicím. Tj. nyní filozofové nebudou čekat na vidlici vedle sebe, ale nebudou čekat nebo prosit číšníka. Zpočátku použijeme pouze User Space, v něm nepoužíváme přerušení pro volání nějakých procedur z jádra (o nich níže).
Řešení v uživatelském prostoru
Zde uděláme to samé, co jsme dříve dělali s jednou vidlicí a dvěma filozofy, budeme se točit v cyklu a čekat. Ale nyní to budou všichni filozofové a jakoby pouze jedna vidlice, t.j. lze říci, že existuje pouze ten filozof, který vzal tuto „zlatou vidlici“ od číšníka. K tomu použijeme SpinLock.
private static SpinLock spinLock = new SpinLock(); // Náš "číšník"
private void RunSpinLock(int i, CancellationToken token)
{
while (true)
{
// Vzájemné blokování pomocí busy waiting. Voláme před try, abychom
// vyhodili výjimku v případě chyby přímo ve SpinLock.
bool hasLock = false;
spinLock.Enter(ref hasLock);
try
{
// Zde může být pouze jeden vlak (vzájemné vyloučení).
forks[Left(i)] = i + 1; // Vezmeme vidlici okamžitě, bez čekání.
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(); // Vyhnout se problému se smrtí filozofa.
}
Think(i);
if (token.IsCancellationRequested)
break;
}
}SpinLock je blokátor, s, zjednodušeně řečeno, tím samým while(true) { if (!lock) break; }, ale s ještě větší „magie“, než v SpinWait (který se tam používá). Nyní umí počítat čekající, trochu je uspat a mnohem více. Zkrátka dělá vše pro optimalizaci. Ale pamatujte, že to je stále ten samý aktivní cyklus, který spotřebovává procesorové zdroje a drží tok, což může vést k hladovění, pokud se jeden z filozofů stane důležitějším než ostatní, ale nemá zlatý příbor (problém prioritního převrácení). Proto jej používáme pouze pro velmi krátké úpravy ve sdílené paměti, bez jakýchkoli externích volání, vnořených zámků a jiných překvapení.

Obrázek pro SpinLock. Toky neustále „bojují“ o zlatý příbor. Dochází k propadům — na obrázku zvýrazněná oblast. Jádra nejsou plně využívána: pouze asi 2/3 těmito čtyřmi toky.
Dalším řešením by zde bylo používat pouze Interlocked.CompareExchange se stejným aktivním čekáním, jak je znázorněno v výše uvedeném kódu (u hladovějících filozofů), ale to může, jak již bylo zmíněno, teoreticky vést k zablokování.
O Interlocked je třeba říct, že tam nejsou jen CompareExchange, ale i další metody pro atomové čtení a zápis. A prostřednictvím opakování změn v případě, že jiný tok stihne provést své změny (čtení 1, čtení 2, zápis 2, zápis 1 špatné), může být použit pro složité změny jedné hodnoty (Interlocked Anything vzor).
Řešení v jádrovém režimu
Aby se zabránilo ztrátě zdrojů v cyklu, podívejme se, jak můžeme zablokovat tok. Jinými slovy, pokračujme v našem příkladu a podívejme se, jak číšník uspat filozofa a probudit ho pouze tehdy, když je třeba. Nejprve se podívejme, jak to provést prostřednictvím jádrového režimu operačního systému. Všechny struktury tam často bývají pomalejší než ty, které jsou v uživatelském prostoru. Pomalejší několikrát, například AutoResetEvent může být 53krát pomalejší SpinLock [Richter]. Ale s jejich pomocí lze synchronizovat procesy po celé systému, ať už jsou řízené nebo ne.
Hlavní konstrukcí zde je semafor, který navrhl Dijkstra před více než půl stoletím. Semafor je, zjednodušeně řečeno, kladné celé číslo, spravované systémem, a dvě operace na něm - zvýšit a snížit. Pokud není možné snížit na nulu, vyvolávající vlákno se zablokuje. Když je číslo zvýšeno nějakým jiným aktivním vláknem/procesem, vlákna jsou propouštěna a semafor se opět snižuje o počet, který prošel. Můžeme si to představit jako vlaky na úzkém místě se semaforem. .NET nabízí několik konstrukcí s podobnými funkcemi: AutoResetEvent, ManualResetEvent, Mutex a sám Semaphore. Budeme používat AutoResetEvent, to je nejjednodušší z těchto konstrukcí: má pouze dvě hodnoty 0 a 1 (false, true). Její metoda WaitOne() zablokuje vyvolávající vlákno, pokud byla hodnota 0, a pokud 1, sníží ji na 0 a propustí ho. A metoda Set() zvyšuje na 1 a propouští jedno čekající, které opět snižuje na 0. Funguje jako turniket v metru.
Zkomplikujeme řešení a budeme používat zámek pro každého filosofa, nikoli pro všechny najednou. Tj. nyní může být několik filosofů současně, nikoli jen jeden. Ale opět blokujeme přístup ke stolu, aby bylo možné správně, bez závodních podmínek, vzít vidlice.
// Для блокирования отдельного философа.
// Инициализируется: 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();
}Abychom pochopili, co se zde děje, zvažme případ, kdy se filosofovi nepodařilo vzít vidlice, pak jeho činnosti budou následující. Čeká na přístup ke stolu. Když ho dostane, pokusí se vzít vidlice. Nepodařilo se. Vrátí přístup ke stolu (vzájemné vyloučení). A projde svým „turniketem“ (AutoResetEvent) (na začátku jsou otevřené). Znovu vstupuje do cyklu, protože nemá vidlice. Pokouší se je vzít a zastaví se u svého „turniketu“. Nějaký šťastnější soused vpravo nebo vlevo, který skončil s jídlem, odblokuje našeho filosofa, „otevírá jeho turniket“. Náš filosof projde jím (a on se za ním zavře) podruhé. Pokouší se potřetí vzít vidlice. Úspěšně. A projde svým turniketem, aby obědval.
Když se v takovém kódu vyskytnou náhodné chyby (což se vždy stává), například když bude nesprávně určen soused nebo vytvořen stejný objekt AutoResetEvent pro všechny (Enumerable.Repeat), pak budou filosofové čekat na vývojáře, protože hledání chyb v takovém kódu je poměrně složité. Dalším problémem s tímto řešením je, že nezaručuje, že nějaký filosof nezačne hladovět.
Hybridní řešení
Zkoumali jsme dva přístupy k synchronizaci: když zůstáváme v uživatelském režimu a točíme se v cyklu, a také když blokujeme proud přes jádro. První metoda je dobrá pro krátké blokace, druhá pro dlouhé. Často je potřeba nejprve krátce čekat na změnu proměnné v cyklu a pak zablokovat proud, když je čekání dlouhé. Tento přístup je realizován v takzvaných hybridních konstrukcích. Zde jsou stejné konstrukce, jaké byly pro režim jádra, ale nyní s cyklem v uživatelském režimu: SemaphoreSlim, ManualResetEventSlim atd. Nejpopulárnější konstrukcí zde je Monitor, protože v C# je známý všem lock syntaktikou. Monitor Je to ten samý semafor s maximální hodnotou 1 (mutex), ale s podporou čekání v cyklu, rekurze, vzoru Condition Variable (o kterém níže) a další. 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 vidlicím, ale nyní odblokujeme všechny proudy najednou, nikoli sousedy, když někdo skončí jíst. Tedy nejprve někdo jí a blokuje sousedy, a když ten někdo skončí, ale chce hned zase jíst, odejde do blokace a probudí své sousedy, protože jeho čas čekání je kratší.
Tak se vyhýbáme deadlockům a hladu nějakého filozofa. Používáme cyklus pro krátké čekání a blokujeme proud pro dlouhé. Odblokování najednou všech pracuje pomaleji, než kdyby bylo odblokování jen souseda, jako v řešení s AutoResetEvent, ale rozdíl by neměl být velký, protože proudy by měly nejprve zůstat v uživatelském režimu.
V lock Syntaktika má nepříjemná překvapení. Doporučuje se používat Monitor přímo [Richter] [Eric Lippert]. Jedním z nich je, že lock vždy vyjde z Monitor, i když došlo k výjimce, a pak jiný proud může změnit stav sdílené paměti. V takových případech je často lepší jít do deadlocku nebo nějak bezpečně ukončit program. Další překvapení je, že Monitor používá synchronizační bloky (SyncBlock), které jsou ve všech objektech. Proto, pokud je vybrán nevhodný objekt, může snadno dojít k deadlocku (například pokud uděláte lock na internovanou řetězce). Vždy používejte skrytý objekt pro toto.
Pattern Condition Variable umožňuje efektivněji implementovat čekání na složité podmínky. V .NET je nedokonalý, podle mého názoru, protože by mělo být více front na různých proměnných (jako v Posix Threadech), a ne jen na jednom zámku. Pak by bylo možné vytvořit je pro všechny filozofy. Ale i v této podobě umožňuje zkrátit kód.
Mnoho filozofů nebo async / await
Dobře, nyní umíme efektivně blokovat vlákna. Ale co když máme hodně filozofů? 100? 10000? Například jsme obdrželi 100000 požadavků na webový server. Vytvořit vlákno pro každý požadavek by bylo příliš nákladné, protože tolik vláken nebude moci běžet současně. Poběží jen tolik, kolik je logických jader (mám 4). A všechna ostatní by jen brala zdroje. Jedno z řešení tohoto problému je asynchronní patter async / await. Myšlenka je taková, že funkce neuvolňuje vlákno, pokud pro její pokračování potřebuje něco čekat. A když se tohle něco stane, funkce obnoví své provádění (ale nemusí nutně ve stejném vlákně!). V našem případě budeme čekat na vidlice.
SemaphoreSlim má pro to WaitAsync() metodu. 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 se překládá do šikovného konečného automatu, který okamžitě vrací svůj vnitřní Úkol. Skrze něj lze čekat na dokončení metody, zrušit ji a dělat vše ostatní, co lze dělat s Task. Uvnitř metody konečný automat kontroluje provádění. Podstatou je to, že pokud není zpoždění, je provádění synchronní, a pokud je, vlákno se uvolní. Pro lepší pochopení je nejlepší podívat se na tento konečný automat. Lze vytvářet řetězce z těchto async / await metod.
Otestujeme. Práce 100 filozofů na stroji se 4 logickými jádry, 8 sekund. Předchozí řešení s Monitor fungovalo pouze na první 4 vlákna, ostatní se vůbec nevykonávala. Každé z těchto 4 vláken bylo v průměru zablokováno asi 2ms. A řešení s async / await vykonalo všech 100, přičemž v průměru každé čekalo 6.8 sekund. Samozřejmě, v reálných systémech je zpoždění 6 sekund nepřijatelné a bylo by lepší nezpracovávat tolik požadavků tímto způsobem. Řešení s Monitor se ukázalo jako zcela neskalovatelné.
Závěr
Jak je vidět z těchto několika příkladů, .NET podporuje mnoho synchronizačních konstrukcí. Není vždy zřejmé, jak je využít. Doufám, že tento článek byl užitečný. Na tomto místě končíme, ale je tu ještě hodně zajímavého, například kolekce s bezpečným přístupem k vláknům, TPL Dataflow, reaktivní programování, model softwarových transakcí a další.
Zdroje
- Vizualizace vláken:
- MSDN: , a mnohé další.
- [Richter] - CLR via C#, Jeffrey Richter
- [Eric Lippert] -
- Obrázek – „Tanec mezi meči“, G. Semiradský
Zdroj: habr.com
