Well-Fed filosofen of kompetitive .NET-programmearring

Well-Fed filosofen of kompetitive .NET-programmearring

Litte wy sjen hoe't simultane en parallelle programmearring wurket yn .Net, mei it Philosophers Dining Problem as foarbyld. It plan is dit, fan 'e syngronisaasje fan threads / prosessen, oant it akteursmodel (yn' e folgjende dielen). It artikel kin nuttich wêze foar de earste kunde of om jo kennis te ferfarskjen.

Wêrom dogge it hielendal? Transistors berikke har minimale grutte, de wet fan Moore rêst op 'e beheining fan' e ljochtsnelheid en dêrom wurdt in ferheging yn it oantal waarnommen, mear transistors kinne makke wurde. Tagelyk groeit de hoemannichte gegevens, en brûkers ferwachtsje in direkte reaksje fan 'e systemen. Yn sa'n situaasje is "normale" programmearring, as wy ien útfierende tried hawwe, net langer effektyf. Jo moatte op ien of oare manier it probleem fan simultane of tagelyk útfiering oplosse. Boppedat bestiet dit probleem op ferskate nivo's: op it nivo fan triedden, op it nivo fan prosessen, op it nivo fan masines yn it netwurk (ferspraat systemen). .NET hat hege-kwaliteit, time-testen technologyen foar fluch en effisjint oplosse sokke problemen.

Objective

Edsger Dijkstra stelde dit probleem al yn 1965 oan syn learlingen. De fêststelde formulearring is as folget. Der is in bepaald (meastal fiif) oantal filosofen en itselde oantal foarken. Se sitte oan in rûne tafel, foarken tusken har. Filosofen kinne fan har platen fan einleaze iten ite, tinke of wachtsje. Om in filosoof te iten, moatte jo twa foarken nimme (de lêste dielt de foarke mei de earste). In foarke ophelje en delsette binne twa aparte aksjes. Alle filosofen swije. De taak is om sa'n algoritme te finen dat se allegear nei 54 jier tinke en fol wêze.

Litte wy earst besykje dit probleem op te lossen troch it brûken fan in dielde romte. De foarken lizze op 'e mienskiplike tafel en de filosofen nimme se gewoan as se binne en sette se werom. Hjir binne der problemen mei syngronisaasje, wannear krekt te nimmen surebets? wat as der gjin foarke is? ensfh Mar lit ús earst de filosofen begjinne.

Om triedden te begjinnen, brûke wy in threadpool troch Task.Run metoade:

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

De threadpool is ûntworpen om it oanmeitsjen en wiskjen fan thread te optimalisearjen. Dit swimbad hat in wachtrige mei taken en de CLR makket of ferwideret triedden ôfhinklik fan it oantal fan dizze taken. Ien pool foar alle AppDomains. Dit swimbad moat brûkt wurde hast altyd, omdat. gjin needsaak om te lestich falle mei it meitsjen, wiskjen fan diskusjes, harren wachtrijen, ensfh It is mooglik sûnder in pool, mar dan moatte jo brûke it direkt Thread, dit is nuttich foar gefallen as jo de prioriteit fan in tried feroarje moatte, as wy in lange operaasje hawwe, foar in Foargrûntried, ensfh.

Mei oare wurden, System.Threading.Tasks.Task klasse is itselde Thread, mar mei allerhanne gemak: de mooglikheid om in taak út te fieren nei in blok fan oare taken, werom te jaan fan funksjes, maklik te ûnderbrekken, en mear. ensfh Se binne nedich om te stypjen async / wachtsje konstruksjes (Taak-basearre Asynchronous Pattern, syntaktyske sûker foar wachtsjen op IO operaasjes). Wy prate hjir letter oer.

CancelationTokenSource hjir is it nedich sadat de tried sels op it sinjaal fan de oproptried ôfslute kin.

Syngronisaasjeproblemen

Blokkearde filosofen

Okee, wy witte hoe't jo threads meitsje kinne, litte wy besykje lunch te hawwen:

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

Hjir besykje wy earst de linker foarke te nimmen, en dan de rjochter foarke, en as it slagget, dan ite wy en sette se werom. It nimmen fan ien foarke is atoom, d.w.s. twa triedden kinne net nimme ien tagelyk (ferkearde: de earste lêst dat de foarke is frij, de twadde - ek, de earste nimt, de twadde nimt). Foar dit Interlocked.CompareExchange, dy't moat wurde ymplementearre mei in prosessor ynstruksje (TSL, XCHG), dy't in stikje ûnthâld beskoattelt foar atomyske opfolgjende lêzen en skriuwen. En SpinWait is lykweardich oan it konstruksje while(true) allinich mei in bytsje "magy" - de tried nimt de prosessor (Thread.SpinWait), mar draacht soms kontrôle oer nei in oare thread (Thread.Yeild) of falt yn sliep (Thread.Sleep).

Mar dizze oplossing wurket net, omdat de streamen binne gau (foar my binnen in sekonde) blokkearre: alle filosofen nimme har lofter foarke, mar net de rjochter. De forks array hat dan de wearden: 1 2 3 4 5.

Well-Fed filosofen of kompetitive .NET-programmearring

Yn de figuer, blokkearjende triedden (deadlock). Grien - útfiering, read - syngronisaasje, griis - de tried sliept. De rhombuses jouwe de starttiid fan Taken oan.

De honger fan 'e filosofen

Hoewol't it is net nedich om te tinken foaral folle iten, mar honger makket immen opjaan filosofy. Litte wy besykje te simulearjen de situaasje fan honger fan triedden yn ús probleem. Starvation is as in tried rint, mar sûnder signifikant wurk, mei oare wurden, dit is deselde deadlock, allinich no sliept de tried net, mar siket aktyf nei wat te iten, mar der is gjin iten. Om faak blokkearjen foar te kommen, sille wy de foarke werom sette as wy net in oare nimme koene.

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

It wichtige ding oer dizze koade is dat twa fan de fjouwer filosofen ferjitte om har linker foarke del te setten. En it docht bliken dat se mear iten ite, wylst oaren begjinne te hongerjen, hoewol de triedden deselde prioriteit hawwe. Hjir binne se net hielendal úthongere, want. minne filosofen sette har foarken soms werom. It docht bliken dat goede minsken sa'n 5 kear minder ite as minne. Dus in lytse flater yn 'e koade liedt ta in drop yn prestaasjes. It is hjir ek de muoite wurdich op te merken dat in seldsume situaasje mooglik is as alle filosofen de linker foarke nimme, d'r is gjin rjochter, se sette de linker, wachtsje, nim de lofts wer, ensfh. Dizze situaasje is ek in úthongering, mear as in deadlock. Ik koe it net werhelje. Hjirûnder is in foto foar in situaasje dêr't twa minne filosofen hawwe nommen beide foarken en twa goede binne úthongere.

Well-Fed filosofen of kompetitive .NET-programmearring

Hjir kinne jo sjen dat de triedden soms wekker wurde en besykje de boarne te krijen. Twa fan de fjouwer kearnen dogge neat (griene grafyk hjirboppe).

Dea fan in filosoof

No, in oar probleem dat in hearlik diner fan filosofen ûnderbrekke kin is as ien fan har ynienen stjert mei foarken yn 'e hannen (en se sille him sa begroeven). Dan sitte de buorlju sûnder lunch. Jo kinne komme mei in foarbyld koade foar dit gefal sels, bygelyks, it wurdt smiten út NullReferenceException neidat de filosoof de foarken nimt. En trouwens, de útsûndering sil net wurde behannele en de opropkoade sil it net allinich fange (foar dit AppDomain.CurrentDomain.UnhandledException en ensfh.). Dêrom binne flaterhannelers nedich yn 'e triedden sels en mei sierlike beëiniging.

De klean

Okee, hoe losse wy dit probleem fan deadlock, úthongering en dea op? Wy sille tastean mar ien filosoof te berikken de foarken, foegjen in ûnderlinge útsluting fan triedden foar dit plak. Hoe it te dwaan? Stel dat der in ober njonken de filosofen stiet, dy't elke filosoof tastimming jout om de foarken te nimmen. Hoe meitsje wy dizze ober en hoe sille filosofen him freegje, de fragen binne ynteressant.

De ienfâldichste manier is as de filosofen gewoan de ober konstant freegje om tagong ta de foarken. Dy. no sille filosofen net wachtsje op in foarke tichtby, mar wachtsje of freegje de ober. Yn 't earstoan brûke wy allinich brûkersromte foar dit, dêryn brûke wy gjin interrupts om prosedueres fan' e kernel op te roppen (oer har hjirûnder).

Oplossings yn brûkersromte

Hjir sille wy itselde dwaan as wy eartiids diene mei ien foarke en twa filosofen, wy sille yn in syklus draaie en wachtsje. Mar no sille it allegear filosofen wêze en as it wie mar ien foarke, d.w.s. it kin sein wurde dat allinich de filosoof dy't dizze "gouden foarke" fan 'e ober naam, sil ite. Hjirfoar brûke wy 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 dit is in blocker, mei, rûchwei, itselde while(true) { if (!lock) break; }, mar mei noch mear "magy" as yn SpinWait (dy't dêr brûkt wurdt). No wit er de wachtsjende te tellen, se in bytsje yn 'e sliep te bringen en mear. ensfh Yn it algemien, docht alles mooglik te optimalisearjen. Mar wy moatte betinke dat dit noch altyd deselde aktive syklus is dy't prosessorboarnen opfet en de stream behâldt, wat kin liede ta úthongering as ien fan 'e filosofen mear prioriteit wurdt as oaren, mar hat gjin gouden foarke (Probleem mei prioriteit Inversion) . Dêrom brûke wy it allinich foar heul koarte feroarings yn dielde ûnthâld, sûnder oproppen fan tredden, nestede slûzen en oare ferrassingen.

Well-Fed filosofen of kompetitive .NET-programmearring

Tekenje foar SpinLock. De streamen "fjochtsje" hieltyd om de gouden foarke. Der binne mislearrings - yn 'e figuer, it selektearre gebiet. De kearnen wurde net folslein brûkt: mar sawat 2/3 troch dizze fjouwer triedden.

In oare oplossing hjir soe wêze om allinich te brûken Interlocked.CompareExchange mei itselde aktive wachtsjen as werjûn yn 'e koade hjirboppe (by de úthongere filosofen), mar dit, lykas al sein, kin teoretysk liede ta blokkearjen.

op Interlocked Dêrby moat opmurken wurde dat der net allinnich CompareExchange, mar ek oare metoaden foar atomic lêzen EN skriuwe. En troch werhelling fan feroaringen, yn it gefal dat in oare thread tiid hat om syn wizigingen te meitsjen (lêze 1, lêze 2, skriuwe 2, skriuwe 1 is min), kin it brûkt wurde foar komplekse feroarings oan ien wearde (Interlocked Anything-patroan).

Kernel Mode Solutions

Om foar te kommen fergriemen fan boarnen yn in lus, lit ús sjen hoe't wy kinne blokkearje in tried. Mei oare wurden, trochgean mei ús foarbyld, lit ús sjen hoe't de ober de filosoof yn 'e sliep bringt en him allinich wekker makket as it nedich is. Litte wy earst sjen hoe't jo dit kinne dwaan fia de kernelmodus fan it bestjoeringssysteem. Alle struktueren dêr binne faak stadiger as dy yn brûkersromte. Ferskate kearen stadiger, bygelyks AutoResetEvent miskien 53 kear stadiger SpinLock [Richter]. Mar mei har help kinne jo prosessen yn it hiele systeem syngronisearje, beheard of net.

De basiskonstruksje hjir is de semafoar dy't Dijkstra mear as in heale ieu lyn foarstelde. In semafoar is, gewoanwei, in posityf hiel getal beheard troch it systeem, en twa operaasjes derop, tanimme en fermindere. As it net slagget te ferminderjen, nul, dan wurdt de oproptried blokkearre. As it oantal wurdt ferhege troch in oare aktive thread / proses, dan wurde de triedden oerslein en de semafoar wurdt wer fermindere troch it trochjûn nûmer. Men kin treinen yn in flessehals mei in semafoar foarstelle. .NET biedt ferskate konstruksjes mei ferlykbere funksjonaliteit: AutoResetEvent, ManualResetEvent, Mutex en mysels Semaphore. Wy sille brûke AutoResetEvent, dit is de ienfâldichste fan dizze konstruksjes: mar twa wearden 0 en 1 (falsk, wier). Har metoade WaitOne() blokkearret de oproptried as de wearde 0 wie, en as 1, ferleget it nei 0 en slacht it oer. In metoade Set() ferheft nei 1 en lit ien ober troch, dy't wer ferleget nei 0. Handelt as in metro turnstile.

Litte wy de oplossing komplisearje en it slot brûke foar elke filosoof, en net foar alles tagelyk. Dy. no kinne der ferskate filosofen tagelyk wêze, en net ien. Mar wy wer blokkearje tagong ta de tafel om korrekt, mije races (ras betingsten), nimme 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();
}

Om te begripen wat hjir bart, beskôgje it gefal as de filosoof net slagge om de foarken te nimmen, dan sil syn aksjes as folgjend wêze. Hy wachtet op tagong ta de tafel. Nei't er it krigen hat, besiket er de foarken te nimmen. It slagge net. It jout tagong ta de tafel (ûnderlinge útsluting). En giet syn "turnstile" foarby (AutoResetEvent) (se binne earst iepen). It komt wer yn de syklus, want hy hat gjin foarken. Hy besiket se te nimmen en stopet by syn "turnstile". Guon mear gelokkige buorman oan de rjochterkant of lofts, nei't klear iten, ûntskoattelje ús filosoof, "iepenje syn turnstile." Us filosoof giet it (en it slút der efter) foar de twadde kear troch. Hy besiket foar de tredde kear de foarken te nimmen. Súkses. En hy giet syn draaihek foarby om te iten.

As d'r willekeurige flaters binne yn sa'n koade (se besteane altyd), wurdt bygelyks in buorman ferkeard oantsjutte of itselde objekt wurdt makke AutoResetEvent foar alle (Enumerable.Repeat), dan sille de filosofen wachtsje op de ûntwikkelders, om't It finen fan flaters yn sokke koade is nochal in drege taak. In oar probleem mei dizze oplossing is dat it net garandearret dat guon filosoof net úthongere.

Hybride oplossings

Wy hawwe sjoen nei twa oanpak fan timing, as wy bliuwe yn brûkers modus en loop, en as wy blokkearje tried troch de kernel. De earste metoade is goed foar koarte slûzen, de twadde foar lange. It is faak nedich om earst koart te wachtsjen op in fariabele om te feroarjen yn in lus, en dan de tried blokkearje as it wachtsjen lang is. Dizze oanpak wurdt útfierd yn de saneamde. hybride struktueren. Hjir binne deselde konstruksjes as foar kernel modus, mar no mei in brûker modus loop: SemaphorSlim, ManualResetEventSlim ensfh De meast populêre design hjir is Monitor, omdat yn C # der is in bekende lock syntaksis. Monitor dit is deselde semafoar mei in maksimum wearde fan 1 (mutex), mar mei stipe foar wachtsjen yn in loop, rekursion, de Condition Variable patroan (mear oer dat hjirûnder), ensfh Lit ús sjen nei in oplossing mei it.

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

Hjir blokkearje wy wer de hiele tafel foar tagong ta de foarken, mar no ûntblokkearje wy alle triedden tagelyk, en net buorlju as immen klear is mei iten. Dy. earst, immen yt en blokkearret de buorlju, en as dizze ien klear is, mar wol wer ite daliks, giet er yn blokkearjen en makket syn buorlju wekker, want. syn wachttiid is minder.

Dit is hoe't wy deadlocks en de úthongering fan guon filosoof foarkomme. Wy brûke in loop foar in koart wachtsjen en blokkearje de tried foar in lange. It opheffen fan elkenien yn ien kear is stadiger as as allinnich de buorman wie deblokkearre, lykas yn de oplossing mei AutoResetEvent, mar it ferskil moat net wêze grut, omdat threads moatte earst yn brûkersmodus bliuwe.

У lock syntaksis hat ferfelende ferrassingen. Oanrikkemandearje om te brûken Monitor direkt [Richter] [Eric Lippert]. Ien fan harren is dat lock altyd út Monitor, sels as der in útsûndering wie, yn dat gefal koe in oare tried de steat fan dielde ûnthâld feroarje. Yn sokke gefallen is it faaks better om nei deadlock te gean of it programma op ien of oare manier feilich te beëinigjen. In oare ferrassing is dat Monitor syngronisaasjeblokken brûkt (SyncBlock), dy't oanwêzich binne yn alle objekten. Dêrom, as in net geskikt foarwerp wurdt selektearre, kinne jo maklik krije in deadlock (bygelyks, as jo beskoattelje op in ynterne string). Wy brûke it altyd ferburgen objekt foar dit.

It Condition Variable-patroan lit jo de ferwachting fan guon komplekse betingsten koart útfiere. Yn .NET, it is ûnfolslein, yn myn miening, omdat yn teory, der moatte ferskate wachtrijen op ferskate fariabelen (lykas yn Posix Threads), en net op ien lok. Dan koe men se foar alle filosofen meitsje. Mar sels yn dizze foarm kinne jo de koade ferminderje.

in protte filosofen of async / await

Okee, no kinne wy ​​​​threads effektyf blokkearje. Mar wat as wy in protte filosofen hawwe? 100? 10000? Wy krigen bygelyks 100000 oanfragen oan de webserver. It sil wêze overhead te meitsjen in tried foar elk fersyk, omdat safolle triedden sille net parallel rinne. Sil allinne rinne safolle as der binne logyske kearnen (Ik haw 4). En alle oaren sille gewoan middels nimme. Ien oplossing foar dit probleem is it async / await-patroan. It idee is dat de funksje de tried net hâldt as it moat wachtsje op wat om troch te gean. En as it wat docht, ferfeelt it syn útfiering (mar net needsaaklik op deselde tried!). Yn ús gefal sille wy wachtsje op 'e foarke.

SemaphoreSlim hat foar dit WaitAsync() metoade. Hjir is in ymplemintaasje mei dit patroan.

// Запуск такой же, как раньше. Где-нибудь в программе:
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();
}

Metoade mei async / await wurdt oerset yn in lestich steat masine dy't fuortendaliks jout syn ynterne Task. Troch it kinne jo wachtsje op it foltôgjen fan 'e metoade, it annulearje, en al it oare dat jo kinne dwaan mei Task. Binnen de metoade kontrolearret de steat masine de útfiering. De ûnderste rigel is dat as der gjin fertraging is, dan is de útfiering syngroan, en as der is, dan wurdt de tried frijlitten. Foar in better begripe dit, it is better om te sjen op dizze steat masine. Jo kinne meitsje keatlingen fan dizze async / await metoaden.

Litte wy testen. Wurk fan 100 filosofen op in masine mei 4 logyske kearnen, 8 sekonden. De foarige oplossing mei Monitor rûn allinich de earste 4 threads en de rest rûn hielendal net. Elk fan dizze 4 triedden wie sawat 2ms idle. En de async / await-oplossing rûn alle 100, mei in gemiddelde wacht fan 6.8 sekonden elk. Fansels, yn echte systemen, idle foar 6 sekonden is net akseptabel en it is better net te ferwurkjen safolle fersiken lykas dit. De oplossing mei Monitor die bliken hielendal net skaalber te wêzen.

konklúzje

Sa't jo sjen kinne fan dizze lytse foarbylden, stipet .NET in protte syngronisaasjekonstruksjes. It is lykwols net altyd dúdlik hoe't se se brûke. Ik hoopje dat dit artikel nuttich wie. Foar no is dit it ein, mar d'r binne noch in protte nijsgjirrige dingen oer, bygelyks thread-feilige kolleksjes, TPL Dataflow, Reactive programmearring, Software Transaction model, ensfh.

Boarnen

Boarne: www.habr.com

Add a comment