
Poglejmo, kako deluje konkurenčno in vzporedno programiranje v .Net, na primeru problema obsedelih filozofov. Načrt je takšen: od sinhronizacije niti/procesov do modela igralcev (v naslednjih delih). Članek je lahko koristen za prvo srečanje ali za osvežitev znanja.
Zakaj je to sploh pomembno? Tranzistorji dosegajo svojo minimalno velikost, Moorejev zakon se srečuje z omejitvijo hitrosti svetlobe in zato rast opazimo v številu, tranzistorje lahko izdelamo več. Hkrati pa se količina podatkov povečuje, uporabniki pa pričakujejo takojšnje reakcije sistemov. V taki situaciji »običajno« programiranje, kjer imamo en izvajalni tok, ni več učinkovito. Potrebno je rešiti problem sočasnega ali konkurenčnega izvajanja. Problem obstaja na različnih ravneh: na ravni niti, na ravni procesov, na ravni računalnikov v omrežju (porazdeljeni sistemi). V .NET so kvalitativne, časovno preizkušene tehnologije za hitro in učinkovito reševanje teh nalog.
Naloga
Edsger Dijkstra je to težavo predstavljal svojim študentom že leta 1965. Uveljavljen izraz je takšen. Obstaja določeno (običajno pet) število filozofov in enako število vilic. Sede za okroglo mizo, vilice so med njimi. Filozofi lahko jedo iz svojih krožnikov z neskončno hrano, razmišljajo ali čakajo. Da bi jedli, mora filozof vzeti dve vilici (zadnji deli vilico s prvim). Vzeti in odložiti vilico sta dve ločeni dejSTvi. Vsi filozofi so tiho. Naloga je najti algoritem, ki omogoča, da vsi razmišljajo in so siti tudi po 54 letih.
Najprej poskusimo rešiti to nalogo z uporabo skupnega prostora. Vilice ležijo na skupni mizi in filozofi jih preprosto vzamejo, ko so na voljo, in jih vrnejo nazaj. Tukaj se pojavljajo težave s sinhronizacijo: kdaj vzeti vilice? Kaj storiti, če vilice ni? itd. Toda najprej zagnajmo filozofe.
Za zagon niti uporabljamo skladišče niti preko Task.Run metode:
var cancelTokenSource = new CancellationTokenSource();
Action create = (i) => RunPhilosopher(i, cancelTokenSource.Token);
for (int i = 0; i create(icopy), cancelTokenSource.Token);
}Pool niti je ustvarjen za optimizacijo ustvarjanja in odstranjevanja niti. Ta bazen ima vrsto nalog in CLR ustvarja ali odstranjuje niti glede na število teh nalog. En bazen za vse AppDomain-e. Ta bazen bi morali uporabljati skoraj vedno, saj ni potrebno skrbeti za ustvarjanje, odstranjevanje niti, njihove vrste in drugo. Lahko se tudi brez bazena, a potem je treba neposredno uporabljati Niti, kar je smiselno v primerih, ko je potrebno spremeniti prednost niti, ko imamo dolgotrajno operacijo, za Foreground nit in drugo.
Drugimi besedami, System.Threading.Tasks.Task razred — to je enako Niti, vendar z različnimi priročnostmi: možnostjo zagoniti nalogo po bloku drugih nalog, vračati jih iz funkcij, enostavno jih prekiniti in še mnogo več. Potrebni so za podporo konstrukcijam async/await (Task-based Asynchronous Pattern, sintaktični sladkor za čakanje na IO operacijo). O tem bomo še govorili.
CancelationTokenSource je tukaj potreben, da se nit lahko sama zaključi na signal sprožilne niti.
Težave z usklajevanjem
Blokirani filozofi
Dobro, znamo ustvarjati niti, poskusimo večerjati:
// Кто какие вилки взял. К примеру: 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();
}
}Tukaj najprej poskusimo vzeti levo in potem desno vilico. Če prilijemo, jemo in jih vrnemo nazaj. Vzemanje ene vilice je atomsko, tj. dve niti ne moreta vzeti ene hkrati (narobe: prva prebere, da je vilica prosta, druga — prav tako, prva vzame, druga vzame). Zato Interlocked.CompareExchange, ki naj bi bil uresničen s pomočjo procesorske instrukcije (TSL, XCHG), ki zaklene del pomnilnika za atomsko zaporedno branje in pisanje. SpinWait je enakovreden konstrukciji while(true) s samo malo 'magije' — nit zavzame procesor (Thread.SpinWait), vendar včasih preda nadzor drugi niti (Thread.Yeild) ali zaspi (Thread.Sleep).
Toda ta rešitev ne deluje, saj se niti kmalu (pri meni v sekundi) blokirajo: vsi filozofi vzamejo svojo levo vilico, desne pa nimajo. Množica forks takrat ima vrednosti: 1 2 3 4 5.

Na risbi je blokiranje niti (deadlock). Zeleno — izvajanje, rdeče — usklajevanje, sivo — nit spi. Diamanti označujejo čas za zagon nalog.
Hranjenje filozofov
Čeprav za razmišljanje ne potrebujemo veliko hrane, pa lahko lakota koga razvrže iz filozofske razprave. Poskusimo simulirati situacijo lakote niti v našem primeru. Lakota pomeni, da deluje nit, a ne opravlja nobenega bistvenega dela, drugače rečeno, to je enako mrtvemu zaklepanju, le da zdaj nit ne spi, ampak aktivno išče način, kako bi pojedla, a hrane ni. Da bi se izognili pogosti blokadi, bomo vilo položili nazaj, če nam ne uspe vzeti druge.
// То же что и в 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 tej kodi je pomembno, da dva od štirih filozofov pozabita položiti svojo levo vilo. In tako jedo več hrane, medtem ko drugi začenjajo trpeti lakoto, čeprav imajo niti enake prioritete. Tukaj ne trpijo povsem lakote, saj slabi filozofi včasih ponovno položijo svoje vilice. Izhaja, da dobri jedo približno 5-krat manj kot slabi. Tako majhna napaka v kodi povzroči, da se učinkovitost zniža. Tu je vredno opaziti, da je možna redka situacija, ko vsi filozofi vzamejo levo vilo, desne nimajo, položijo levo, čakajo, ponovno vzamejo levo itd. Ta situacija je prav tako lakota, ki bolj spominja na mrtvo zaklepanje. Ni mi uspelo ponoviti. Spodaj je slika situacije, ko sta dva slaba filozofa vzela obe vili, medtem ko se dva dobra mučita z lakoto.

Tukaj je vidno, da se niti občasno zbudijo in poskusijo pridobiti vir. Dva jedra od štirih ne delata nič (zelena grafika na vrhu).
Smrt filozofa
Še ena težava, ki lahko prekine slavna kosila filozofov, je, če eden od njih nenadoma umre z vilicami v rokah (in ga tako tudi pokopljejo). Takrat sosedje ostanejo brez kosila. Primer kode za to situacijo si lahko izmislite sami, na primer se sproži NullReferenceException potem ko filozof vzame vilice. In, mimogrede, izjema ne bo obdelana in klicna koda je ne bo ujela (za to AppDomain.CurrentDomain.UnhandledException in drugi). Zato so obdelovalci napak potrebni znotraj samih niti in z ustreznim zaključkom.
Natakar
Dobro, kako da rešimo ovaj problem međusobnog blokiranja, gladi i smrti? Dozvolićemo samo jednom filozofu da uzme viljuške, dodajući međusobno isključivanje (mutual exclusion) kod ovog mesta. Kako to uraditi? Pretpostavimo da je pored filozofa konobar koji daje dozvolu nekom filozofu da uzme viljuške. Kako da osmislimo ovog konobara i kako će ga filozofi moliti, to su zanimljiva pitanja.
Najjednostavniji način je kada filozofi jednostavno neprekidno traže konobara da im omogući pristup viljuškama. To jest, sada filozofi neće čekati viljušku pored sebe, već će čekati ili moliti konobara. Najpre ćemo za to koristiti samo korisnički prostor, u kojem ne koristimo prekide za pozivanje nekih procedura iz jezgra (o tome više kasnije).
Rešenja u korisničkom prostoru
Ovde ćemo raditi isto što smo ranije radili s jednom viljuškom i dva filozofa, obavljaćemo petlju i čekati. Ali sada će to biti svi filozofi i kao da postoji samo jedna viljuška, tj. može se reći da će jesti samo onaj filozof koji uzme tu "zlatnu viljušku" od konobara. Za ovo ćemo koristiti SpinLock.
private static SpinLock spinLock = new SpinLock(); // Naš "konobar"
private void RunSpinLock(int i, CancellationToken token)
{
while (true)
{
// Vezano za međusobno blokiranje kroz busy waiting. Pozivamo do try, da
// bismo bacili izuzetak u slučaju greške u samom SpinLock.
bool hasLock = false;
spinLock.Enter(ref hasLock);
try
{
// Ovde može biti samo jedna nit (mutual exclusion).
forks[Left(i)] = i + 1; // Uzimamo viljušku odmah, bez čekanja.
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(); // Izbegavamo problem smrti filozofa.
}
Think(i);
if (token.IsCancellationRequested)
break;
}
}SpinLock to je zaključavanje, sa, grubo rečeno, istim while(true) { if (!lock) break; }, ali sa još većom „magijom“ nego u SpinWait (ki se uporablja tam). Zdaj zna šteti čakajoče, jih nekoliko uspavati in še marsikaj drugega. Skratka, naredi vse, kar je mogoče za optimizacijo. Vendar pa je treba pomniti, da gre še vedno za aktivno zanko, ki porablja procesorske vire in ohranja tok, kar lahko pripelje do stradanja, če eden od filozofov postane prednosten pred drugimi, a nima zlate vilice (problem prioritetne inverzije). Zato ga uporabljamo samo za zelo, zelo kratke spremembe v skupni pomnilnik, brez kakršnih koli zunanjih klicev, vstavljenih zaklepov in drugih presenečenj.

Slika za SpinLock. Tokovi nenehno »vojskujejo« za zlato vilico. Dogajajo se padci — na sliki označeno območje. Jedra niso popolnoma izkoriščena: samo približno 2/3 s temi štirimi tokovi.
Druga rešitev bi bila uporabiti le Interlocked.CompareExchange z enakim aktivnim čakajočim, kot je prikazano v zgornji kodi (pri stradanjih filozofov), vendar to, kot je bilo že omenjeno, teoretično lahko privede do blokade.
O Interlocked treba povedati, da tam ni le CompareExchange, ampak tudi druge metode za atomsko branje in pisanje. In s ponovnim spreminjanjem v primeru, da drugi tok uspe vnesti svoje spremembe (bral 1, bral 2, pisal 2, pisal 1 slabo), se lahko uporablja za kompleksne spremembe enega vrednosti (Interlocked Anything vzorec).
Rešitve v načinu jedra
Da bi se izognili izgubi virov v zanki, poglejmo, kako lahko blokiramo tok. Drugimi besedami, nadaljujoč z našim primerom, poglejmo, kako natakar uspava filozofa in ga zbudi šele takrat, ko je potrebno. Najprej poglejmo, kako to doseči preko načina jedra operacijskega sistema. Vse strukture so tam pogosto počasnejše kot tiste v uporabniškem prostoru. Počasnejše večkrat, na primer AutoResetEvent je lahko 53-krat počasnejši SpinLock [Richter]. Toda s pomočjo teh lahko sinhroniziramo procese po celotni sistemu, bodisi nadzorovane bodisi ne.
Osnovna konstrukcija ovde je semafor, koji je predložio Dijkstra pre više od pola veka. Semafor je, pojednostavljeno rečeno, pozitivno celo broj, kojim upravlja sistem, i dve operacije nad njim — povećanje i smanjenje. Ako smanjenje nije moguće, kad dostigne nulu, pozivajući nit se blokira. Kada neko drugi aktivni nit/proces poveća broj, tada se niti propuštaju, a semafor se ponovo smanjuje za propuštene. Može se zamisliti kao vozovi na užem mestu sa semaforom. .NET pruža nekoliko konstrukcija sa sličnim funkcijama: AutoResetEvent, ManualResetEvent, Mutex i sam Semaphore. Uporabili bomo AutoResetEvent, to je najjednostavnija od ovih konstrukcija: samo dve vrednosti 0 i 1 (false, true). Njegova metoda WaitOne() blokira pozivajuću nit, ako je vrednost bila 0, a ako je 1, snižava na 0 i propušta je. A metoda Set() povećava na 1 i propušta jednog čekatelja, koji ponovo snižava na 0. Deluje kao turniket u metrou.
Uskompljićemo rešenje i koristićemo zaključavanje za svakog filozofa, a ne za sve odjednom. Tj. sada može biti nekoliko filozofa odjednom, a ne samo jedan. Ali ponovo blokiramo pristup stolu, kako bismo ispravno, izbegavajući trke (race conditions), uzeli vilice.
// Для блокирования отдельного философа.
// Инициализируется: 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 bismo razumeli šta se ovde dešava, razmotrimo slučaj kada filozofu ne uspeva da uzme vilice, tada će njegovi postupci biti sledeći. Čeka pristup stolu. Kada ga dobije, pokušava da uzme vilice. Nije uspelo. Vraća pristup stolu (uzajamna isključenost). I prolazi kroz svoj 'turniket' (AutoResetEvent) (isprva su otvoreni). Ponovo ulazi u ciklus, jer nema vilice. Pokušava da ih uzme i staje kod svog 'turniketa'. Neki srećniji komšija s desne ili leve, koji je završio sa jelom, otključava našeg filozofa, 'otvarajući njegov turniket'. Naš filozof prolazi kroz njega (i on se zatvara iza njega) po drugi put. Pokušava treći put da uzme vilice. Uspeno. I prolazi kroz svoj turniket da večera.
Kada se u ovakvom kodu jave slučajne greške (one uvek postoje), na primer, ako je nepravilno označen komšija ili je kreiran isti objekat AutoResetEvent za sve (Enumerable.Repeat), tada će filozofi čekati već programere, jer je pronalazak grešaka u ovakvom kodu prilično složeno zanimanje. Još jedan problem sa ovom rešenjem je taj što ne garantuje da neki filozof neće početi da umire od gladi.
Hibridna rešenja
Razpravljali smo o dveh pristopih k sinhronizaciji, kjer ostajamo v načinu uporabnika in kolesarimo v zanki, ter kjer blokiramo niti preko jedra. Prva metoda je dobra za kratke blokade, druga pa za dolge. Pogosto je potrebno najprej kratko čakati na spremembo spremenljivke v zanki, nato pa blokirati nit, ko je čakanje dolgo. Ta pristop je realiziran v tako imenovanih hibridnih konstrukcijah. Tu so enake konstrukcije, kot so bile za način jedra, vendar zdaj s krogom v načinu uporabnika: SemaphoreSlim, ManualResetEventSlim itd. Najbolj priljubljena konstrukcija tukaj je Monitor, saj ima C# znan lock sintakso. Monitor To je isti semafor z največjo vrednostjo 1 (mutes), vendar s podporo za čakanje v zanki, rekurzijo, vzorcem Condition Variable (o njem spodaj) in drugo. Poglejmo rešitev z 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);
}
}Tukaj ponovno blokiramo celotno mizo za dostop do vilic, vendar sedaj takoj odklenemo vse niti, ne le sosede, ko nekdo konča jesti. T.j. najprej nekdo je in blokira sosede, in ko ta nekdo konča, a želi takoj ponovno jesti, gre v blokado in zbudi svoje sosede, saj je njegov čas čakanja krajši.
Tako se izognemo mrtvim zavrženjem in lakoti katerega koli filozofa. Uporabimo zanko za kratko čakanje in blokiramo nit za dolgo. Takojšnje odklepanje vseh deluje počasneje kot če bi bilo odklepanje le soseda, kot v rešitvi z AutoResetEvent, vendar razlika ne bi smela biti velika, saj bi morale niti ostati v načinu uporabnika najprej.
Pri lock Sintaksa ima neprijetna presenečenja. Priporočajo, da se uporablja Monitor direktno [Richter] [Eric Lippert]. Ena izmed njih je, da lock vedno izhaja iz Monitor, tudi če je prišlo do izjeme, in takrat lahko druga nit spremeni stanje skupne pomnilnika. V takih primerih je pogosto bolje iti v mrtvo zavrnitev ali varno zaključiti program. Drugo presenečenje je, da Monitor uporablja sinhronizacijske bloke (SyncBlock), ki so prisotni v vseh objektih. Zato, če izberete neprimeren objekt, lahko enostavno pridobite mrtvo zavrnitev (na primer, če shranite na internirano niz). Vedno uporabljamo skriti objekt za to.
Patern Condition Variable omogućava sažeto implementirati čekanje na određeni složen uvjet. U .NET-u nije potpun, po mom mišljenju, jer bi trebale postojati više redova na više varijabli (kao u Posix Threads), a ne na jednom zaključavanju. Tada bi ih mogli napraviti za sve filozofe. No, u ovom obliku, također omogućava skraćivanje koda.
Mnogi filozofi ili async / await
U redu, sada znamo učinkovito blokirati niti. No, što ako imamo mnogo filozofa? 100? 10000? Na primjer, dobili smo 100000 zahtjeva na web poslužitelj. Kreiranje nove niti za svaki zahtjev bi bilo skupo, jer se toliko niti ne može istovremeno izvršavati. Izvršavat će se samo onoliko koliko je logičkih jezgri (imam 4). A sve ostale će jednostavno trošiti resurse. Jedno od rješenja ovog problema je async / await obrazac. Ideja je da funkcija ne zadrži nit ako treba čekati na nešto za svoje nastavak. Kada se to nešto dogodi, nastavlja svoje izvršavanje (ali ne nužno u istoj niti!). U našem slučaju, čekat ćemo na vilice.
SemaphoreSlim ima za to WaitAsync() metodu. Evo implementacije koristeći 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 prevodi se u pametni konačni automat koji odmah vraća svoj unutarnji Naloga. Kroz njega možemo čekati završetak metode, otkazati je i sve ostalo što možemo raditi s Task. Unutra metode, konačni automat kontrolira izvršavanje. Bit je u tome da ako nema kašnjenja, tada je izvršavanje sinkrono, a ako postoji, nit se oslobađa. Za bolje razumijevanje ovog najbolje je pogledati ovaj konačni automat. Možemo stvarati lance od ovih async / await metoda.
Testirat ćemo. Rad 100 filozofa na računalu s 4 logičke jezgre, 8 sekundi. Prethodno rješenje s Monitorom izvršavalo je samo prve 4 niti, dok se ostale nisu izvršavale nijednom. Svaka od tih 4 niti čekala je oko 2 ms. A rješenje s async / await izvršavalo je svih 100, pri čemu je prosječno svaka čekala 6,8 sekundi. Naravno, u stvarnim sustavima čekanje od 6 sekundi nije prihvatljivo i bolje je ne obraditi toliko zahtjeva na taj način. Rješenje s Monitorom nije se pokazalo scalable ni na koji način.
Zaključek
Kot kaže, iz teh majhnih primerov .NET podpira mnoge sinhronizacijske konstrukcije. Vendar pa ni vedno očitno, kako jih uporabiti. Upam, da je bil ta članek koristen. Zaenkrat zaključujemo, a še vedno je veliko zanimivega, na primer varne zbirke, TPL Dataflow, reaktivno programiranje, model programske transakcije in drugo.
Izvori
- Vizualizacija niti:
- MSDN: , in še veliko več.
- [Richter] — CLR via C#, Jeffrey Richter
- [Eric Lippert] —
- Slika — 'Ples med meči', H. Semiradski
Izvor: habr.com
