Hyvin ruokitut filosofit tai kilpailukykyinen .NET-ohjelmointi

Hyvin ruokitut filosofit tai kilpailukykyinen .NET-ohjelmointi

Katsotaanpa, miten rinnakkais- ja rinnakkaisohjelmointi toimii .Netissä, esimerkkinä Philosophers Dining Problem. Suunnitelma on tämä, säikeiden/prosessien synkronoinnista toimijamalliin (seuraavissa osissa). Artikkeli voi olla hyödyllinen ensimmäiselle tutustumiselle tai tietojesi päivittämiseen.

Miksi se ylipäätään tehdään? Transistorit saavuttavat minimikokonsa, Mooren laki perustuu valonnopeuden rajoitukseen ja siksi lukumäärässä havaitaan kasvua, transistoreita voidaan valmistaa lisää. Samaan aikaan tiedon määrä kasvaa ja käyttäjät odottavat välitöntä vastausta järjestelmiltä. Tällaisessa tilanteessa "normaali" ohjelmointi, kun meillä on yksi suoritussäie, ei ole enää tehokasta. Sinun on jotenkin ratkaistava samanaikaisen tai samanaikaisen suorituksen ongelma. Lisäksi tämä ongelma esiintyy eri tasoilla: säikeiden tasolla, prosessien tasolla, verkon koneiden tasolla (hajautetut järjestelmät). .NET:llä on korkealaatuisia, ajan testattuja tekniikoita tällaisten ongelmien nopeaan ja tehokkaaseen ratkaisemiseen.

Tehtävä

Edsger Dijkstra esitti tämän ongelman opiskelijoilleen jo vuonna 1965. Vakiintunut muotoilu on seuraava. Filosofeja on tietty määrä (yleensä viisi) ja haarukoita sama määrä. He istuvat pyöreän pöydän ääressä haarukoiden välissä. Filosofit voivat syödä loputtomasti lautaseltaan, ajatella tai odottaa. Jos haluat syödä filosofin, sinun on otettava kaksi haarukkaa (viimeinen jakaa haarukan ensimmäisen kanssa). Haarukan nostaminen ja laskeminen ovat kaksi erillistä toimenpidettä. Kaikki filosofit ovat hiljaa. Tehtävänä on löytää sellainen algoritmi, että kaikki ajattelisivat ja olisivat täynnä vielä 54 vuoden jälkeenkin.

Yritetään ensin ratkaista tämä ongelma käyttämällä jaettua tilaa. Haarukat ovat yhteisellä pöydällä ja filosofit yksinkertaisesti ottavat ne, kun ne ovat, ja laittavat ne takaisin. Tässä on synkronointiongelmia, milloin tarkalleen ottaen varmistetaan? entä jos ei ole haarukkaa? jne. Mutta ensin aloitetaan filosofit.

Säikeiden aloittamiseen käytämme läpivientiä Task.Run menetelmä:

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

Säievarasto on suunniteltu optimoimaan säikeiden luominen ja poistaminen. Tässä poolissa on tehtävien jono, ja CLR luo tai poistaa säikeitä näiden tehtävien lukumäärän mukaan. Yksi pooli kaikille AppDomaineille. Tätä uima-allasta tulisi käyttää melkein aina, koska. ei tarvitse huolehtia säikeiden, niiden jonojen jne. luomisesta, poistamisesta. Se on mahdollista ilman poolia, mutta silloin sinun on käytettävä sitä suoraan Thread, tämä on hyödyllistä tapauksissa, joissa sinun on muutettava säikeen prioriteettia, kun toimintamme on pitkä, etualan säiettä varten jne.

Другими словами, System.Threading.Tasks.Task luokka on sama Thread, mutta kaikenlaisilla mukavuuksilla: mahdollisuus suorittaa tehtävä joukon muita tehtäviä jälkeen, palauttaa ne toiminnoista, keskeyttää ne kätevästi ja paljon muuta. jne. Niitä tarvitaan tukemaan async / await -rakenteita (Task-based Asynchronous Pattern, syntaktinen sokeri IO-toimintojen odottamiseen). Puhumme tästä myöhemmin.

CancelationTokenSource tässä sitä tarvitaan, jotta säie voi lopettaa itsensä kutsuvan säikeen signaaliin.

Synkronointiongelmat

Estetyt filosofit

Okei, osaamme luoda säikeitä, yritetään syödä lounasta:

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

Täällä yritämme ensin ottaa vasen ja sitten oikea haarukka, ja jos se onnistuu, syömme ja laitamme ne takaisin. Yhden haarukan ottaminen on atomista, ts. kaksi lankaa ei voi ottaa yhtä yhtä aikaa (väärin: ensimmäinen lukee, että haarukka on vapaa, toinen - myös, ensimmäinen vie, toinen ottaa). Tätä varten Interlocked.CompareExchange, joka tulee toteuttaa prosessorikäskyllä ​​(TSL, XCHG), joka lukitsee osan muistista atomin peräkkäistä lukemista ja kirjoittamista varten. Ja SpinWait vastaa konstruktia while(true) vain pienellä "taikuudella" - lanka vie prosessorin (Thread.SpinWait), mutta joskus siirtää ohjauksen toiseen säikeeseen (Thread.Yeild) tai nukahtaa (Thread.Sleep).

Mutta tämä ratkaisu ei toimi, koska virtaukset ovat pian (minulle sekunnissa) tukossa: kaikki filosofit ottavat vasenta haaraa, mutta eivät oikeaa. Forks-taulukolla on sitten arvot: 1 2 3 4 5.

Hyvin ruokitut filosofit tai kilpailukykyinen .NET-ohjelmointi

Kuvassa lukituskierteet (umpilukko). Vihreä - suoritus, punainen - synkronointi, harmaa - lanka nukkuu. Rombukset osoittavat tehtävien alkamisajan.

Filosofien nälkä

Vaikka ruokaa ei tarvitse ajatella erityisen paljon, mutta nälkä saa kenet tahansa luopumaan filosofiasta. Yritetään simuloida ongelmamme lankojen nälkätilannetta. Nälkä on kun lanka on käynnissä, mutta ilman merkittävää työtä, toisin sanoen tämä on sama umpikuja, vain nyt lanka ei nuku, vaan etsii aktiivisesti syötävää, mutta ruokaa ei ole. Toistuvan tukkeutumisen välttämiseksi asetamme haarukan takaisin, jos emme voineet ottaa toista.

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

Tärkeää tässä koodissa on, että kaksi neljästä filosofista unohtaa laskea vasemman haarukkansa. Ja käy ilmi, että he syövät enemmän ruokaa, kun taas toiset alkavat nälkiä, vaikka langoilla on sama prioriteetti. Täällä he eivät ole täysin nälkäisiä, koska. huonot filosofit laittavat joskus haarukkansa takaisin. Osoittautuu, että hyvät ihmiset syövät noin viisi kertaa vähemmän kuin huonot. Joten pieni virhe koodissa johtaa suorituskyvyn laskuun. Tässä on myös syytä huomata, että harvinainen tilanne on mahdollinen, kun kaikki filosofit ottavat vasemman haarukan, oikeaa ei ole, he laittavat vasemman, odottavat, ottavat jälleen vasemman jne. Tämä tilanne on myös nälkä, enemmän kuin umpikuja. En onnistunut toistamaan sitä. Alla on kuva tilanteesta, jossa kaksi huonoa filosofia on ottanut molemmat haarukat ja kaksi hyvää näkee nälkää.

Hyvin ruokitut filosofit tai kilpailukykyinen .NET-ohjelmointi

Täällä voit nähdä, että säikeet heräävät joskus ja yrittävät saada resurssia. Kaksi neljästä ytimestä ei tee mitään (vihreä kaavio yllä).

Filosofin kuolema

No, toinen ongelma, joka voi keskeyttää filosofien loistavan illallisen, on, jos joku heistä yhtäkkiä kuolee haarukat käsissään (ja he hautaavat hänet sillä tavalla). Sitten naapurit jäävät ilman illallista. Voit itse keksiä esimerkkikoodin tähän tapaukseen, esimerkiksi se heitetään ulos NullReferenceException sen jälkeen, kun filosofi ottaa haarukat. Ja muuten, poikkeusta ei käsitellä eikä kutsukoodi vain nappaa sitä (tätä varten AppDomain.CurrentDomain.UnhandledException jne.). Siksi virhekäsittelijöitä tarvitaan itse säikeissä ja sulavalla lopetuksella.

tarjoilija

Okei, kuinka ratkaisemme tämän umpikujan, nälänhädän ja kuoleman ongelman? Annamme vain yhden filosofin päästä haarukoille, lisäämme lankojen vastavuoroisen poissulkemisen tälle paikalle. Kuinka tehdä se? Oletetaan, että filosofien vieressä seisoo tarjoilija, joka antaa kenelle tahansa filosofille luvan ottaa haarukat. Kuinka teemme tämän tarjoilijan ja miten filosofit kysyvät häneltä, kysymykset ovat mielenkiintoisia.

Yksinkertaisin tapa on, kun filosofit yksinkertaisesti pyytävät jatkuvasti tarjoilijalta pääsyä haarukoille. Nuo. nyt filosofit eivät odota haarukkaa lähellä, vaan odottavat tai kysyvät tarjoilijalta. Aluksi käytämme tähän vain User Spacea, siinä emme käytä keskeytyksiä kutsuaksemme proseduureja ytimestä (niistä alla).

Ratkaisut käyttäjätilassa

Täällä teemme samoin kuin aiemmin yhden haarukan ja kahden filosofin kanssa, pyörimme syklissä ja odotamme. Mutta nyt se on kaikki filosofeja ja ikään kuin vain yksi haarukka, ts. voidaan sanoa, että vain se filosofi, joka otti tarjoilijalta tämän "kultaisen haarukan", syö. Tätä varten käytämme SpinLockia.

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 tämä on esto, jolla on karkeasti sanottuna sama while(true) { if (!lock) break; }, mutta vielä enemmän "taikuutta" kuin sisällä SpinWait (jota siellä käytetään). Nyt hän osaa laskea odottavat, nukuttaa heidät ja paljon muuta. jne. Yleensä tekee kaiken mahdollisen optimoidakseen. Mutta meidän on muistettava, että tämä on edelleen sama aktiivinen sykli, joka syö prosessorin resursseja ja ylläpitää virtausta, mikä voi johtaa nälkään, jos joku filosofeista tulee etusijalle kuin toiset, mutta hänellä ei ole kultahaarukkaa (Priority Inversion -ongelma) . Siksi käytämme sitä vain erittäin lyhyisiin jaetun muistin muutoksiin ilman kolmansien osapuolien puheluita, sisäkkäisiä lukkoja ja muita yllätyksiä.

Hyvin ruokitut filosofit tai kilpailukykyinen .NET-ohjelmointi

Piirustus varten SpinLock. Virrat "taistelevat" jatkuvasti kultaisen haarukan puolesta. On vikoja - kuvassa valittu alue. Ytimet eivät ole täysin käytössä: vain noin 2/3 näistä neljästä säikeestä.

Toinen ratkaisu tässä olisi käyttää vain Interlocked.CompareExchange samalla aktiivisella odotuksella kuin yllä olevassa koodissa (nälkäisten filosofien kohdalla), mutta tämä, kuten jo sanottiin, voi teoriassa johtaa tukkoon.

Про Interlocked On huomattava, että ei ole vain CompareExchange, mutta myös muita menetelmiä atomien lukemiseen JA kirjoittamiseen. Ja muutoksen toiston kautta, jos toisella säikeellä on aikaa tehdä muutokset (lue 1, lue 2, kirjoita 2, kirjoitus 1 on huono), sitä voidaan käyttää monimutkaisiin muutoksiin yksittäiseen arvoon (Interlocked Anything -malli).

Ydintilan ratkaisut

Jotta vältetään resurssien tuhlaaminen silmukassa, katsotaanpa, kuinka voimme estää säiettä. Toisin sanoen, esimerkkiämme jatkettaessa katsotaan kuinka tarjoilija nukuttaa filosofin ja herättää hänet vain tarvittaessa. Katsotaanpa ensin, kuinka tämä tehdään käyttöjärjestelmän ydintilan kautta. Kaikki siellä olevat rakenteet ovat usein hitaampia kuin käyttäjätilassa. Esimerkiksi useita kertoja hitaammin AutoResetEvent ehkä 53 kertaa hitaampi SpinLock [Richter]. Mutta heidän avullaan voit synkronoida prosesseja koko järjestelmän sisällä, hallinnassa tai ei.

Perusrakenne tässä on Dijkstran yli puoli vuosisataa sitten ehdottama semafori. Semafori on yksinkertaisesti sanottuna järjestelmän hallitsema positiivinen kokonaisluku ja siihen kaksi operaatiota, inkrementti ja vähennys. Jos se ei pienene, nolla, kutsusäie on estetty. Kun lukua kasvatetaan jollain muulla aktiivisella säikeellä/prosessilla, säikeet ohitetaan ja semaforia taas pienennetään ohitetulla numerolla. Voidaan kuvitella junia pullonkaulassa semaforin kanssa. .NET tarjoaa useita rakenteita, joilla on samanlaiset toiminnot: AutoResetEvent, ManualResetEvent, Mutex ja minä Semaphore. Me käytämme AutoResetEvent, tämä on yksinkertaisin näistä rakenteista: vain kaksi arvoa 0 ja 1 (false, true). Hänen menetelmänsä WaitOne() estää kutsuvan säikeen, jos arvo oli 0, ja jos arvo on 1, laskee sen nollaan ja ohittaa sen. Menetelmä Set() korottaa 1:een ja päästää yhden tarjoilijan läpi, joka taas laskee 0:aan. Toimii kuin metron kääntöportti.

Monimutkaistaan ​​ratkaisua ja käytämme lukkoa jokaiselle filosofille, ei kaikille kerralla. Nuo. nyt voi olla useita filosofeja kerralla, eikä yhtä. Mutta estämme jälleen pääsyn pöytään, jotta voimme oikein, välttäen kilpailuja (kisaolosuhteet), ottaa varmuudet.

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

Ymmärtääksesi, mitä täällä tapahtuu, harkitse tapausta, jossa filosofi epäonnistui ottamaan haarukat, niin hänen toimintansa ovat seuraavat. Hän odottaa pääsyä pöytään. Saatuaan sen hän yrittää ottaa haarukat. Ei toiminut. Se antaa pääsyn taulukkoon (keskinäinen poissulkeminen). Ja ohittaa "kääntöporttinsa" (AutoResetEvent) (ne ovat aluksi auki). Se joutuu taas kiertoon, koska hänellä ei ole haarukoita. Hän yrittää ottaa ne ja pysähtyy "kääntöportilleen". Joku onnellisempi oikealla tai vasemmalla oleva naapuri, kun hän on syönyt, avaa filosofimme lukituksen "avaamalla kääntöporttinsa". Filosofimme ohittaa sen (ja se sulkeutuu sen taakse) toisen kerran. Hän yrittää kolmatta kertaa ottaa haarukat. Onnea. Ja hän välittää kääntöporttinsa syömään.

Kun tällaisessa koodissa on satunnaisia ​​virheitä (niitä on aina olemassa), esimerkiksi naapuri on määritetty väärin tai luodaan sama objekti AutoResetEvent kaikille (Enumerable.Repeat), sitten filosofit odottavat kehittäjiä, koska Virheiden löytäminen tällaisesta koodista on melko vaikea tehtävä. Toinen ongelma tässä ratkaisussa on, että se ei takaa, etteikö joku filosofi jää nälkäiseksi.

Hybridiratkaisut

Olemme tarkastelleet kahta lähestymistapaa ajoitukseen, kun pysymme käyttäjätilassa ja silmukassa ja kun estämme säiettä ytimen läpi. Ensimmäinen menetelmä sopii lyhyille lukoille, toinen pitkille. Usein on ensin odotettava hetken, että muuttuja muuttuu silmukassa, ja sitten lukita lanka, kun odotus on pitkä. Tämä lähestymistapa on toteutettu ns. hybridirakenteet. Tässä on samat rakenteet kuin ydintilassa, mutta nyt käyttäjätilasilmukalla: SemaphorSlim, ManualResetEventSlim jne. Suosituin malli täällä on Monitor, koska C#:ssa on hyvin tunnettu lock syntaksi. Monitor tämä on sama semafori, jonka maksimiarvo on 1 (mutex), mutta tukee silmukassa odottamista, rekursiota, ehtomuuttujakuviota (lisätietoja alla) jne. Katsotaanpa ratkaisua sen kanssa.

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

Täällä estämme taas koko pöydän päästäkseen käsiksi haarukoille, mutta nyt vapautamme kaikki viestiketjut kerralla, emme naapureita, kun joku lopettaa syömisen. Nuo. Ensin joku syö ja estää naapurit, ja kun tämä joku lopettaa, mutta haluaa syödä heti uudestaan, hän menee tukkoon ja herättää naapurit, koska. sen odotusaika on lyhyempi.

Näin vältymme umpikujalta ja jonkun filosofin nälkään. Odotamme silmukkaa ja suljemme langan pitkän ajan. Kaikkien lukituksen poistaminen kerralla on hitaampaa kuin jos vain naapuri olisi vapautettu, kuten ratkaisussa AutoResetEvent, mutta eron ei pitäisi olla suuri, koska säikeiden on ensin pysyttävä käyttäjätilassa.

У lock syntaksissa on ikäviä yllätyksiä. Suosittele käyttöä Monitor suoraan [Richter] [Eric Lippert]. Yksi niistä on se lock aina pois Monitor, vaikka kyseessä olisi poikkeus, jolloin toinen säie voisi muuttaa jaetun muistin tilaa. Tällaisissa tapauksissa on usein parempi mennä umpikujaan tai jollakin tavalla lopettaa ohjelma turvallisesti. Toinen yllätys on, että Monitor käyttää synkronointilohkoja (SyncBlock), joita on kaikissa esineissä. Siksi, jos valitset sopimattoman objektin, voit helposti saada umpikujan (esimerkiksi jos lukitset sisäänrakennetun merkkijonon). Käytämme tähän aina piilotettua esinettä.

Condition Variable -mallin avulla voit toteuttaa tiiviimmin jonkin monimutkaisen ehdon odotuksen. NET:ssä se on mielestäni epätäydellinen, koska teoriassa pitäisi olla useita jonoja useilla muuttujilla (kuten Posix Threadsissa), ei yhdessä lokissa. Sitten niitä voitaisiin tehdä kaikille filosofeille. Mutta jopa tässä muodossa sen avulla voit pienentää koodia.

monet filosofit tai async / await

Okei, nyt voimme tehokkaasti estää viestiketjut. Mutta entä jos meillä on paljon filosofeja? 100? 10000 100000? Saimme esimerkiksi 4 XNUMX pyyntöä verkkopalvelimelle. Jokaista pyyntöä varten säikeen luominen on ylimääräistä, koska niin monet säikeet eivät kulje rinnakkain. Käynnistyy vain niin monta kuin on loogisia ytimiä (minulla on XNUMX). Ja kaikki muut vievät vain resursseja. Yksi ratkaisu tähän ongelmaan on async/wait-kuvio. Sen ideana on, että funktio ei pidä säiettä, jos sen on odotettava, että jokin jatkuu. Ja kun se tekee jotain, se jatkaa sen suorittamista (mutta ei välttämättä samassa säikeessä!). Meidän tapauksessamme odotamme haarukkaa.

SemaphoreSlim on tätä varten WaitAsync() menetelmä. Tässä on tätä mallia käyttävä toteutus.

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

Menetelmä kanssa async / await on käännetty hankalaksi tilakoneeksi, joka palauttaa välittömästi sisäisen Task. Sen kautta voit odottaa menetelmän valmistumista, peruuttaa sen ja kaiken muun, mitä voit tehdä Taskilla. Metodin sisällä tilakone ohjaa suoritusta. Tärkeintä on, että jos viivettä ei ole, suoritus on synkroninen, ja jos on, säie vapautetaan. Tämän paremman ymmärtämiseksi on parempi tarkastella tätä tilakonetta. Näistä voit luoda ketjuja async / await menetelmiä.

Testataan. 100 filosofin työ koneella, jossa on 4 loogista ydintä, 8 sekuntia. Edellinen Monitorin ratkaisu juoksi vain ensimmäiset 4 säiettä ja loput eivät toimineet ollenkaan. Jokainen näistä 4 säikeestä oli käyttämättömänä noin 2 ms. Ja async/wait-ratkaisu suoritti kaikki 100 ja keskimääräinen odotusaika oli 6.8 sekuntia. Tietenkin todellisissa järjestelmissä 6 sekunnin tyhjäkäynti ei ole hyväksyttävää, ja on parempi olla käsittelemättä niin monia tällaisia ​​pyyntöjä. Ratkaisu Monitorilla ei osoittautunut ollenkaan skaalautuvaksi.

Johtopäätös

Kuten näistä pienistä esimerkeistä näet, .NET tukee monia synkronointirakenteita. Aina ei kuitenkaan ole selvää, miten niitä käytetään. Toivottavasti tästä artikkelista oli apua. Toistaiseksi tämä on loppu, mutta paljon mielenkiintoista on vielä jäljellä, esimerkiksi säikeen varmat kokoelmat, TPL Dataflow, Reaktiivinen ohjelmointi, Software Transaction -malli jne.

lähteet

Lähde: will.com

Lisää kommentti