Ongi elikatutako filosofoak edo .NET programazio lehiakorra

Ongi elikatutako filosofoak edo .NET programazio lehiakorra

Ikus dezagun nola funtzionatzen duen programazio konkurrente eta paraleloa .Net-en, Philosophers Dining Problema adibide gisa erabiliz. Plana hau da, hari/prozesuen sinkronizaziotik hasi eta aktore-ereduraino (ondoko zatietan). Artikulua baliagarria izan daiteke lehen ezagunarentzat edo ezagutzak freskatzeko.

Zergatik egin? Transistoreak bere gutxieneko tamainara iristen dira, Mooreren legea argiaren abiaduraren mugan oinarritzen da eta horregatik kopuruaren igoera ikusten da, transistore gehiago egin daitezke. Aldi berean, datu kopurua hazten ari da, eta erabiltzaileek berehalako erantzuna espero dute sistemen aldetik. Egoera horretan, programazio "normala", hari bat exekutatzen dugunean, ez da eraginkorra. Aldibereko edo aldi berean exekuzioaren arazoa nolabait konpondu behar duzu. Gainera, arazo hau maila ezberdinetan dago: harien mailan, prozesuen mailan, sareko makinen mailan (sistema banatuak). .NET-ek kalitate handiko teknologiak ditu, denboran frogatuta, horrelako arazoak azkar eta eraginkortasunez konpontzeko.

Task

Edsger Dijkstrak arazo hau planteatu zien bere ikasleei 1965ean. Ezarritako formulazioa honakoa da. Filosofo kopuru jakin bat (normalean bost) eta sardexka kopuru bera dago. Mahai-inguru batean esertzen dira, sardexkak haien artean. Filosofoek janari amaigabeko plateretatik jan dezakete, pentsatu edo itxaron. Filosofo bat jateko bi sardexka hartu behar dira (azkenak sardexka lehenengoarekin partekatzen du). Sardexka bat jasotzea eta uztea bi ekintza bereizi dira. Filosofo guztiak isilik daude. Zeregin hori da, 54 urte igarota ere denak pentsatu eta beteta egon daitezen halako algoritmo bat aurkitzea.

Lehenik eta behin, saia gaitezen arazo hau konpontzen espazio partekatu baten bitartez. Sardexkak mahai arruntean daude eta filosofoek daudenean hartu eta berriro jarri besterik ez dute egiten. Hemen arazoak daude sinkronizazioarekin, noiz hartu behar diren ziur apostuak? zer gertatzen da sardexkarik ez badago? etab. Baina lehenik eta behin, has gaitezen filosofoak.

Hariak hasteko, hari multzo bat erabiltzen dugu Task.Run metodoa:

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

Hari multzoa haria sortzea eta ezabatzea optimizatzeko diseinatuta dago. Igerileku honek zereginekin ilara bat dauka eta CLR-k hariak sortu edo kentzen ditu zeregin horien kopuruaren arabera. Aplikazio-domeinu guztietarako igerileku bat. Igerileku hau ia beti erabili behar da, zeren. ez da trabarik sortu, ezabatzen hariak, haien ilarak, etab. Igerilekurik gabe posible da, baina gero zuzenean erabili behar duzu. Thread, hau erabilgarria da hari baten lehentasuna aldatu behar duzun kasuetarako, eragiketa luzea dugunean, Lehen planoko hari baterako, etab.

Beste hitz batzutan, System.Threading.Tasks.Task klasea berdina da Thread, baina era guztietako erosotasunekin: zeregin bat beste zereginen bloke baten ondoren exekutatzeko gaitasuna, funtzioetatik itzultzeko, eroso eteteko eta abar. eta abar. Beharrezkoa da eraikuntza asinkronikoak / itxaroten laguntzeko (Zereginetan oinarritutako eredu asinkronoa, IO eragiketen zain egoteko azukre sintaktikoa). Honetaz gero hitz egingo dugu.

CancelationTokenSource hemen behar da haria dei egiten duen hariaren seinalean amaitu ahal izateko.

Sinkronizazio arazoak

Blokeatutako filosofoak

Ados, badakigu hariak sortzen, saia gaitezen bazkaltzen:

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

Hemen lehenik ezkerreko sardexka hartzen saiatzen gara, eta gero eskuineko sardexka, eta ondo bada, jan eta berriro jartzen ditugu. Sardexka bat hartzea atomikoa da, hau da. bi hari ezin dute bat hartu aldi berean (okerra: lehenengoak sardexka libre dagoela irakurtzen du, bigarrenak ere, lehenengoak hartzen, bigarrenak). Honetarako Interlocked.CompareExchange, prozesadorearen instrukzio batekin inplementatu behar dena (TSL, XCHG), irakurketa eta idazketa sekuentzial atomikorako memoria zati bat blokeatzen duena. Eta SpinWait eraikuntzaren baliokidea da while(true) "Magia" pixka batekin bakarrik - hariak prozesadorea hartzen du (Thread.SpinWait), baina batzuetan kontrola beste hari batera transferitzen du (Thread.Yeild) edo lo egiten du (Thread.Sleep).

Baina irtenbide honek ez du funtzionatzen, zeren fluxuak laster (niretzat segundo batean) blokeatzen dira: filosofo guztiek hartzen dute ezkerreko sardexka, baina ez eskuinekoa. Horks arrayak balio hauek ditu: 1 2 3 4 5.

Ongi elikatutako filosofoak edo .NET programazio lehiakorra

Irudian, hariak blokeatzea (blokeoa). Berdea - exekuzioa, gorria - sinkronizazioa, grisa - haria lo dago. Erronboek Zereginen hasiera-ordua adierazten dute.

Filosofoen gosea

Batez ere janari asko pentsatu behar ez den arren, goseak edonork filosofiari uko egiten dio. Saia gaitezen gure arazoan harien gosete egoera simulatzen. Gosea hari bat martxan dagoenean da, baina lan esanguratsurik gabe, hau da, blokeo berdina da, soilik orain haria ez dago lo, baina aktiboki jateko zerbait bilatzen ari da, baina ez dago janaririk. Maiz blokeatzea saihesteko, sardexka berriro jarriko dugu beste bat hartu ezin badugu.

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

Kode honen garrantzitsuena lau filosofotik bi ezkerreko sardexka uztea ahazten zaiela da. Eta ematen da janari gehiago jaten dutela, beste batzuk gosez hiltzen hasten diren bitartean, hariek lehentasun bera duten arren. Hemen ez dira guztiz gosez hiltzen, zeren. filosofo gaiztoek sardexka atzera jartzen dute batzuetan. Ikusten da jende onek txarrak baino 5 aldiz gutxiago jaten duela. Beraz, kodearen errore txiki batek errendimenduaren jaitsiera dakar. Hemen ere azpimarratzekoa da egoera arraroa posible dela filosofo guztiek ezkerreko bidegurutzea hartzen dutenean, eskuinekorik ez dagoenean, ezkerra jartzen dutenean, itxaron, berriro ezkerrera hartu, etab. Egoera hau ere gosea da, blokeo baten antzera. Ez nuen errepikatu. Behean bi filosofo txar bi sardexkak hartu dituzten eta bi on gosez hiltzen diren egoera baten argazkia dago.

Ongi elikatutako filosofoak edo .NET programazio lehiakorra

Hemen ikus dezakezu hariak batzuetan esnatzen direla eta baliabidea lortzen saiatzen direla. Lau nukleoetatik bik ez dute ezer egiten (goiko grafiko berdea).

Filosofo baten heriotza

Bada, filosofoen afari loriatsu bat eten dezakeen beste arazo bat hauetako bat bat-batean sardexka eskuetan duela hiltzen bada (eta horrela ehortziko dute). Orduan auzokideak afaririk gabe geratuko dira. Kasu honetarako kode adibide bat atera dezakezu zuk zeuk, adibidez, bota egiten da NullReferenceException filosofoak sardexka hartu ondoren. Eta, bide batez, salbuespena ez da kudeatuko eta dei-kodeak ez du bakarrik harrapatuko (horretarako AppDomain.CurrentDomain.UnhandledException eta abar). Hori dela eta, errore-kudeatzaileak behar dira harietan bertan eta amaiera dotorearekin.

zerbitzari

Ados, nola konponduko dugu blokeoa, gosea eta heriotza arazo hau? Filosofo bakarra sardexketara iristen utziko dugu, toki honetarako harien elkarrekiko bazterketa gehituko diogu. Nola egin? Demagun filosofoen ondoan zerbitzari bat dagoela, edozein filosofori sardexkak hartzeko baimena ematen diona. Nola egiten dugu zerbitzari hau eta filosofoek nola galdetuko dioten, galderak interesgarriak dira.

Modurik errazena filosofoek zerbitzariari sardexketarako sarbidea eskatzen diotenean besterik gabe. Horiek. orain filosofoek ez dute sardexka baten zain egongo gertu, itxaron edo galdetu zerbitzariari baizik. Hasieran, Erabiltzaile-espazioa bakarrik erabiltzen dugu horretarako, bertan ez dugu etenik erabiltzen nukleotik inolako prozedurarik deitzeko (behean horiei buruz).

Irtenbideak erabiltzaileen espazioan

Hemen sardexka batekin eta bi filosoforekin egiten genuen berdina egingo dugu, ziklo batean bira egingo dugu eta itxarongo dugu. Baina orain filosofo guztiak izango dira eta, nolabait, sardexka bakarra, alegia. zerbitzariari β€œurrezko sardexka” hori hartu zuen filosofoak bakarrik jango duela esan daiteke. Horretarako SpinLock erabiltzen dugu.

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 hau blokeatzailea da, gutxi gorabehera, berdinarekin while(true) { if (!lock) break; }, baina are "magia" gehiagorekin SpinWait (hor erabiltzen dena). Orain badaki zain daudenak zenbatzen, lo pixka bat jartzen eta gehiago. etab. Oro har, ahal den guztia egiten du optimizatzeko. Baina gogoratu behar dugu oraindik prozesadorearen baliabideak jaten dituen eta fluxua mantentzen duen ziklo aktibo bera dela, eta horrek gosea ekar dezakeela filosoforen batek besteek baino lehentasun handiagoa hartzen badu, baina urrezko sardexkarik ez badu (Lehentasunezko Inbertsioaren arazoa) . Hori dela eta, memoria partekatuan aldaketa oso laburretarako bakarrik erabiltzen dugu, hirugarrenen deirik, blokeo habiaraturik eta bestelako sorpresarik gabe.

Ongi elikatutako filosofoak edo .NET programazio lehiakorra

Marrazkia SpinLock. Errekak etengabe "borrokan" dabiltza urrezko sardexkaren alde. Porrotak daude - irudian, hautatutako eremua. Nukleoak ez dira guztiz erabiltzen: lau hari hauen 2/3 inguru bakarrik.

Hemen beste irtenbide bat bakarrik erabiltzea litzateke Interlocked.CompareExchange goiko kodean erakusten den itxaron aktibo berarekin (goseak diren filosofoetan), baina honek, esan bezala, teorikoki blokeoa ekar lezake.

ΠŸΡ€ΠΎ Interlocked Kontuan izan behar da ez dagoela bakarrik CompareExchange, baina baita irakurketa ETA idazketa atomikorako beste metodo batzuk ere. Eta aldaketen errepikapenaren bidez, beste hari batek bere aldaketak egiteko denbora badu (irakurri 1, irakurri 2, idatzi 2, idatzi 1 txarra da), balio bakarreko aldaketa konplexuetarako erabil daiteke (Interlocked Anything eredua).

Kernel moduko irtenbideak

Baliabideak begizta batean alferrik gal ez dadin, ikus dezagun nola blokeatu dezakegun hari bat. Alegia, gure adibidearekin jarraituz, ikus dezagun zerbitzariak filosofoa lo egiten duen eta behar denean bakarrik esnatzen duen. Lehenik eta behin, ikus dezagun nola egin sistema eragilearen kernel moduan. Bertan dauden egitura guztiak erabiltzaileen espazioan daudenak baino motelagoak izaten dira. Hainbat aldiz motelagoa, adibidez AutoResetEvent agian 53 aldiz motelagoa SpinLock [Richter]. Baina haien laguntzarekin, prozesuak sinkroniza ditzakezu sistema osoan, kudeatutako edo ez.

Hemen oinarrizko eraikuntza Dijkstrak duela mende erdi baino gehiago proposatutako semaforoa da. Semaforo bat, besterik gabe, sistemak kudeatzen duen zenbaki oso positibo bat da, eta haren gainean bi eragiketa, handitzea eta txikitzea. Murrizten huts egiten badu, zero, orduan deitzen duen haria blokeatuta dago. Zenbakia beste hari/prozesu aktiboren batek gehitzen duenean, hariak saltatzen dira eta semaforoa berriro gutxitzen da gainditutako zenbakiaren arabera. Irudika daitezke trenak semaforo batekin botila batean. .NET-ek antzeko funtzionalitate duten hainbat eraikuntza eskaintzen ditu: AutoResetEvent, ManualResetEvent, Mutex eta ni neu Semaphore. Erabiliko dugu AutoResetEvent, hau da eraikuntza hauetako sinpleena: bi balio bakarrik 0 eta 1 (faltsua, egia). Bere Metodoa WaitOne() deitzen duen haria blokeatzen du balioa 0 bada, eta 1 bada, 0ra jaisten du eta saltatzen du. Metodo bat Set() 1era igotzen da eta zerbitzari bati pasatzen uzten dio, eta honek berriro 0ra jaisten du. Metroko tornika gisa jokatzen du.

Zaildu dezagun konponbidea eta erabil dezagun sarraila filosofo bakoitzarentzat, eta ez denentzat aldi berean. Horiek. orain hainbat filosofo egon daitezke aldi berean, eta ez bat. Baina berriro ere blokeatzen dugu mahairako sarbidea, lasterketak (lasterketen baldintzak) saihestuz, surebets zuzen egiteko.

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

Hemen gertatzen ari dena ulertzeko, kontuan hartu filosofoak sardexkak hartu ez zituen kasua, orduan bere ekintzak hauek izango dira. Mahaira sartzeko zain dago. Jasota, sardexkak hartzen saiatzen da. Ez zuen funtzionatu. Mahairako sarbidea ematen du (elkarrekiko bazterketa). Eta bere "tornotxoa" pasatzen du (AutoResetEvent) (hasieran irekita daude). Zikloan sartzen da berriro, zeren ez du sardexkarik. Hartzen saiatzen da eta bere β€œtornotxoan” gelditzen da. Eskuineko edo ezkerreko bizilagun zoriontsuagoren batek, jaten bukatuta, desblokeatzen du gure filosofoa, Β«tornuska irekizΒ». Gure filosofoak pasatzen du (eta atzean ixten da) bigarren aldiz. Hirugarren aldiz saiatzen da bidegurutzeak hartzen. Zorte on. Eta afaltzera pasatzen du bere txanoa.

Kode horretan ausazko akatsak daudenean (beti existitzen dira), adibidez, auzokide bat gaizki zehazten da edo objektu bera sortzen da. AutoResetEvent guztientzat (Enumerable.Repeat), orduan filosofoak garatzaileen zain egongo dira, zeren Kode horretan akatsak aurkitzea nahiko lan zaila da. Irtenbide honen beste arazo bat da ez duela bermatzen filosoforen bat gosez hilko ez denik.

Soluzio hibridoak

Denboraldiaren bi ikuspegi aztertu ditugu, erabiltzaile moduan eta begiztetan geratzen garenean eta nukleoaren bidez haria blokeatzen dugunean. Lehenengo metodoa ona da sarrail laburretarako, bigarrena luzeetarako. Askotan beharrezkoa da aldagai bat begizta batean aldatzeko labur itxarotea lehenik eta behin, eta gero haria blokeatu itxaronaldia luzea denean. Planteamendu hau deiturikoak ezartzen dira. egitura hibridoak. Hona hemen nukleoaren moduko eraikuntza berdinak, baina orain erabiltzaile moduaren begizta batekin: SemaphorSlim, ManualResetEventSlim etab. Hemen diseinu ezagunena da Monitor, zeren C#-n ezaguna da lock sintaxia. Monitor hau semaforo bera da 1 (mutex) gehienezko balioa duena, baina begizta batean itxaroteko laguntzarekin, errekurtsioarekin, Baldintza aldagaien ereduarekin (hori gehiago behean), etab. Ikus dezagun harekin irtenbide bat.

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

Hemen berriro mahai osoa blokeatzen ari gara sardexketarako sarbidea izateko, baina orain hari guztiak aldi berean desblokeatzen ari gara, eta ez auzokideak jaten amaitzen duenean. Horiek. lehenengo, norbaitek auzokoak jan eta blokeatzen ditu, eta hau norbaitek amaitzen duenean, baina berehala berriro jan nahi duenean, blokeatzen sartu eta auzokideak esnatzen ditu, zeren. bere itxaron denbora txikiagoa da.

Horrela saihesten ditugu blokeoak eta filosofo batzuen gosea. Begizta bat erabiltzen dugu itxaronaldi labur baterako eta haria blokeatzen dugu luzerako. Guztiak aldi berean desblokeatzea bizilaguna bakarrik desblokeatuko balitz baino motelagoa da, irtenbidean bezala AutoResetEvent, baina aldea ez da handia izan behar, izan ere hariak erabiltzaile moduan egon behar dute lehenik.

Π£ lock sintaxiak sorpresa txarrak ditu. Erabiltzea gomendatu Monitor zuzenean [Richter] [Eric Lippert]. Horietako bat hori da lock beti kanpoan Monitor, nahiz eta salbuespen bat egon, kasu horretan beste hari batek partekatutako memoriaren egoera alda dezake. Horrelako kasuetan, sarritan hobe da blokeora joatea edo nolabait programa seguru amaitzea. Beste sorpresa bat da Monitoreak sinkronizazio blokeak erabiltzen dituela (SyncBlock), objektu guztietan daudenak. Hori dela eta, objektu desegoki bat hautatzen bada, erraz lor dezakezu blokeoa (adibidez, barneratutako kate batean blokeatzen baduzu). Beti ezkutuko objektua erabiltzen dugu horretarako.

Baldintza aldagaiaren ereduak baldintza konplexu batzuen itxaropena zehatzago ezartzeko aukera ematen du. .NETen, osatu gabe dago, nire ustez, zeren teorian, hainbat ilara egon beharko lirateke hainbat aldagaitan (Posix Threads-en bezala), eta ez lok batean. Orduan filosofo guztientzat egin litezke. Baina forma honetan ere, kodea murrizteko aukera ematen du.

filosofo asko edo async / await

Ados, orain eraginkortasunez blokea ditzakegu hariak. Baina filosofo asko baditugu? 100? 10000? Adibidez, 100000 eskaera jaso ditugu web zerbitzariari. Eskaera bakoitzerako hari bat sortzea gainkostua izango da, zeren hainbeste hari ez dira paraleloan ibiliko. Nukleo logikoak dauden adina bakarrik exekutatzen ditu (4 ditut). Eta beste guztiek baliabideak kenduko dituzte. Arazo honen irtenbide bat async/wait eredua da. Bere ideia da funtzioak ez duela haria eusten zerbaitek jarraitu arte itxaron behar badu. Eta zerbait egiten duenean, bere exekuzioari ekiten dio berriro (baina ez nahitaez hari berean!). Gure kasuan, sardexkaren zain egongo gara.

SemaphoreSlim du horretarako WaitAsync() metodoa. Hona hemen eredu hau erabiltzen duen inplementazioa.

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

Metodoarekin async / await bere barnekoa berehala itzultzen duen egoera zaila den makina batera itzultzen da Task. Horren bidez, metodoa amaitu arte itxaron dezakezu, bertan behera utzi eta Task-ekin egin dezakezun beste guztia. Metodoaren barruan, egoera makinak kontrolatzen du exekuzioa. Beheko lerroa da atzerapenik ez badago, orduan exekuzioa sinkronoa da, eta badago, haria askatzen da. Hau hobeto ulertzeko, hobe da egoera makina honi begiratzea. Hauetatik kateak sor ditzakezu async / await metodoak.

Egin dezagun proba. 100 filosoforen lana 4 nukleo logikoko makina batean, 8 segundo. Monitor-ekin aurreko irtenbideak lehen 4 hariak bakarrik exekutatzen zituen eta gainerakoak ez ziren batere exekutatu. 4 hari horietako bakoitza 2 ms inguru egon zen inaktibo. Eta async/wait irtenbideak 100 guztiak exekutatu zituen, batez beste 6.8 segundoko itxaronarekin. Jakina, sistema errealetan, 6 segundo inaktiboa onartezina da eta hobe da horrelako hainbeste eskaera ez prozesatzea. Monitor-ekin irtenbidea ez zen batere eskalagarria izan.

Ondorioa

Adibide txiki hauetatik ikus dezakezunez, .NET-ek sinkronizazio-eraikuntza asko onartzen ditu. Hala ere, ez da beti argi nola erabili. Artikulu hau lagungarria izatea espero dut. Oraingoz, hau da amaiera, baina oraindik gauza interesgarri asko geratzen dira, adibidez, thread-safe bildumak, TPL Dataflow, Programazio erreaktiboa, Software Transaction eredua, etab.

iturri

Iturria: www.habr.com

Gehitu iruzkin berria