Labi paēduši filozofi vai konkurētspējīga .NET programmēšana

Labi paēduši filozofi vai konkurētspējīga .NET programmēšana

Apskatīsim, kā .Net darbojas vienlaicīga un paralēla programmēšana, kā piemēru izmantojot Philosophers Dining Problem. Plāns ir šāds, sākot no pavedienu / procesu sinhronizācijas līdz aktiera modelim (nākamajās daļās). Raksts var būt noderīgs pirmajai iepazīšanai vai zināšanu atsvaidzināšanai.

Kāpēc to vispār darīt? Tranzistori sasniedz savu minimālo izmēru, Mūra likums balstās uz gaismas ātruma ierobežojumu un tāpēc ir vērojams skaita pieaugums, var izgatavot vairāk tranzistoru. Tajā pašā laikā pieaug datu apjoms, un lietotāji sagaida tūlītēju reakciju no sistēmām. Šādā situācijā "parastā" programmēšana, kad mums ir viens izpildes pavediens, vairs nav efektīva. Jums kaut kā jāatrisina vienlaicīgas vai vienlaicīgas izpildes problēma. Turklāt šī problēma pastāv dažādos līmeņos: pavedienu līmenī, procesu līmenī, mašīnu līmenī tīklā (izplatītās sistēmas). .NET ir augstas kvalitātes, laika pārbaudītas tehnoloģijas, lai ātri un efektīvi atrisinātu šādas problēmas.

Uzdevums

Edsgers Dijkstra šo problēmu saviem studentiem izvirzīja jau 1965. gadā. Izveidotais formulējums ir šāds. Ir noteikts (parasti pieci) filozofu skaits un tikpat daudz dakšiņu. Viņi sēž pie apaļa galda, starp viņiem ir dakšas. Filozofi var ēst no saviem šķīvjiem ar bezgalīgu ēdienu, domāt vai gaidīt. Lai apēstu filozofu, jāņem divas dakšiņas (pēdējā dala dakšiņu ar pirmo). Dakšas pacelšana un nolikšana ir divas atsevišķas darbības. Visi filozofi klusē. Uzdevums ir atrast tādu algoritmu, lai visi domātu un būtu pilni arī pēc 54 gadiem.

Vispirms mēģināsim atrisināt šo problēmu, izmantojot koplietošanas telpu. Dakšiņas atrodas uz kopējā galda, un filozofi tās vienkārši paņem un noliek atpakaļ. Šeit ir problēmas ar sinhronizāciju, kad tieši ņemt surebets? ko darīt, ja nav dakšas? utt. Bet vispirms sāksim filozofus.

Lai sāktu pavedienus, mēs izmantojam pavedienu kopu Task.Run metode:

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

Pavedienu kopums ir izveidots, lai optimizētu pavedienu izveidi un dzēšanu. Šajā pūlā ir rinda ar uzdevumiem, un CLR izveido vai noņem pavedienus atkarībā no šo uzdevumu skaita. Viens baseins visiem AppDomains. Šo baseinu vajadzētu izmantot gandrīz vienmēr, jo. nav jāuztraucas ar pavedienu veidošanu, dzēšanu, to rindu utt. Tas ir iespējams bez pūla, bet tad tas ir jāizmanto tieši Thread, tas ir noderīgi gadījumos, kad jāmaina pavediena prioritāte, kad mums ir ilga darbība, priekšplāna pavedienam utt.

Citiem vārdiem sakot, System.Threading.Tasks.Task klase ir tāda pati Thread, bet ar visdažādākajām ērtībām: iespēju palaist uzdevumu pēc citu uzdevumu bloka, atgriezt tos no funkcijām, ērti tos pārtraukt un daudz ko citu. utt. Tie ir nepieciešami, lai atbalstītu asinhronās / gaidīšanas konstrukcijas (uz uzdevumiem balstīts asinhronais modelis, sintaktiskais cukurs IO operāciju gaidīšanai). Mēs par to runāsim vēlāk.

CancelationTokenSource šeit tas ir vajadzīgs, lai pavediens varētu beigties pēc izsaucošā pavediena signāla.

Sinhronizācijas problēmas

Bloķētie filozofi

Labi, mēs zinām, kā izveidot pavedienus, mēģināsim paēst pusdienas:

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

Šeit mēs vispirms mēģinām paņemt kreiso dakšiņu un pēc tam labo dakšiņu, un, ja tas izdodas, tad ēdam un noliekam atpakaļ. Vienas dakšiņas ņemšana ir atomāra, t.i. divus pavedienus nevar paņemt vienlaikus (nepareizi: pirmajā rakstīts, ka dakša ir brīva, otrā - arī, pirmais ņem, otrais ņem). Priekš šī Interlocked.CompareExchange, kas jārealizē ar procesora instrukciju (TSL, XCHG), kas bloķē atmiņas daļu atomu secīgai lasīšanai un rakstīšanai. Un SpinWait ir līdzvērtīgs konstrukcijai while(true) tikai ar nelielu "maģiju" - pavediens aizņem procesoru (Thread.SpinWait), bet dažreiz nodod vadību citam pavedienam (Thread.Yeild) vai aizmieg (Thread.Sleep).

Bet šis risinājums nedarbojas, jo plūsmas drīz (man sekundes laikā) tiek bloķētas: visi filozofi ņem kreiso dakšiņu, bet ne labo. Pēc tam dakšu masīvam ir šādas vērtības: 1 2 3 4 5.

Labi paēduši filozofi vai konkurētspējīga .NET programmēšana

Attēlā vītņu bloķēšana (strupējs). Zaļš - izpilde, sarkans - sinhronizācija, pelēks - pavediens guļ. Rombi norāda Uzdevumu sākuma laiku.

Filozofu bads

Ēdienu gan īpaši daudz domāt nevajag, bet izsalkums liek jebkuram atteikties no filozofijas. Mēģināsim simulēt mūsu problēmas diegu bada situāciju. Bads ir tad, kad pavediens iet, bet bez ievērojama darba, citiem vārdiem sakot, šis ir tas pats strupceļš, tikai tagad pavediens neguļ, bet aktīvi meklē, ko ēst, bet nav pārtikas. Lai izvairītos no biežas bloķēšanas, mēs noliksim dakšiņu atpakaļ, ja nevarēsim paņemt citu.

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

Svarīgākais šajā kodā ir tas, ka divi no četriem filozofiem aizmirst nolikt kreiso dakšiņu. Un izrādās, ka viņi ēd vairāk pārtikas, bet citi sāk badoties, lai gan pavedieniem ir tāda pati prioritāte. Šeit viņi nav pilnībā izsalkuši, jo. slikti filozofi dažreiz atliek dakšiņas. Izrādās, ka labie cilvēki ēd apmēram 5 reizes mazāk nekā sliktie. Tātad neliela kļūda kodā noved pie veiktspējas samazināšanās. Šeit ir arī vērts atzīmēt, ka ir iespējama reta situācija, kad visi filozofi ņem kreiso dakšiņu, labās nav, viņi noliek kreiso, gaida, atkal ņem kreiso utt. Šī situācija ir arī bads, vairāk kā strupceļš. Man neizdevās to atkārtot. Zemāk ir bilde situācijai, kad divi sliktie filozofi ir paņēmuši abas dakšiņas un divi labie badā.

Labi paēduši filozofi vai konkurētspējīga .NET programmēšana

Šeit var redzēt, ka pavedieni dažreiz pamostas un mēģina iegūt resursu. Divi no četriem kodoliem neko nedara (zaļš grafiks iepriekš).

Filozofa nāve

Nu, vēl viena problēma, kas var pārtraukt krāšņās filozofu vakariņas, ir, ja kāds no viņiem pēkšņi nomirst ar dakšām rokās (un viņi viņu tā apglabās). Tad kaimiņi paliks bez pusdienām. Šim gadījumam varat izdomāt piemēru kodu, piemēram, tas tiek izmests NullReferenceException pēc tam, kad filozofs paņem dakšas. Un, starp citu, izņēmums netiks apstrādāts, un zvanīšanas kods to vienkārši neuztvers (šī AppDomain.CurrentDomain.UnhandledException un utt.). Tāpēc kļūdu apstrādātāji ir nepieciešami pašos pavedienos un ar graciozu izbeigšanu.

Gaidītājs

Labi, kā mēs atrisinām šo strupceļa, bada un nāves problēmu? Mēs ļausim tikai vienam filozofam sasniegt dakšas, pievienosim šai vietai savstarpēju pavedienu izslēgšanu. Kā to izdarīt? Pieņemsim, ka blakus filozofiem ir viesmīlis, kurš dod atļauju jebkuram filozofam paņemt dakšiņas. Kā mēs uztaisām šo viesmīli un kā viņam jautās filozofi, jautājumi ir interesanti.

Vienkāršākais veids ir tad, kad filozofi vienkārši pastāvīgi lūgs viesmīlim piekļuvi dakšām. Tie. tagad filozofi negaidīs dakšiņu tuvumā, bet gaidīs vai pajautās oficiantam. Sākumā mēs šim nolūkam izmantojam tikai User Space, tajā mēs neizmantojam pārtraukumus, lai izsauktu nekādas procedūras no kodola (par tām tālāk).

Risinājumi lietotāju telpā

Šeit mēs darīsim to pašu, ko mēs darījām ar vienu dakšiņu un diviem filozofiem, mēs griezīsimies ciklā un gaidīsim. Bet tagad tie būs visi filozofi un it kā tikai viena dakša, t.i. var teikt, ka ēdīs tikai tas filozofs, kurš viesmīlim paņēma šo “zelta dakšiņu”. Šim nolūkam mēs izmantojam 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 tas ir bloķētājs, ar, rupji sakot, tas pats while(true) { if (!lock) break; }, bet ar vēl lielāku "maģiju" nekā iekšā SpinWait (kas tur tiek izmantots). Tagad viņš zina, kā saskaitīt tos, kas gaida, nedaudz iemidzināt un daudz ko citu. utt. Kopumā dara visu iespējamo, lai optimizētu. Bet jāatceras, ka šis joprojām ir tas pats aktīvais cikls, kas apēd procesora resursus un notur plūsmu, kas var novest pie bada, ja kāds no filozofiem kļūst prioritārāks par citiem, bet viņam nav zelta dakšiņas (Prioritātes inversijas problēma) . Tāpēc mēs to izmantojam tikai ļoti īsām koplietojamās atmiņas izmaiņām, bez jebkādiem trešo pušu zvaniem, ligzdotām slēdzenēm un citiem pārsteigumiem.

Labi paēduši filozofi vai konkurētspējīga .NET programmēšana

Zīmējums priekš SpinLock. Straumes nepārtraukti "cīnās" par zelta dakšiņu. Ir kļūmes - attēlā izvēlētā zona. Serdeņi nav pilnībā izmantoti: tikai aptuveni 2/3 no šiem četriem pavedieniem.

Cits risinājums šeit būtu tikai izmantot Interlocked.CompareExchange ar tādu pašu aktīvo gaidīšanu, kā parādīts augstāk esošajā kodā (izsalkušajiem filozofiem), taču tas, kā jau teikts, teorētiski var novest pie bloķēšanas.

uz Interlocked Jāpiebilst, ka ir ne tikai CompareExchange, bet arī citas metodes atomu lasīšanai UN rakstīšanai. Un, atkārtojot izmaiņas, ja citam pavedienam ir laiks veikt izmaiņas (lasīt 1, lasīt 2, rakstīt 2, rakstīt 1 ir slikti), to var izmantot sarežģītām vienas vērtības izmaiņām (Interlocked Anything modelis). .

Kodola režīma risinājumi

Lai izvairītos no resursu izšķērdēšanas ciklā, redzēsim, kā mēs varam bloķēt pavedienu. Citiem vārdiem sakot, turpinot mūsu piemēru, redzēsim, kā viesmīlis iemidzina filozofu un pamodina viņu tikai nepieciešamības gadījumā. Vispirms apskatīsim, kā to izdarīt, izmantojot operētājsistēmas kodola režīmu. Visas tur esošās struktūras bieži ir lēnākas nekā tās, kas atrodas lietotāju telpā. Piemēram, vairākas reizes lēnāk AutoResetEvent varbūt 53 reizes lēnāk SpinLock [Rihters]. Bet ar viņu palīdzību jūs varat sinhronizēt procesus visā sistēmā neatkarīgi no tā, vai tas tiek pārvaldīts vai nē.

Pamata konstrukcija šeit ir Dijkstra piedāvātais semafors pirms vairāk nekā pusgadsimta. Semafors, vienkārši sakot, ir pozitīvs vesels skaitlis, ko pārvalda sistēma, un divas darbības ar to: palielināšana un samazināšana. Ja to neizdodas samazināt, nulle, tad izsaucošais pavediens tiek bloķēts. Kad skaitlis tiek palielināts ar kādu citu aktīvu pavedienu/procesu, pavedieni tiek izlaisti un semafors atkal tiek samazināts par nodoto skaitu. Var iedomāties vilcienus sašaurinātā kaklā ar semaforu. .NET piedāvā vairākas konstrukcijas ar līdzīgu funkcionalitāti: AutoResetEvent, ManualResetEvent, Mutex un es pats Semaphore. Mēs izmantosim AutoResetEvent, šī ir vienkāršākā no šīm konstrukcijām: tikai divas vērtības 0 un 1 (false, true). Viņas metode WaitOne() bloķē izsaucošo pavedienu, ja vērtība bija 0, un, ja vērtība ir 1, samazina to līdz 0 un izlaiž to. Metode Set() paaugstina uz 1 un laiž cauri vienu oficiantu, kurš atkal nolaiž līdz 0. Darbojas kā metro turnikets.

Sarežģīsim risinājumu un izmantosim slēdzeni katram filozofam, nevis visiem uzreiz. Tie. tagad var būt vairāki filozofi vienlaikus, un ne viens. Bet mēs atkal bloķējam piekļuvi galdam, lai pareizi, izvairoties no sacīkstēm (sacīkšu apstākļi), veiktu pārliecības.

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

Lai saprastu, kas šeit notiek, apsveriet gadījumu, kad filozofam neizdevās paņemt dakšas, tad viņa rīcība būs šāda. Viņš gaida pieeju pie galda. To saņēmis, viņš mēģina paņemt dakšiņas. Neizdevās. Tas nodrošina piekļuvi tabulai (savstarpēja izslēgšana). Un iet garām savam "turniketim" (AutoResetEvent) (sākotnēji tie ir atvērti). Tas atkal nokļūst ciklā, jo viņam nav dakšu. Viņš mēģina tos paņemt un apstājas pie sava "turniketa". Kāds laimīgāks kaimiņš labajā vai kreisajā pusē, pabeidzis ēst, atslēdz mūsu filozofu, "atverot turniketu". Mūsu filozofs tai iet garām (un aizveras aiz tā) otro reizi. Viņš trešo reizi mēģina paņemt dakšas. Veiksmi. Un viņš padod savu turniketu, lai pusdienotu.

Ja šādā kodā ir nejaušas kļūdas (tās vienmēr pastāv), piemēram, ir nepareizi norādīts kaimiņš vai izveidots tas pats objekts AutoResetEvent visiem (Enumerable.Repeat), tad filozofi gaidīs izstrādātājus, jo Kļūdu atrašana šādā kodā ir diezgan grūts uzdevums. Vēl viena šī risinājuma problēma ir tā, ka tas negarantē, ka kāds filozofs nepaliks izsalcis.

Hibrīdie risinājumi

Mēs esam izskatījuši divas pieejas laika noteikšanai, kad mēs paliekam lietotāja režīmā un cilpa, un kad mēs bloķējam pavedienu caur kodolu. Pirmā metode ir piemērota īsām slēdzenēm, otrā - garām. Bieži vien vispirms ir īsi jāpagaida, līdz mainās mainīgais cilpā, un pēc tam jābloķē pavediens, kad gaidīšana ir ilga. Šī pieeja tiek īstenota t.s. hibrīda struktūras. Šeit ir tādas pašas konstrukcijas kā kodola režīmam, bet tagad ar lietotāja režīma cilpu: SemaphorSlim, ManualResetEventSlim utt. Populārākais dizains šeit ir Monitor, jo C# ir labi zināms lock sintakse. Monitor šis ir tas pats semafors ar maksimālo vērtību 1 (mutex), bet ar atbalstu gaidīšanai cilpā, rekursijai, nosacījuma mainīgajam modelim (vairāk par to tālāk) utt. Apskatīsim risinājumu ar to.

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

Šeit mēs atkal bloķējam visu galdu, lai piekļūtu dakšām, bet tagad mēs atbloķējam visus pavedienus uzreiz, nevis kaimiņus, kad kāds beidz ēst. Tie. pirmkārt, kāds ēd un bloķē kaimiņus, un, kad šis kāds beidz, bet tūlīt atkal grib ēst, viņš ieiet bloķēšanā un pamodina savus kaimiņus, jo. tā gaidīšanas laiks ir mazāks.

Tā mēs izvairāmies no strupceļiem un kāda filozofa nāves. Mēs izmantojam cilpu īsai gaidīšanai un bloķējam pavedienu uz ilgu laiku. Visu atbloķēšana uzreiz notiek lēnāk nekā tad, ja tiktu atbloķēts tikai kaimiņš, kā risinājumā ar AutoResetEvent, bet atšķirībai nevajadzētu būt lielai, jo pavedieniem vispirms jāpaliek lietotāja režīmā.

У lock sintaksē ir nepatīkami pārsteigumi. Iesakām lietot Monitor tieši [Rihters] [Ēriks Liperts]. Viens no tiem ir tas lock vienmēr ārā no Monitor, pat ja būtu izņēmums, un tādā gadījumā cits pavediens varētu mainīt koplietojamās atmiņas stāvokli. Šādos gadījumos bieži vien labāk ir nonākt strupceļā vai kaut kā droši pārtraukt programmu. Vēl viens pārsteigums ir tas, ka monitors izmanto sinhronizācijas blokus (SyncBlock), kas atrodas visos objektos. Tāpēc, ja tiek atlasīts nepiemērots objekts, jūs varat viegli nokļūt strupceļā (piemēram, ja bloķējat internētu virkni). Šim nolūkam mēs izmantojam vienmēr slēpto objektu.

Stāvokļa mainīgā modelis ļauj kodolīgāk īstenot cerības uz kādu sarežģītu nosacījumu. .NET, manuprāt, tas ir nepilnīgs, jo teorētiski vajadzētu būt vairākām rindām uz vairākiem mainīgajiem (kā Posix Threads), nevis vienā lokā. Tad tos varētu izveidot visiem filozofiem. Bet pat šajā formā tas ļauj samazināt kodu.

daudzi filozofi vai async / await

Labi, tagad mēs varam efektīvi bloķēt pavedienus. Bet ja mums ir daudz filozofu? 100? 10000? Piemēram, mēs saņēmām 100000 4 pieprasījumu tīmekļa serverim. Katram pieprasījumam izveidot pavedienu būs papildu izmaksas, jo tik daudzi pavedieni nedarbosies paralēli. Darbosies tikai tik daudz, cik ir loģiskie kodoli (man ir XNUMX). Un visi pārējie vienkārši atņems resursus. Viens no šīs problēmas risinājumiem ir asinhronā/gaidīšanas shēma. Tās ideja ir tāda, ka funkcija netur pavedienu, ja tai jāgaida, kamēr kaut kas turpināsies. Un, kad tas kaut ko dara, tas atsāk izpildi (bet ne obligāti tajā pašā pavedienā!). Mūsu gadījumā mēs gaidīsim dakšiņu.

SemaphoreSlim ir šim nolūkam WaitAsync() metodi. Šeit ir īstenošana, izmantojot šo modeli.

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

Metode ar async / await tiek pārveidots par sarežģītu stāvokli, kas nekavējoties atgriež savu iekšējo Task. Izmantojot to, varat gaidīt metodes pabeigšanu, atcelt to un visu pārējo, ko varat darīt ar uzdevumu. Metodes iekšienē izpildi kontrolē stāvokļa mašīna. Secinājums ir tāds, ka, ja nav kavēšanās, tad izpilde ir sinhrona, un, ja ir, tad pavediens tiek atbrīvots. Lai to labāk izprastu, labāk ir apskatīt šo stāvokļa mašīnu. No tiem varat izveidot ķēdes async / await metodes.

Pārbaudīsim. 100 filozofu darbs uz mašīnas ar 4 loģiskajiem kodoliem, 8 sekundes. Iepriekšējais risinājums ar Monitor palaida tikai pirmos 4 pavedienus, bet pārējie nedarbojās vispār. Katrs no šiem 4 pavedieniem bija dīkstāvē apmēram 2 ms. Un asinhronais/gaidīšanas risinājums darbojās visas 100 — katrs vidēji gaidīja 6.8 sekundes. Protams, reālajās sistēmās dīkstāve 6 sekundes ir nepieņemama, un labāk neapstrādāt tik daudz pieprasījumu kā šis. Risinājums ar Monitor izrādījās vispār nav mērogojams.

Secinājums

Kā redzat no šiem mazajiem piemēriem, .NET atbalsta daudzas sinhronizācijas konstrukcijas. Tomēr ne vienmēr ir skaidrs, kā tos izmantot. Es ceru, ka šis raksts bija noderīgs. Pagaidām šīs ir beigas, bet vēl ir palicis daudz interesanta, piemēram, pavedienu drošas kolekcijas, TPL datu plūsma, reaktīvā programmēšana, programmatūras transakcijas modelis utt.

avoti

Avots: www.habr.com

Pievieno komentāru