Hästi toidetud filosoofid või konkurentsivõimeline .NET-i programmeerimine

Hästi toidetud filosoofid või konkurentsivõimeline .NET-i programmeerimine

Vaatame lõunasöögifilosoofide probleemi näitel, kuidas töötab .Netis samaaegne ja paralleelne programmeerimine. Plaan on järgmine, lõime/protsesside sünkroniseerimisest näitleja mudelini (järgmistes osades). Artikkel võib olla kasulik esmatutvuse jaoks või teadmiste värskendamiseks.

Miks üldse teada, kuidas seda teha? Transistorid on saavutamas oma miinimumsuurust, Moore'i seadus jõuab valguse kiiruse piirini ja seetõttu on märgata arvu kasvu, transistore saab teha rohkem. Samal ajal andmete hulk kasvab ja kasutajad ootavad süsteemidelt kohest reageerimist. Sellises olukorras ei ole "tavaline" programmeerimine, kui meil on üks täitevlõng, enam efektiivne. Peame kuidagi lahendama samaaegse või samaaegse täitmise probleemi. Veelgi enam, see probleem esineb erinevatel tasanditel: lõime tasemel, protsessi tasemel, võrgus olevate masinate tasemel (hajutatud süsteemid). .NET-il on selliste probleemide kiireks ja tõhusaks lahendamiseks kvaliteetsed ajaproovitud tehnoloogiad.

Ülesanne

Edsger Dijkstra esitas selle probleemi oma õpilastele juba 1965. aastal. Väljakujunenud sõnastus on järgmine. Filosoofe on teatud (tavaliselt viis) arv ja kahvleid sama palju. Nad istuvad ümmarguse laua taga, kahvlid nende vahel. Filosoofid võivad oma taldrikutelt lõputut toitu süüa, mõelda või oodata. Söömiseks peab filosoof võtma kaks kahvlit (viimane jagab esimesega kahvlit). Kahvli ülesvõtmine ja maha panemine on kaks eraldi toimingut. Kõik filosoofid vaikivad. Ülesanne on leida selline algoritm, et nad kõik mõtleksid ja oleksid hästi toidetud ka 54 aasta pärast.

Esmalt proovime seda probleemi lahendada jagatud ruumi abil. Kahvlid lebavad ühisel laual ja filosoofid võtavad need lihtsalt kohal olles ja panevad tagasi. Siin tekivadki probleemid sünkroniseerimisega, millal täpselt kahvlid võtta? mida teha, kui pistikut pole? jne. Aga kõigepealt alustame filosoofidest.

Lõimede alustamiseks kasutame lõimekogumi kaudu Task.Run meetod:

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

Keermekogum on loodud niitide loomise ja eemaldamise optimeerimiseks. Selles kogumis on ülesannete järjekord ja CLR loob või kustutab lõime olenevalt nende ülesannete arvust. Üks bassein kõigi AppDomainide jaoks. Seda basseini tuleks kasutada peaaegu alati, sest... pole vaja vaeva näha lõimede, nende järjekordade jms loomise ja kustutamisega. Saate seda teha ka ilma basseinita, kuid siis peate seda otse kasutama Thread, see on kasulik juhtudel, kui peame muutma lõime prioriteeti, kui meil on pikk toiming, esiplaani lõime jaoks jne.

Teisisõnu, System.Threading.Tasks.Task klass on sama Thread, kuid kõikvõimalike mugavustega: võimalus käivitada ülesanne pärast teiste ülesannete plokki, tagastada need funktsioonidest, mugavalt katkestada ja palju muud. jne. Neid on vaja asünkrooni/ootama konstruktsioonide toetamiseks (Task-based Asynchronous Pattern, süntaktiline suhkur IO-operatsioonide ootamiseks). Sellest räägime hiljem.

CancelationTokenSource siin on vajalik, et lõim saaks kutsuva lõime signaali korral ise lõpetada.

Sünkroonimisprobleemid

Blokeeritud filosoofid

Olgu, me teame, kuidas lõime luua, proovime lõunat:

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

Siin proovime kõigepealt võtta vasaku ja siis parema kahvli ja kui see töötab, siis sööme ja paneme need tagasi. Ühe kahvli võtmine on aatomiline, st. kahte niiti ei saa korraga võtta (vale: esimene loeb, et kahvel on vaba, teine ​​teeb sama, esimene võtab, teine ​​võtab). Selle jaoks Interlocked.CompareExchange, mis tuleb realiseerida protsessori juhiste abil (TSL, XCHG), mis lukustab mälutüki aatomite järjestikuseks lugemiseks ja kirjutamiseks. Ja SpinWait on samaväärne ehitusega while(true) ainult väikese "maagiaga" - niit võtab protsessori (Thread.SpinWait), kuid mõnikord annab juhtimise üle teisele lõimele (Thread.Yeild) või jääb magama (Thread.Sleep).

Kuid see lahendus ei tööta, sest... niidid varsti (minu jaoks sekundi jooksul) on blokeeritud: kõik filosoofid võtavad vasaku haru, kuid paremat pole. Kahvlimassiivil on siis järgmised väärtused: 1 2 3 4 5.

Hästi toidetud filosoofid või konkurentsivõimeline .NET-i programmeerimine

Pildil blokeerivad niidid (sulk). Roheline tähistab täitmist, punane tähistab sünkroonimist ja hall näitab, et niit magab. Teemandid näitavad ülesannete käivitamise aega.

Filosoofide nälg

Kuigi mõtlemiseks pole vaja palju süüa, võib nälg sundida kedagi filosoofiast loobuma. Proovime oma probleemis simuleerida niidinälgimise olukorda. Nälgimine on see, kui niit töötab, kuid ilma märkimisväärse tööta, teisisõnu, see on sama ummikseisus, ainult et nüüd niit ei maga, vaid otsib aktiivselt midagi süüa, kuid süüa pole. Sagedase blokeerimise vältimiseks paneme kahvli tagasi, kui teist võtta ei saanud.

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

Selle koodi puhul on oluline see, et kaks neljast filosoofist unustavad vasaku kahvli maha panna. Ja selgub, et nad söövad rohkem toitu ja teised hakkavad nälgima, kuigi niidid on sama prioriteediga. Siin nad päris nälga ei jää, sest... halvad filosoofid panevad mõnikord kahvlid tagasi. Selgub, et head söövad umbes 5 korda vähem kui halvad. Nii et väike viga koodis viib jõudluse languseni. Siinkohal tasub ka tähele panna, et võimalik on harukordne olukord, kui kõik filosoofid võtavad vasakpoolse kahvli, õiget pole, panevad vasaku maha, ootavad, võtavad uuesti vasaku jne. See olukord on ka nälgimine, pigem vastastikune blokeering. Ma ei suutnud seda korrata. Allpool on pilt olukorra kohta, kus kaks halba filosoofi on võtnud mõlemad kahvlid ja kaks head nälgivad.

Hästi toidetud filosoofid või konkurentsivõimeline .NET-i programmeerimine

Siin näete, et lõimed ärkavad mõnikord üles ja proovivad ressurssi hankida. Kaks neljast tuumast ei tee midagi (ülalpool on roheline graafik).

Filosoofi surm

Noh, veel üks probleem, mis võib filosoofide uhke õhtusöögi katkestada, on see, kui üks neist äkki sureb, kahvlid käes (ja nii ta maetakse). Siis jäävad naabrid lõunata. Näidiskoodi saad selle juhtumi jaoks ise välja mõelda, näiteks visatakse ära NullReferenceException pärast seda, kui filosoof võtab kahvlid. Ja muide, erandit ei käsitleta ja helistamiskood ei saa seda lihtsalt kinni (selleks AppDomain.CurrentDomain.UnhandledException ja jne). Seetõttu on veakäsitlejaid vaja lõimedes endas ja graatsilise lõpetamisega.

Kelner

Olgu, kuidas me lahendame selle ummikseisu, nälgimise ja surmade probleemi? Lubame hargidesse ainult ühe filosoofi ja lisame selle koha jaoks lõimede vastastikuse välistamise. Kuidas seda teha? Oletame, et filosoofide kõrval on kelner, kes annab ühele filosoofile loa kahvlid võtta. Huvitavad küsimused on, kuidas me peaksime seda kelnerit valmistama ja kuidas filosoofid teda küsivad.

Lihtsaim viis on, et filosoofid küsivad kelnerilt pidevalt juurdepääsu kahvlitele. Need. Nüüd ei hakka filosoofid ootama kahvlit läheduses, vaid ootavad või küsivad kelnerilt. Algul kasutame selleks ainult kasutajaruumi; selles ei kasuta me tuumast ühegi protseduuri kutsumiseks katkestusi (nende kohta lähemalt allpool).

Kasutajaruumi lahendused

Siin teeme sama, mida tegime varem ühe kahvli ja kahe filosoofiga, keerutame ringi ja ootame. Nüüd on aga kõik filosoofid ja justkui ainult üks hark, s.t. võime öelda, et sööb ainult see filosoof, kes selle kelnerilt selle “kuldse kahvli” võttis. Selleks kasutame SpinLocki.

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 see on blokeerija, jämedalt öeldes sama while(true) { if (!lock) break; }, kuid veelgi suurema maagiaga kui aastal SpinWait (mida seal kasutatakse). Nüüd teab ta, kuidas ootajaid üles lugeda, neid veidi magama panna ja palju muud. jne. Üldiselt teeb see optimeerimiseks kõik võimaliku. Kuid me peame meeles pidama, et see on ikkagi sama aktiivne silmus, mis sööb protsessori ressursse ja hoiab endas lõime, mis võib põhjustada nälga, kui üks filosoofidest muutub teistest prioriteetsemaks, kuid tal pole kuldset kahvlit (Priority Inversion problem ). Seetõttu kasutame seda ainult väga lühikeste ühismälu muudatuste tegemiseks, ilma kolmandate osapoolte kõnede, pesastatud lukkude või muude üllatusteta.

Hästi toidetud filosoofid või konkurentsivõimeline .NET-i programmeerimine

Joonistamine jaoks SpinLock. Ojad “võitlevad” pidevalt kuldse kahvli pärast. Esinevad tõrked – joonisel esiletõstetud ala. Südamikud pole täielikult ära kasutatud: ainult umbes 2/3 nendest neljast niidist.

Teine lahendus siin oleks ainult kasutamine Interlocked.CompareExchange sama aktiivse ootamisega, nagu on näidatud ülaltoodud koodis (nälgivatel filosoofidel), kuid see, nagu juba öeldud, võib teoreetiliselt viia blokeerimiseni.

edasi Interlocked tasub öelda, et pole mitte ainult CompareExchange, aga ka muid meetodeid aatomi lugemiseks JA kirjutamiseks. Ja muudatust korrates, kui mõnel teisel lõimel õnnestub oma muudatused teha (loe 1, loe 2, kirjuta 2, kirjuta 1 on halb), saab seda kasutada ühe väärtuse keerukateks muutmiseks (Interlocked Anything muster).

Kerneli režiimi lahendused

Ressursside raiskamise vältimiseks silmuses vaatame, kuidas lõime blokeerida. Ehk siis oma näidet jätkates vaatame, kuidas kelner filosoofi magama paneb ja ainult vajaduse korral üles äratab. Kõigepealt vaatame, kuidas seda teha operatsioonisüsteemi kerneli režiimi kaudu. Kõik sealsed struktuurid on sageli aeglasemad kui kasutajaruumis olevad struktuurid. Näiteks mitu korda aeglasemalt AutoResetEvent võib-olla 53 korda aeglasem SpinLock [Richter]. Kuid nende abiga saate kogu süsteemi protsesse sünkroonida, olenemata sellest, kas seda hallatakse või mitte.

Põhikujundus on siin semafor, mille Dijkstra pakkus välja rohkem kui pool sajandit tagasi. Lihtsamalt öeldes on semafor positiivne täisarv, mida kontrollib süsteem, ja sellel on kaks toimingut - suurendamine ja vähendamine. Kui nulli pole võimalik vähendada, on kutsuv lõim blokeeritud. Kui arvu suurendatakse mõne muu aktiivse lõime/protsessiga, siis lõimed läbitakse ja semafori vähendatakse uuesti läbitud arvu võrra. Võite ette kujutada ronge, mis on semaforiga pudelikaelas. .NET pakub mitmeid sarnaste funktsioonidega konstruktsioone: AutoResetEvent, ManualResetEvent, Mutex ja mina ise Semaphore. Me kasutame AutoResetEvent, see on nendest konstruktsioonidest kõige lihtsam: ainult kaks väärtust 0 ja 1 (vale, tõene). Tema meetod WaitOne() blokeerib kutsuva lõime, kui väärtus oli 0, ja kui 1, siis alandab selle 0-ks ja jätab selle vahele. Meetod Set() suureneb 1-ni ja laseb läbi ühe inimese, kes jälle väheneb 0-ni. Toimib nagu turnikee metroos.

Teeme lahenduse keerulisemaks ja kasutame blokeerimist iga filosoofi jaoks, mitte kõigi jaoks korraga. Need. Nüüd saab korraga süüa mitu filosoofi, mitte ainult üks. Kuid me blokeerime taas juurdepääsu lauale, et kahvlid õigesti haarata, vältides võistlustingimusi.

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

Siin toimuva mõistmiseks mõelge juhtumile, kui filosoofil ei õnnestunud kahvleid võtta, siis on tema tegevus järgmine. Ta ootab lauale pääsu. Selle kätte saanud, püüab ta kahvleid võtta. Ei tulnud välja. See annab juurdepääsu lauale (vastastikune välistamine). Ja ta möödub oma "turnikust" (AutoResetEvent) (algul on need avatud). See langeb uuesti tsüklisse, sest tal pole kahvleid. Ta proovib neid võtta ja peatub oma "turnike" juures. Mõni õnnelikum naaber paremal või vasakul, olles söömise lõpetanud, vabastab meie filosoofi blokeeringu, "avades oma turnikee". Meie filosoof läbib selle (ja see sulgub tema selja taga) teist korda. Proovib kolmandat korda kahvleid võtta. Edukas. Ja ta läheb oma turnikesest läbi, et lõunat süüa.

Kui sellises koodis esineb juhuslikke vigu (need on alati olemas), siis näiteks määratakse naaber valesti või luuakse sama objekt AutoResetEvent kõigi jaoks (Enumerable.Repeat), siis jäävad filosoofid arendajaid ootama, sest Sellises koodis vigade leidmine on üsna keeruline ülesanne. Selle lahenduse teine ​​probleem on see, et see ei garanteeri, et mõni filosoof nälga ei jää.

Hübriidlahendused

Vaatlesime kahte sünkroonimisviisi, kui jääme kasutajarežiimi ja keerleme tsüklis ning kui blokeerime lõime läbi kerneli. Esimene meetod sobib lühikeste, teine ​​pikkade plokkide jaoks. Sageli peate esmalt hetkeks ootama, kuni muutuja tsüklis muutub, ja seejärel lõime blokeerida, kui ootamine on pikk. Seda lähenemist rakendatakse nn. hübriidsed kujundused. Sellel on samad konstruktsioonid nagu kerneli režiimil, kuid nüüd koos kasutajarežiimi tsükliga: SemaphorSlim, ManualResetEventSlim jne. Kõige populaarsem disain siin on Monitor, sest C#-s on tuntud lock süntaks. Monitor see on sama semafor, mille maksimaalne väärtus on 1 (mutex), kuid toetab tsüklis ootamist, rekursiooni, tingimusmuutuja mustrit (sellest lähemalt allpool) jne. Vaatame sellega lahendust.

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

Siin blokeerime taas terve laua kahvlitele juurdepääsu, kuid nüüd blokeerime kõik lõimed korraga, mitte aga naabrite blokeeringu, kui keegi söömise lõpetab. Need. Esiteks, keegi sööb ja blokeerib naabreid ja kui see keegi lõpetab, kuid tahab kohe uuesti süüa, läheb ta blokki ja äratab oma naabrid, sest selle ooteaeg on lühem.

Nii väldime ummikseisu ja mõne filosoofi nälgimist. Me kasutame silmust, et oodata lühikest aega ja blokeerida niit pikka aega. Kõigi korraga deblokeerimine on aeglasem kui siis, kui ainult naaber oleks blokeeringust vabastatud, nagu lahenduses AutoResetEvent, kuid vahe ei tohiks olla suur, sest lõimed peavad jääma esmalt kasutajarežiimi.

У lock süntaksil on ebameeldivaid üllatusi. Soovitatav kasutada Monitor otse [Richter] [Eric Lippert]. Üks neist on see lock tuleb alati välja Monitor, isegi kui oli erand, ja siis võib mõni teine ​​lõim ühismälu olekut muuta. Sellistel juhtudel on sageli parem minna ummikseisu või programm kuidagi turvaliselt lõpetada. Teine üllatus on see, et monitor kasutab kellaplokke (SyncBlock), mis esinevad kõikides objektides. Seega, kui valitakse sobimatu objekt, võite kergesti sattuda ummikseisu (näiteks kui lukustate interneeritud stringi). Kasutame selleks alati peidetud objekti.

Tingimuse muutuja muster võimaldab lühidalt ellu viia mõne keeruka tingimuse ootust. NET-is on see minu arvates puudulik, kuna... Teoreetiliselt peaks olema mitu järjekorda mitmel muutujal (nagu Posix Threadsis), mitte ühel lukul. Siis oleks võimalik neid teha kõikidele filosoofidele. Kuid isegi sellisel kujul võimaldab see koodi lühendada.

Paljud filosoofid või async / await

Olgu, nüüd saame lõimed tõhusalt blokeerida. Aga mis siis, kui meil on palju filosoofe? 100? 10000? Näiteks saime veebiserverisse 100000 4 päringut. Lõime loomine iga päringu jaoks on kulukas, sest nii palju lõime ei käivitata paralleelselt. Käivitatakse ainult nii palju loogilisi südamikke (mul on XNUMX). Ja kõik teised võtavad lihtsalt ressursid ära. Üks lahendus sellele probleemile on asünkroonimis- / ootamismuster. Selle idee seisneb selles, et funktsioon ei hoia lõime, kui ta peab ootama, kuni midagi jätkub. Ja kui midagi juhtub, jätkab see täitmist (aga mitte tingimata samas lõimes!). Meie puhul ootame kahvlit.

SemaphoreSlim on selleks WaitAsync() meetod. Siin on seda mustrit kasutav teostus.

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

Meetod koos async / await on tõlgitud kavalaks lõpliku oleku masinaks, mis tagastab kohe oma sisemise Task. Selle kaudu saate oodata meetodi lõpuleviimist, selle tühistada ja kõike muud, mida saate ülesandega teha. Meetodi sees juhib täitmist olekumasin. Põhimõte on see, et kui viivitust pole, on täitmine sünkroonne ja kui on, siis lõng vabastatakse. Selle paremaks mõistmiseks on parem vaadata seda olekumasinat. Nendest saate luua ahelaid async / await meetodid.

Testime seda. 100 filosoofi töö 4 loogilise tuumaga masinal, 8 sekundit. Eelmine lahendus Monitoriga käivitas ainult esimesed 4 lõime ja ülejäänud ei käivitanud üldse. Kõik need 4 lõime olid jõude umbes 2 ms. Ja asünkroonimise / ootamise lahendus tegi kõik 100, iga ooteajaga keskmiselt 6.8 sekundit. Muidugi on reaalsetes süsteemides 6 sekundit jõude olemine vastuvõetamatu ja parem on mitte nii palju taotlusi sel viisil töödelda. Monitoriga lahendus osutus üldse mitte skaleeritavaks.

Järeldus

Nagu nendest väikestest näidetest näha, toetab .NET paljusid sünkroonimiskonstruktsioone. Siiski pole alati selge, kuidas neid kasutada. Loodan, et sellest artiklist oli abi. Praegu lõpetame selle, kuid alles on veel palju huvitavat, näiteks lõimekindlad kogud, TPL-i andmevoog, reaktiivne programmeerimine, tarkvaratehingu mudel jne.

allikatest

Allikas: www.habr.com

Lisa kommentaar