Vel nærðir heimspekingar eða samkeppnishæf .NET forritun

Vel nærðir heimspekingar eða samkeppnishæf .NET forritun

Við skulum skoða hvernig samhliða og samhliða forritun virkar í .Net, með því að nota dæmi um hádegisheimspekinga vandamálið. Áætlunin er sem hér segir, frá samstillingu þráðar/ferla til leikaralíkans (í eftirfarandi hlutum). Greinin gæti verið gagnleg fyrir fyrstu kynni eða til að hressa upp á þekkingu þína.

Af hverju jafnvel að vita hvernig á að gera þetta? Smári eru að ná lágmarksstærð, lögmál Moore er að ná mörkum ljóshraða og þess vegna sést vöxtur í fjölda; hægt er að búa til fleiri smára. Á sama tíma eykst gagnamagn og búast notendur við tafarlausum viðbrögðum frá kerfum. Í slíkum aðstæðum er „venjuleg“ forritun, þegar við höfum einn keyrsluþráð, ekki lengur áhrifarík. Við þurfum einhvern veginn að leysa vandamálið við framkvæmd samtímis eða samhliða. Þar að auki er þetta vandamál til á mismunandi stigum: á þræðistigi, á ferlistigi, á stigi véla á netinu (dreifð kerfi). .NET hefur hágæða, tímaprófaða tækni til að leysa slík vandamál fljótt og vel.

Verkefni

Edsger Dijkstra spurði nemendur sína um þetta vandamál árið 1965. Uppsetningin er sem hér segir. Það eru ákveðinn (venjulega fimm) fjöldi heimspekinga og sama fjöldi gaffla. Þeir sitja við hringborð, gaffla á milli sín. Heimspekingar geta borðað af diskunum sínum af endalausum mat, hugsað eða beðið. Til að borða þarf heimspekingur að taka tvo gaffla (sá síðarnefndi deilir gaffli með þeim fyrri). Að taka upp og setja niður gaffal eru tvær aðskildar aðgerðir. Allir heimspekingar þegja. Verkefnið er að finna slíkt reiknirit þannig að þeir hugsi allir og séu vel nærðir jafnvel eftir 54 ár.

Fyrst skulum við reyna að leysa þetta vandamál með því að nota sameiginlegt rými. Gafflarnir liggja á sameiginlega borðinu og heimspekingarnir taka þá einfaldlega þegar þeir eru þar og setja þá aftur. Þetta er þar sem vandamál með samstillingu koma upp, hvenær nákvæmlega á að taka gaffla? hvað á að gera ef það er engin stinga? o.s.frv. En fyrst skulum við byrja á heimspekingunum.

Til að stofna þræði notum við þráðasafn í gegnum Task.Run aðferð:

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

Þráðalaugin er hönnuð til að hámarka gerð og fjarlægingu þráða. Þessi hópur hefur röð verkefna og CLR býr til eða eyðir þráðum eftir fjölda þessara verkefna. Ein sundlaug fyrir öll AppDomains. Þessa laug ætti að nota nánast alltaf vegna þess að... engin þörf á að skipta sér af því að búa til og eyða þráðum, biðröðum þeirra o.s.frv. Þú getur gert það án sundlaugar, en þá þarftu að nota það beint Thread, þetta er gagnlegt fyrir tilvik þegar við þurfum að breyta forgangi þráðs, þegar við erum með langa aðgerð, fyrir forgrunnsþráð o.s.frv.

Með öðrum orðum, System.Threading.Tasks.Task bekk er það sama Thread, en með alls kyns þægindum: getu til að ræsa verkefni eftir blokk af öðrum verkefnum, skila þeim frá aðgerðum, trufla þau á þægilegan hátt og margt fleira. o.s.frv. Þeir eru nauðsynlegir til að styðja við ósamstillta/bíða byggingar (Task-based Asynchronous Pattern, setningafræðilegur sykur til að bíða eftir IO aðgerðum). Við tölum um þetta síðar.

CancelationTokenSource hér er nauðsynlegt að þráðurinn geti slitið sjálfum sér við merki frá kallþræðinum.

Samstillingarvandamál

Lokaðir heimspekingar

Allt í lagi, við vitum hvernig á að búa til þræði, við skulum prófa hádegismat:

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

Hér reynum við fyrst að taka vinstri og síðan hægri gaffalinn og ef það virkar borðum við og setjum þá aftur. Að taka einn gaffal er atómbundið, þ.e. tveir þræðir geta ekki tekið einn á sama tíma (rangt: sá fyrsti segir að gaffalinn sé laus, sá seinni gerir það sama, sá fyrsti tekur, sá annar tekur). Fyrir þetta Interlocked.CompareExchange, sem verður að útfæra með því að nota örgjörvaleiðbeiningar (TSL, XCHG), sem læsir minnishluta fyrir lestur og ritun í lotukerfinu. Og SpinWait jafngildir byggingunni while(true) aðeins með smá "töfrum" - þráðurinn tekur upp örgjörvann (Thread.SpinWait), en fer stundum yfir á annan þráð (Thread.Yeild) eða sofnar (Thread.Sleep).

En þessi lausn virkar ekki, vegna þess að... þræðir fljótlega (innan sekúndu fyrir mig) eru lokaðir: allir heimspekingar taka vinstri gaffalinn sinn, en það er enginn réttur. Forks fylkingin hefur þá gildin: 1 2 3 4 5.

Vel nærðir heimspekingar eða samkeppnishæf .NET forritun

Á myndinni, blokkandi þræði (deadlock). Grænt gefur til kynna framkvæmd, rautt gefur til kynna samstillingu og grátt gefur til kynna að þráðurinn sé sofandi. Demantar gefa til kynna upphafstíma Verkefna.

Hungur heimspekinganna

Þó að þú þurfir ekki mikinn mat til að hugsa getur hungur neytt hvern sem er til að gefast upp á heimspeki. Við skulum reyna að líkja eftir ástandi þráðasvelturs í vandamáli okkar. Hungur er þegar þráður virkar, en án verulegrar vinnu, með öðrum orðum, það er sama stoppið, aðeins núna sefur þráðurinn ekki, heldur er hann að leita að einhverju að borða, en það er enginn matur. Til að forðast tíðar blokkir munum við setja gaffalinn aftur ef við gætum ekki tekið annan.

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

Það sem er mikilvægt við þennan kóða er að tveir af hverjum fjórum heimspekingum gleyma að leggja niður vinstri gaffalinn. Og það kemur í ljós að þeir borða meiri mat og aðrir byrja að svelta, þó þræðir hafi sama forgang. Hér eru þeir ekki alveg að svelta, því... vondir heimspekingar leggja stundum gafflana aftur. Það kemur í ljós að þeir góðu borða um 5 sinnum minna en þeir vondu. Svo lítil villa í kóðanum leiðir til lækkunar á frammistöðu. Hér er líka rétt að taka fram að sjaldgæft ástand er mögulegt þegar allir heimspekingar taka vinstri gaffalinn, það er enginn hægri, þeir leggja þann vinstri niður, bíða, taka þann vinstri aftur o.s.frv. Þetta ástand er líka hungursneyð, meira eins og gagnkvæm stífla. Ég gat ekki endurtekið það. Hér að neðan er mynd af aðstæðum þar sem tveir vondir heimspekingar hafa tekið báða gafflana og tveir góðir svelta.

Vel nærðir heimspekingar eða samkeppnishæf .NET forritun

Hér má sjá að þræðir vakna stundum og reyna að ná í úrræði. Tveir af hverjum fjórum kjarna gera ekkert (grænt graf að ofan).

Dauði heimspekings

Jæja, enn eitt vandamálið sem gæti truflað glæsilegan kvöldverð heimspekinga er ef einn þeirra deyr skyndilega með gaffla í höndunum (og hann verður grafinn þannig). Þá verða nágrannarnir eftir án nesti. Þú getur sjálfur komið með dæmi um kóða fyrir þetta mál, honum er til dæmis hent NullReferenceException eftir að heimspekingurinn tekur gafflana. Og við the vegur, undantekningin verður ekki meðhöndluð og hringingarkóði mun ekki einfaldlega ná henni (fyrir þetta AppDomain.CurrentDomain.UnhandledException og o.s.frv.). Þess vegna er þörf á villumeðferðaraðilum í þræðinum sjálfum og með þokkafullri uppsögn.

Þjónn

Allt í lagi, hvernig leysum við þetta vandamál, svelti, hungur og dauðsföll? Við munum hleypa aðeins einum heimspekingi á blað og við munum bæta við gagnkvæmri útilokun þráða fyrir þennan stað. Hvernig á að gera það? Segjum sem svo að við hlið heimspekinganna sé þjónn sem gefur einum heimspekingi leyfi til að taka gafflana. Hvernig við ættum að búa þennan þjón og hvernig heimspekingar munu spyrja hann eru áhugaverðar spurningar.

Einfaldasta leiðin er að heimspekingar biðji einfaldlega stöðugt þjóninn um aðgang að gafflunum. Þeir. Nú munu heimspekingar ekki bíða eftir gaffli í nágrenninu, heldur bíða eða spyrja þjóninn. Í fyrstu notum við aðeins notendarými fyrir þetta; í því notum við ekki truflanir til að kalla fram neinar verklagsreglur frá kjarnanum (meira um þær hér að neðan).

Notendarýmislausnir

Hér munum við gera það sama og við gerðum áður með einum gaffli og tveimur heimspekingum, við munum snúast í lykkju og bíða. En nú verða það allir heimspekingar og sem sagt aðeins einn gaffli, þ.e. við getum sagt að aðeins heimspekingurinn sem tók þennan „gullna gaffal“ af þjóninum mun borða. Til að gera þetta notum við 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 þetta er blokkari, með í grófum dráttum það sama while(true) { if (!lock) break; }, en með enn meiri „töfrum“ en í SpinWait (sem er notað þar). Nú kann hann að telja þá sem bíða, svæfa þá aðeins og margt fleira. o.fl. Almennt séð gerir það allt sem hægt er til að hagræða. En við verðum að muna að þetta er enn sama virka lykkjan sem étur upp örgjörvaauðlindir og heldur þræði, sem getur leitt til hungurs ef einn af heimspekingunum verður í meiri forgangi en hinir, en er ekki með gullna gaffal (Forgang Inversion vandamál ). Þess vegna notum við það aðeins fyrir mjög stuttar breytingar á samnýttu minni, án símtala frá þriðja aðila, hreiðra læsinga eða annarra óvæntra.

Vel nærðir heimspekingar eða samkeppnishæf .NET forritun

Teikning fyrir SpinLock. Straumar eru stöðugt að „berjast“ um gullna gaffalinn. Bilanir eiga sér stað - auðkennda svæðið á myndinni. Kjarnarnir eru ekki notaðir að fullu: aðeins um 2/3 af þessum fjórum þráðum.

Önnur lausn hér væri að nota aðeins Interlocked.CompareExchange með sömu virku biðinni og sýnt er í kóðanum hér að ofan (hjá sveltandi heimspekingum), en þetta gæti, eins og áður hefur verið sagt, fræðilega leitt til blokkunar.

á Interlocked það er þess virði að segja að það er ekki aðeins CompareExchange, en einnig aðrar aðferðir við atómlestur OG ritun. Og með því að endurtaka breytinguna, ef öðrum þræði tekst að gera breytingar sínar (lesa 1, lesa 2, skrifa 2, skrifa 1 er slæmt), er hægt að nota hann fyrir flóknar breytingar á einu gildi (Interlocked Anything mynstur).

Kjarnaham lausnir

Til að forðast að eyða auðlindum í lykkju skulum við skoða hvernig á að loka á þráð. Með öðrum orðum, áframhaldandi dæmi okkar, skulum við sjá hvernig þjónninn svæfir heimspekinginn og vekur hann aðeins þegar þörf krefur. Fyrst skulum við skoða hvernig á að gera þetta í gegnum kjarnaham stýrikerfisins. Öll mannvirki þar enda oft hægari en þau sem eru í notendarými. Hægari nokkrum sinnum, til dæmis AutoResetEvent kannski 53 sinnum hægar SpinLock [Richter]. En með hjálp þeirra geturðu samstillt ferla yfir allt kerfið, stjórnað eða ekki.

Grunnhönnunin hér er semafór, sem Dijkstra lagði til fyrir meira en hálfri öld. Semafór er einfaldlega jákvæð heiltala sem stjórnað er af kerfinu og tvær aðgerðir á henni - auka og minnka. Ef það er ekki hægt að minnka núll, þá er hringingarþráðurinn læstur. Þegar fjöldinn er aukinn með einhverjum öðrum virkum þræði/ferli, þá eru þræðir liðnir og semafóran er aftur lækkuð um þann fjölda sem er liðinn. Þú getur ímyndað þér lestir í flöskuhálsi með semafór. .NET býður upp á nokkrar smíðar með svipaða virkni: AutoResetEvent, ManualResetEvent, Mutex og ég sjálfur Semaphore. Við munum nota AutoResetEvent, þetta er einfaldasta af þessum smíðum: aðeins tvö gildi 0 og 1 (ósatt, satt). Aðferð hennar WaitOne() lokar á kallþráðinn ef gildið var 0, og ef 1, lækkar það síðan í 0 og sleppir því. Aðferð Set() hækkar í 1 og hleypir einum manni í gegn, sem aftur lækkar í 0. Virkar eins og snúningshringur í neðanjarðarlestinni.

Við skulum flækja lausnina og nota blokkun fyrir hvern heimspeking, og ekki fyrir alla í einu. Þeir. Nú geta nokkrir heimspekingar borðað í einu, en ekki bara einn. En við lokum aftur fyrir aðgang að borðinu til að taka gaffla á réttan hátt og forðast keppnisaðstæður.

// Для блокирования отдельного философа.
// Инициализируется: 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();
}

Til að skilja hvað er að gerast hér skaltu íhuga málið þegar heimspekingurinn tókst ekki að taka gafflana, þá verða aðgerðir hans sem hér segir. Hann bíður eftir aðgangi að borðinu. Eftir að hafa fengið það, reynir hann að taka gafflana. Gekk ekki upp. Það veitir aðgang að borðinu (gagnkvæm útilokun). Og hann fer framhjá „beygjuhringnum“ sínum (AutoResetEvent) (í fyrstu eru þau opin). Það fellur inn í hringrásina aftur, vegna þess hann er ekki með gaffla. Hann reynir að taka þá og stoppar við „beygjuhlífina“ sína. Einhver heppnari nágranni til hægri eða vinstri, sem hefur lokið við að borða, mun opna heimspekinginn okkar með því að „opna snúningshólfið sitt“. Heimspekingurinn okkar fer í gegnum það (og það lokar á eftir honum) í annað sinn. Reynir í þriðja sinn að taka gafflana. Vel heppnað. Og hann fer í gegnum snúningshjólið sitt til að borða hádegismat.

Þegar það eru tilviljunarkenndar villur í slíkum kóða (þær eru alltaf til), til dæmis, verður nágranni rangt tilgreindur eða sami hluturinn verður til AutoResetEvent fyrir alla (Enumerable.Repeat), þá munu heimspekingarnir bíða eftir hönnuðunum, því Að finna villur í slíkum kóða er frekar erfitt verkefni. Annað vandamál við þessa lausn er að hún tryggir ekki að einhver heimspekingur svelti ekki.

Hybrid lausnir

Við skoðuðum tvær aðferðir við samstillingu, þegar við höldum okkur í notendaham og snúumst í lykkju og þegar við lokum þræðinum í gegnum kjarnann. Fyrri aðferðin er góð fyrir stutta kubba, önnur fyrir langa. Oft þarf fyrst að bíða í stutta stund eftir að breyta breytist í lykkju, og loka síðan þræðinum þegar biðin er löng. Þessi nálgun er útfærð í svokölluðu. hybrid hönnun. Það hefur sömu smíðar og fyrir kjarnaham, en nú með notendaham lykkju: SemaphorSlim, ManualResetEventSlim o.fl. Vinsælasta hönnunin hér er Monitor, vegna þess í C# er vel þekkt lock setningafræði. Monitor þetta er sama semafóran með hámarksgildið 1 (mutex), en með stuðningi við bið í lykkju, endurtekningu, ástandsbreytilegt mynstur (nánar um það hér að neðan) o.s.frv. Skoðum lausn með henni.

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

Hér lokum við aftur á allt borðið fyrir aðgang að gafflunum, en nú opnum við alla þræði í einu, frekar en nágranna þegar einhver lýkur að borða. Þeir. fyrst borðar einhver og blokkar nágrannana, og þegar þessi er búinn, en vill strax borða aftur, fer hann inn í blokkina og vekur nágranna sína, því biðtími hennar er minni.

Þannig komumst við í veg fyrir stöðnun og hungursvelti einhvers heimspekings. Við notum lykkju til að bíða í stuttan tíma og loka þráðnum í langan tíma. Að opna alla í einu er hægara en ef aðeins nágranninn væri opnaður, eins og í lausninni með AutoResetEvent, en munurinn ætti ekki að vera mikill, því þræðir verða að vera í notendaham fyrst.

У lock setningafræði kemur óþægilega á óvart. Mælt með að nota Monitor beint [Richter] [Eric Lippert]. Eitt þeirra er það lock kemur alltaf út Monitor, jafnvel þó að það væri undantekning, og þá getur annar þráður breytt stöðu samnýtta minnisins. Í slíkum tilfellum er oft betra að fara í biðstöðu eða slíta forritinu á einhvern hátt á öruggan hátt. Annað sem kemur á óvart er að Monitor notar klukkublokkir (SyncBlock), sem eru til staðar í öllum hlutum. Þess vegna, ef óviðeigandi hlutur er valinn, geturðu auðveldlega fengið dauðalás (til dæmis ef þú læsir á innbyggðan streng). Við notum alltaf falinn hlut fyrir þetta.

Skilyrðisbreytilegt mynstrið gerir þér kleift að útfæra væntingar um flókið ástand á nákvæmari hátt. Í .NET er það ófullnægjandi, að mínu mati, vegna þess að... Í orði ættu að vera nokkrar biðraðir á nokkrum breytum (eins og í Posix Threads), en ekki á einum lás. Þá væri hægt að gera þær fyrir alla heimspekinga. En jafnvel í þessu formi gerir það þér kleift að stytta kóðann.

Margir heimspekingar eða async / await

Allt í lagi, nú getum við í raun lokað fyrir þræði. En hvað ef við eigum marga heimspekinga? 100? 10000? Til dæmis fengum við 100000 beiðnir til vefþjónsins. Það verður dýrt að búa til þráð fyrir hverja beiðni vegna þess svo margir þræðir verða ekki keyrðir samhliða. Aðeins eins margir röklegir kjarna verða keyrðir (ég er með 4). Og allir aðrir munu einfaldlega taka í burtu auðlindir. Ein lausn á þessu vandamáli er ósamstillt / bíður mynstur. Hugmyndin er sú að aðgerð haldi ekki þræði ef hún þarf að bíða eftir að eitthvað haldi áfram. Og þegar eitthvað gerist heldur það framkvæmd sinni aftur (en ekki endilega í sama þræði!). Í okkar tilviki munum við bíða eftir gaffli.

SemaphoreSlim hefur fyrir þessu WaitAsync() aðferð. Hér er útfærsla sem notar þetta mynstur.

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

Aðferð með async / await er þýtt í lævísa endanlegu ástandsvél, sem skilar strax innri sínu Task. Í gegnum það geturðu beðið eftir að aðferðin ljúki, hætt við hana og allt annað sem þú getur gert með Task. Inni í aðferðinni stjórnar ríkisvél framkvæmd. Niðurstaðan er sú að ef það er engin töf, þá er framkvæmdin samstillt, og ef svo er, þá losnar þráðurinn. Til að fá betri skilning á þessu er betra að skoða þessa ríkisvél. Þú getur búið til keðjur úr þessum async / await aðferðir.

Við skulum prófa það. Vinna 100 heimspekinga á vél með 4 rökfræðilegum kjarna, 8 sekúndur. Fyrri lausnin með Monitor framkvæmdi aðeins fyrstu 4 þræðina og keyrði alls ekki restina. Hver þessara 4 þráða var aðgerðalaus í um 2ms. Og ósamstilltur / bíða lausnin gerði allar 100, með að meðaltali 6.8 sekúndur í hverri bið. Auðvitað, í raunverulegum kerfum, er óásættanlegt að vera aðgerðalaus í 6 sekúndur og það er betra að vinna ekki svona margar beiðnir með þessum hætti. Lausnin með Monitor reyndist alls ekki skalanleg.

Ályktun

Eins og þú sérð af þessum litlu dæmum styður .NET margar samstillingar. Hins vegar er ekki alltaf augljóst hvernig á að nota þá. Ég vona að þessi grein hafi verið gagnleg. Við erum að klára þetta í bili, en það er enn margt áhugavert eftir, til dæmis þráðlaus söfn, TPL Dataflow, Reactive forritun, Software Transaction líkan, o.s.frv.

Heimildir

Heimild: www.habr.com

Bæta við athugasemd