Wanafalsafa Waliolishwa Vizuri au Utayarishaji wa NET

Wanafalsafa Waliolishwa Vizuri au Utayarishaji wa NET

Hebu tuone jinsi upangaji programu kwa wakati mmoja na sambamba unavyofanya kazi katika .Net, kwa kutumia Tatizo la Kula la Wanafalsafa kama mfano. Mpango ni huu, kutoka kwa maingiliano ya nyuzi / michakato, hadi mfano wa mwigizaji (katika sehemu zifuatazo). Nakala hiyo inaweza kuwa muhimu kwa marafiki wa kwanza au ili kuburudisha maarifa yako.

Kwa nini kufanya hivyo wakati wote? Transistors hufikia ukubwa wao wa chini, sheria ya Moore inategemea ukomo wa kasi ya mwanga na kwa hiyo ongezeko linazingatiwa kwa idadi, transistors zaidi yanaweza kufanywa. Wakati huo huo, kiasi cha data kinaongezeka, na watumiaji wanatarajia majibu ya haraka kutoka kwa mifumo. Katika hali kama hiyo, programu "ya kawaida", tunapokuwa na uzi mmoja wa kutekeleza, haifai tena. Unahitaji kwa namna fulani kutatua tatizo la utekelezaji wa wakati mmoja au wa wakati mmoja. Aidha, tatizo hili lipo katika viwango tofauti: katika ngazi ya nyuzi, katika ngazi ya taratibu, katika ngazi ya mashine katika mtandao (mifumo iliyosambazwa). NET ina ubora wa juu, teknolojia zilizojaribiwa kwa wakati kwa kutatua shida kama hizo haraka na kwa ufanisi.

Kazi

Edsger Dijkstra alitoa tatizo hili kwa wanafunzi wake mapema kama 1965. Uundaji ulioanzishwa ni kama ifuatavyo. Kuna idadi fulani (kawaida tano) ya wanafalsafa na idadi sawa ya uma. Wanakaa kwenye meza ya pande zote, uma kati yao. Wanafalsafa wanaweza kula kutoka kwa sahani zao za chakula kisicho na mwisho, kufikiri au kusubiri. Ili kula mwanafalsafa, unahitaji kuchukua uma mbili (wa mwisho anashiriki uma na wa kwanza). Kuokota na kuweka uma ni vitendo viwili tofauti. Wanafalsafa wote wako kimya. Kazi ni kupata algorithm ambayo wote wangefikiria na kuwa kamili hata baada ya miaka 54.

Kwanza, hebu tujaribu kutatua tatizo hili kwa kutumia nafasi ya pamoja. Uma ziko kwenye meza ya kawaida na wanafalsafa huzichukua tu zilipo na kuzirudisha. Hapa kuna matatizo na ulandanishi, ni wakati gani hasa wa kuchukua surebets? nini ikiwa hakuna uma? nk Lakini kwanza, tuanze wanafalsafa.

Ili kuanza nyuzi, tunatumia dimbwi la nyuzi kupitia Task.Run njia:

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

Dimbwi la nyuzi limeundwa ili kuboresha uundaji na ufutaji wa nyuzi. Bwawa hili lina foleni na majukumu na CLR huunda au kuondoa nyuzi kulingana na idadi ya majukumu haya. Dimbwi moja kwa AppDomains zote. Bwawa hili linapaswa kutumika karibu kila mara, kwa sababu. hakuna haja ya kujisumbua na kuunda, kufuta nyuzi, foleni zao, nk. Inawezekana bila bwawa, lakini basi lazima uitumie moja kwa moja. Thread, hii ni muhimu kwa kesi wakati unahitaji kubadilisha kipaumbele cha thread, wakati tuna operesheni ndefu, kwa thread ya Foreground, nk.

Kwa maneno mengine, System.Threading.Tasks.Task darasa ni sawa Thread, lakini kwa kila aina ya urahisi: uwezo wa kuendesha kazi baada ya kizuizi cha kazi zingine, kuzirudisha kutoka kwa kazi, kuzisumbua kwa urahisi, na zaidi. n.k. Zinahitajika ili kusaidia miundo isiyolingana/kungoja (Mchoro wa Asynchronous unaotegemea Kazi, sukari ya kisintaksia kwa kusubiri shughuli za IO). Tutazungumza juu ya hili baadaye.

CancelationTokenSource hapa inahitajika ili thread iweze kukomesha yenyewe kwa ishara ya thread ya wito.

Masuala ya Usawazishaji

Wanafalsafa Waliozuiwa

Sawa, tunajua jinsi ya kuunda nyuzi, wacha tujaribu kula chakula cha mchana:

// ΠšΡ‚ΠΎ ΠΊΠ°ΠΊΠΈΠ΅ Π²ΠΈΠ»ΠΊΠΈ взял. К ΠΏΡ€ΠΈΠΌΠ΅Ρ€Ρƒ: 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();
    }
}

Hapa sisi kwanza tunajaribu kuchukua uma wa kushoto, na kisha uma wa kulia, na ikiwa inafanya kazi, basi tunakula na kuwaweka tena. Kuchukua uma moja ni atomiki, i.e. nyuzi mbili haziwezi kuchukua moja kwa wakati mmoja (sio sahihi: ya kwanza inasoma kwamba uma ni bure, pili - pia, ya kwanza inachukua, ya pili inachukua). Kwa hii; kwa hili Interlocked.CompareExchange, ambayo lazima itekelezwe kwa maagizo ya processor (TSL, XCHG), ambayo hufunga kipande cha kumbukumbu kwa usomaji na uandishi wa mpangilio wa atomiki. Na SpinWait ni sawa na ujenzi while(true) tu na "uchawi" kidogo - thread inachukua processor (Thread.SpinWait), lakini wakati mwingine huhamisha udhibiti kwa uzi mwingine (Thread.Yeild) au analala (Thread.Sleep).

Lakini suluhisho hili halifanyi kazi, kwa sababu mtiririko wa hivi karibuni (kwa ajili yangu ndani ya sekunde) umezuiwa: wanafalsafa wote huchukua uma wao wa kushoto, lakini sio sawa. Safu ya uma basi ina maadili: 1 2 3 4 5.

Wanafalsafa Waliolishwa Vizuri au Utayarishaji wa NET

Katika takwimu, kuzuia threads (deadlock). Kijani - utekelezaji, nyekundu - maingiliano, kijivu - thread inalala. Rombuses zinaonyesha wakati wa kuanza kwa Kazi.

Njaa ya Wanafalsafa

Ingawa sio lazima kufikiria chakula kingi, lakini njaa hufanya mtu yeyote aache falsafa. Wacha tujaribu kuiga hali ya njaa ya nyuzi kwenye shida yetu. Njaa ni wakati thread inaendesha, lakini bila kazi kubwa, kwa maneno mengine, hii ni msuguano sawa, sasa tu thread haijalala, lakini inatafuta kwa bidii kitu cha kula, lakini hakuna chakula. Ili kuzuia kuzuia mara kwa mara, tutarudisha uma ikiwa hatukuweza kuchukua nyingine.

// Π’ΠΎ ΠΆΠ΅ Ρ‡Ρ‚ΠΎ ΠΈ Π² 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)
    );
}

Jambo muhimu kuhusu kanuni hii ni kwamba wanafalsafa wawili kati ya wanne husahau kuweka chini uma wao wa kushoto. Na zinageuka kuwa wanakula chakula zaidi, wakati wengine wanaanza kufa na njaa, ingawa nyuzi zina kipaumbele sawa. Hapa hawana njaa kabisa, kwa sababu. wanafalsafa wabaya hurudisha uma zao nyuma wakati mwingine. Inageuka kuwa watu wazuri hula karibu mara 5 kuliko mbaya. Kwa hivyo hitilafu ndogo katika msimbo husababisha kushuka kwa utendaji. Pia ni muhimu kuzingatia hapa kwamba hali ya nadra inawezekana wakati wanafalsafa wote wanachukua uma wa kushoto, hakuna moja ya haki, wanaweka kushoto, kusubiri, kuchukua kushoto tena, nk. Hali hii pia ni njaa, zaidi kama mkwamo. Nilishindwa kurudia. Chini ni picha ya hali ambapo wanafalsafa wawili wabaya wamechukua uma na wawili wazuri wana njaa.

Wanafalsafa Waliolishwa Vizuri au Utayarishaji wa NET

Hapa unaweza kuona kwamba nyuzi zinaamka wakati mwingine na kujaribu kupata rasilimali. Cores mbili kati ya nne hazifanyi chochote (grafu ya kijani kibichi hapo juu).

Kifo cha Mwanafalsafa

Naam, tatizo jingine linaloweza kukatiza karamu tukufu ya wanafalsafa ni iwapo mmoja wao atakufa ghafla akiwa na uma mikononi mwake (na watamzika hivyo). Kisha majirani wataachwa bila chakula cha mchana. Unaweza kuja na msimbo wa mfano wa kesi hii mwenyewe, kwa mfano, inatupwa nje NullReferenceException baada ya mwanafalsafa kuchukua uma. Na, kwa njia, ubaguzi hautashughulikiwa na nambari ya simu haitaipata tu (kwa hili AppDomain.CurrentDomain.UnhandledException na nk). Kwa hivyo, vidhibiti vya makosa vinahitajika kwenye nyuzi zenyewe na kwa uondoaji mzuri.

Kijitabu

Sawa, je, tunatatuaje tatizo hili la mkwamo, njaa na kifo? Tutaruhusu mwanafalsafa mmoja tu kufikia uma, tuongeze kutengwa kwa nyuzi mahali hapa. Jinsi ya kufanya hivyo? Tuseme kwamba mhudumu amesimama karibu na wanafalsafa, ambaye hutoa ruhusa kwa mwanafalsafa yeyote kuchukua uma. Tunafanyaje mhudumu huyu na jinsi wanafalsafa watakavyomuuliza, maswali yanavutia.

Njia rahisi ni wakati wanafalsafa watauliza tu mhudumu kila wakati kwa ufikiaji wa uma. Wale. sasa wanafalsafa hawatasubiri uma karibu, lakini subiri au waulize mhudumu. Mara ya kwanza, tunatumia Nafasi ya Mtumiaji tu kwa hili, ndani yake hatutumii usumbufu kuita taratibu zozote kutoka kwa kernel (kuhusu wao hapa chini).

Suluhisho katika nafasi ya mtumiaji

Hapa tutafanya vile vile tulivyokuwa tukifanya kwa uma moja na wanafalsafa wawili, tutazunguka katika mzunguko na kusubiri. Lakini sasa itakuwa wanafalsafa wote na, kama ilivyokuwa, uma moja tu, i.e. inaweza kusemwa kwamba ni mwanafalsafa tu ambaye alichukua "uma wa dhahabu" kutoka kwa mhudumu ndiye atakayekula. Kwa hili tunatumia 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 hii ni blocker, na, takriban kusema, sawa while(true) { if (!lock) break; }, lakini kwa "uchawi" zaidi kuliko ndani SpinWait (ambayo inatumika hapo). Sasa anajua jinsi ya kuhesabu wale wanaosubiri, kuwaweka usingizi kidogo, na zaidi. nk Kwa ujumla, hufanya kila linalowezekana ili kuboresha. Lakini lazima tukumbuke kuwa hii bado ni mzunguko wa kazi ambao unakula rasilimali za wasindikaji na kuweka mtiririko, ambayo inaweza kusababisha njaa ikiwa mmoja wa wanafalsafa atakuwa kipaumbele zaidi kuliko wengine, lakini hana uma wa dhahabu ( Tatizo la Inversion ya Kipaumbele) . Kwa hivyo, tunaitumia tu kwa mabadiliko mafupi sana katika kumbukumbu iliyoshirikiwa, bila simu za mtu wa tatu, kufuli zilizowekwa kiota, na mshangao mwingine.

Wanafalsafa Waliolishwa Vizuri au Utayarishaji wa NET

Kuchora kwa SpinLock. Mito ni mara kwa mara "kupigana" kwa uma ya dhahabu. Kuna kushindwa - katika takwimu, eneo lililochaguliwa. Cores hazitumiki kikamilifu: karibu 2/3 tu na nyuzi hizi nne.

Suluhisho lingine hapa litakuwa kutumia tu Interlocked.CompareExchange na kungojea sawa kama inavyoonyeshwa kwenye nambari iliyo hapo juu (katika wanafalsafa wenye njaa), lakini hii, kama ilivyosemwa tayari, inaweza kusababisha kuzuia.

juu ya Interlocked Ikumbukwe kwamba hakuna tu CompareExchange, lakini pia njia zingine za kusoma na kuandika kwa atomiki. Na kupitia marudio ya mabadiliko, ikiwa nyuzi nyingine ina wakati wa kufanya mabadiliko yake (soma 1, soma 2, andika 2, andika 1 ni mbaya), inaweza kutumika kwa mabadiliko magumu kwa thamani moja (Iliyounganishwa Chochote muundo) .

Suluhisho za Njia ya Kernel

Ili kuepuka kupoteza rasilimali katika kitanzi, hebu tuone jinsi tunaweza kuzuia thread. Yaani tukiendelea na mfano wetu, tuone mhudumu anavyomlaza mwanafalsafa na kumwamsha inapobidi tu. Kwanza, hebu tuangalie jinsi ya kufanya hivyo kupitia hali ya kernel ya mfumo wa uendeshaji. Miundo yote huko mara nyingi ni polepole kuliko ile iliyo kwenye nafasi ya mtumiaji. Mara kadhaa polepole, kwa mfano AutoResetEvent labda mara 53 polepole SpinLock [Richter]. Lakini kwa msaada wao, unaweza kusawazisha michakato katika mfumo mzima, kusimamiwa au la.

Muundo wa kimsingi hapa ni semaphore iliyopendekezwa na Dijkstra zaidi ya nusu karne iliyopita. Semaphore ni, kwa urahisi, nambari kamili inayodhibitiwa na mfumo, na shughuli mbili juu yake, ongezeko na kupungua. Ikiwa inashindwa kupungua, sifuri, basi thread ya kupiga simu imefungwa. Nambari inapoongezwa na uzi/mchakato mwingine unaotumika, basi nyuzi kurukwa na semaphore inapunguzwa tena na nambari iliyopitishwa. Mtu anaweza kufikiria treni kwenye kizuizi na semaphore. .NET hutoa miundo kadhaa yenye utendakazi sawa: AutoResetEvent, ManualResetEvent, Mutex na mimi mwenyewe Semaphore. Tutatumia AutoResetEvent, hii ndiyo rahisi zaidi ya ujenzi huu: maadili mawili tu 0 na 1 (uongo, kweli). Mbinu Yake WaitOne() huzuia uzi wa kupiga simu ikiwa thamani ilikuwa 0, na ikiwa 1, inaishusha hadi 0 na kuiruka. Mbinu Set() huongeza hadi 1 na kuruhusu mhudumu mmoja apite, ambaye tena hupungua hadi 0. Hufanya kazi kama njia ya kugeuza chini ya ardhi.

Wacha tufanye ugumu wa suluhisho na kutumia kufuli kwa kila mwanafalsafa, na sio kwa wote mara moja. Wale. sasa kunaweza kuwa na wanafalsafa kadhaa mara moja, na sio mmoja. Lakini tunazuia tena ufikiaji wa meza ili kwa usahihi, epuka jamii (hali za mbio), kuchukua dhamana.

// Для блокирования ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ философа.
// Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅Ρ‚ΡΡ: 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();
}

Ili kuelewa kinachotokea hapa, fikiria kesi wakati mwanafalsafa alishindwa kuchukua uma, basi matendo yake yatakuwa kama ifuatavyo. Anasubiri upatikanaji wa meza. Baada ya kuipokea, anajaribu kuchukua uma. Haikufanikiwa. Inatoa ufikiaji wa jedwali (kutengwa kwa pande zote). Na hupita "turnstile" yake (AutoResetEvent) (hapo awali ziko wazi). Inaingia kwenye mzunguko tena, kwa sababu hana uma. Anajaribu kuwachukua na kuacha kwenye "turnstile" yake. Jirani wengine wa bahati zaidi kulia au kushoto, baada ya kumaliza kula, anafungua mwanafalsafa wetu, "kufungua zamu yake." Mwanafalsafa wetu anaipitisha (na inafunga nyuma yake) kwa mara ya pili. Anajaribu kwa mara ya tatu kuchukua uma. Bahati njema. Naye hupitisha zamu yake kula.

Wakati kuna makosa ya nasibu katika nambari kama hiyo (zipo kila wakati), kwa mfano, jirani imeainishwa vibaya au kitu sawa kimeundwa. AutoResetEvent kwa wote (Enumerable.Repeat), basi wanafalsafa watasubiri watengenezaji, kwa sababu Kupata makosa katika nambari kama hiyo ni kazi ngumu sana. Shida nyingine na suluhisho hili ni kwamba haihakikishi kuwa mwanafalsafa fulani hatakufa njaa.

Suluhisho la Mseto

Tumeangalia mbinu mbili za kuweka muda, tunapokaa katika hali ya mtumiaji na kitanzi, na tunapozuia uzi kupitia kernel. Njia ya kwanza ni nzuri kwa kufuli fupi, ya pili kwa muda mrefu. Mara nyingi ni muhimu kwanza kusubiri kwa ufupi kwa kutofautiana kwa mabadiliko katika kitanzi, na kisha kuzuia thread wakati kusubiri ni muda mrefu. Mbinu hii inatekelezwa katika kinachojulikana. miundo ya mseto. Hapa kuna muundo sawa na wa hali ya kernel, lakini sasa na kitanzi cha hali ya mtumiaji: SemaphorSlim, ManualResetEventSlim nk Muundo maarufu zaidi hapa ni Monitor, kwa sababu katika C # kuna inayojulikana lock sintaksia. Monitor hii ni semaphore sawa na thamani ya juu ya 1 (mutex), lakini kwa usaidizi wa kusubiri katika kitanzi, kujirudia, muundo wa Hali ya Kubadilika (zaidi juu ya hapo chini), nk. Hebu tuangalie suluhisho nayo.

// БпрячСм ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ для ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€Π° ΠΎΡ‚ всСх, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π±Π΅Π· Π΄Π΅Π΄Π»ΠΎΠΊΠΎΠ².
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);
    }
}

Hapa tunazuia tena meza nzima kwa ufikiaji wa uma, lakini sasa tunafungua nyuzi zote mara moja, na sio majirani wakati mtu anamaliza kula. Wale. kwanza, mtu anakula na kuzuia majirani, na wakati mtu huyu anapomaliza, lakini anataka kula tena mara moja, anaingia katika kuzuia na kuamsha majirani zake, kwa sababu. muda wake wa kusubiri ni mdogo.

Hivi ndivyo tunavyoepuka mikwaruzo na njaa ya mwanafalsafa fulani. Tunatumia kitanzi kwa kusubiri kwa muda mfupi na kuzuia thread kwa muda mrefu. Kufungua kila mtu mara moja ni polepole kuliko ikiwa tu jirani alifunguliwa, kama kwenye suluhisho na AutoResetEvent, lakini tofauti haipaswi kuwa kubwa, kwa sababu nyuzi lazima zibaki katika hali ya mtumiaji kwanza.

Π£ lock syntax ina mshangao mbaya. Pendekeza kutumia Monitor moja kwa moja [Richter] [Eric Lippert]. Mmoja wao ni kwamba lock daima nje ya Monitor, hata ikiwa kulikuwa na ubaguzi, kwa hali ambayo nyuzi nyingine inaweza kubadilisha hali ya kumbukumbu iliyoshirikiwa. Katika hali kama hizi, mara nyingi ni bora kwenda kwa msuguano au kwa njia fulani kusitisha programu kwa usalama. Mshangao mwingine ni kwamba Monitor hutumia vizuizi vya maingiliano (SyncBlock), ambazo zipo katika vitu vyote. Kwa hivyo, ikiwa kitu kisichofaa kimechaguliwa, unaweza kupata kizuizi kwa urahisi (kwa mfano, ikiwa utafunga kwenye kamba iliyoingiliana). Tunatumia kitu kilichofichwa kila wakati kwa hili.

Muundo wa Kubadilika kwa Hali hukuruhusu kutekeleza kwa ufupi zaidi matarajio ya hali fulani changamano. Katika NET, haijakamilika, kwa maoni yangu, kwa sababu kwa nadharia, kunapaswa kuwa na foleni kadhaa kwenye anuwai kadhaa (kama kwenye Posix Threads), na sio kwenye lok moja. Kisha mtu anaweza kuwafanya kwa wanafalsafa wote. Lakini hata katika fomu hii, inakuwezesha kupunguza msimbo.

wanafalsafa wengi au async / await

Sawa, sasa tunaweza kuzuia nyuzi. Lakini vipi ikiwa tuna wanafalsafa wengi? 100? 10000? Kwa mfano, tulipokea maombi 100000 kwa seva ya wavuti. Itakuwa juu sana kuunda thread kwa kila ombi, kwa sababu nyuzi nyingi sana hazitaenda sambamba. Nitaendesha tu kama vile kuna alama za kimantiki (nina 4). Na kila mtu mwingine atachukua tu rasilimali. Suluhisho moja la tatizo hili ni muundo wa async/wait. Wazo lake ni kwamba kazi haishiki thread ikiwa inahitaji kusubiri kitu kuendelea. Na inapofanya kitu, huanza tena utekelezaji wake (lakini sio lazima kwenye uzi huo huo!). Kwa upande wetu, tutasubiri uma.

SemaphoreSlim ina kwa hili WaitAsync() njia. Hapa kuna utekelezaji kwa kutumia muundo huu.

// Запуск Ρ‚Π°ΠΊΠΎΠΉ ΠΆΠ΅, ΠΊΠ°ΠΊ Ρ€Π°Π½ΡŒΡˆΠ΅. Π“Π΄Π΅-Π½ΠΈΠ±ΡƒΠ΄ΡŒ Π² ΠΏΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠ΅:
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();
}

Mbinu na async / await inatafsiriwa katika mashine ya hali ya hila ambayo inarudisha ndani yake mara moja Task. Kupitia hiyo, unaweza kusubiri kukamilika kwa njia, kufuta, na kila kitu kingine ambacho unaweza kufanya na Task. Ndani ya njia, mashine ya serikali inadhibiti utekelezaji. Jambo la msingi ni kwamba ikiwa hakuna kuchelewa, basi utekelezaji ni synchronous, na ikiwa kuna, basi thread inatolewa. Kwa ufahamu bora wa hili, ni bora kuangalia mashine hii ya serikali. Unaweza kuunda minyororo kutoka kwa haya async / await mbinu.

Hebu tujaribu. Kazi ya wanafalsafa 100 kwenye mashine yenye cores 4 za kimantiki, sekunde 8. Suluhisho la hapo awali na Monitor liliendesha tu nyuzi 4 za kwanza na zingine hazikufanya kazi hata kidogo. Kila moja ya nyuzi hizi 4 haikufanya kitu kwa takriban 2ms. Na suluhisho la async / kungojea liliendesha 100 zote, na wastani wa kungojea kwa sekunde 6.8 kila moja. Kwa kweli, katika mifumo halisi, bila kufanya kazi kwa sekunde 6 haikubaliki na ni bora kutoshughulikia maombi mengi kama haya. Suluhisho na Monitor iligeuka kuwa sio hatari hata kidogo.

Hitimisho

Kama unaweza kuona kutoka kwa mifano hii ndogo, .NET inasaidia miundo mingi ya ulandanishi. Walakini, sio wazi kila wakati jinsi ya kuzitumia. Natumaini makala hii ilikuwa ya manufaa. Kwa sasa, hii ndiyo mwisho, lakini bado kuna mambo mengi ya kuvutia yaliyosalia, kwa mfano, makusanyo ya thread-salama, TPL Dataflow, Reactive programming, Software Transaction model, nk.

Vyanzo

Chanzo: mapenzi.com

Kuongeza maoni