Filozofë të ushqyer mirë ose programim konkurrues .NET

Filozofë të ushqyer mirë ose programim konkurrues .NET

Le të shohim se si funksionon programimi i njëkohshëm dhe paralel në .Net, duke përdorur si shembull problemin e ngrënies së filozofëve. Plani është ky, nga sinkronizimi i fijeve/proceseve, te modeli i aktorit (në pjesët në vijim). Artikulli mund të jetë i dobishëm për njohjen e parë ose për të rifreskuar njohuritë tuaja.

Pse ta bëni fare? Transistorët arrijnë madhësinë e tyre minimale, ligji i Moore-it mbështetet në kufizimin e shpejtësisë së dritës dhe për këtë arsye vërehet një rritje në numër, mund të bëhen më shumë transistorë. Në të njëjtën kohë, sasia e të dhënave po rritet dhe përdoruesit presin një përgjigje të menjëhershme nga sistemet. Në një situatë të tillë, programimi "normal", kur kemi një thread ekzekutues, nuk është më efektiv. Ju duhet të zgjidhni disi problemin e ekzekutimit të njëkohshëm ose të njëkohshëm. Për më tepër, ky problem ekziston në nivele të ndryshme: në nivelin e thread-eve, në nivelin e proceseve, në nivelin e makinave në rrjet (sistemet e shpërndara). .NET ka teknologji të cilësisë së lartë, të testuara me kohë për zgjidhjen e shpejtë dhe efikase të problemeve të tilla.

Detyrë

Edsger Dijkstra ua shtroi këtë problem studentëve të tij që në vitin 1965. Formulimi i vendosur është si më poshtë. Ekziston një numër i caktuar (zakonisht pesë) filozofësh dhe i njëjti numër pirunësh. Ata ulen në një tryezë të rrumbullakët, me pirunë midis tyre. Filozofët mund të hanë nga pjatat e tyre ushqim të pafund, të mendojnë ose të presin. Për të ngrënë një filozof, duhet të marrësh dy pirunë (e fundit e ndan pirunin me të parën). Marrja dhe ulja e një piruni janë dy veprime të veçanta. Të gjithë filozofët heshtin. Detyra është të gjesh një algoritëm të tillë që të gjithë të mendojnë dhe të jenë plot edhe pas 54 vjetësh.

Së pari, le të përpiqemi ta zgjidhim këtë problem përmes përdorimit të një hapësire të përbashkët. Pirunët shtrihen në tryezën e përbashkët dhe filozofët thjesht i marrin kur janë dhe i vendosin përsëri. Këtu ka probleme me sinkronizimin, kur saktësisht të merrni surebets? po sikur të mos ketë pirun? etj. Por së pari, le të fillojmë filozofët.

Për të filluar fijet, ne përdorim një grup fijesh përmes Task.Run metoda:

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

Grupi i temave është krijuar për të optimizuar krijimin dhe fshirjen e temave. Ky grup ka një radhë me detyra dhe CLR krijon ose heq thread në varësi të numrit të këtyre detyrave. Një pishinë për të gjitha AppDomains. Kjo pishinë duhet të përdoret pothuajse gjithmonë, sepse. nuk ka nevojë të shqetësoheni për krijimin, fshirjen e temave, radhët e tyre, etj. Është e mundur pa një pishinë, por atëherë duhet ta përdorni drejtpërdrejt Thread, kjo është e dobishme për rastet kur duhet të ndryshoni prioritetin e një filli, kur kemi një operacion të gjatë, për një fill të parë, etj.

Me fjale te tjera, System.Threading.Tasks.Task klasa është e njëjtë Thread, por me të gjitha llojet e komoditeteve: aftësia për të ekzekutuar një detyrë pas një blloku detyrash të tjera, për t'i kthyer ato nga funksionet, për t'i ndërprerë ato me lehtësi dhe më shumë. etj. Ato nevojiten për të mbështetur ndërtimet asinkronike/pritëse (Modeli asinkron i bazuar në detyrë, sheqer sintaksor për pritjen e operacioneve IO). Ne do të flasim për këtë më vonë.

CancelationTokenSource këtu është e nevojshme në mënyrë që filli të mund të përfundojë vetë në sinjalin e fillit thirrës.

Problemet e sinkronizimit

Filozofë të bllokuar

Mirë, ne dimë të krijojmë tema, le të përpiqemi të hamë drekë:

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

Këtu fillimisht përpiqemi të marrim pirunin e majtë, pastaj pirunin e djathtë, dhe nëse funksionon, atëherë i hamë dhe i vendosim përsëri. Marrja e një piruni është atomike, d.m.th. dy fije nuk mund të marrin një në të njëjtën kohë (e pasaktë: e para lexon se piruni është i lirë, e dyta - gjithashtu, e para merr, e dyta merr). Për këtë Interlocked.CompareExchange, i cili duhet të zbatohet me një udhëzim të procesorit (TSL, XCHG), i cili bllokon një pjesë të memories për leximin dhe shkrimin sekuencial atomik. Dhe SpinWait është ekuivalente me konstruktin while(true) vetëm me pak "magji" - filli merr procesorin (Thread.SpinWait), por ndonjëherë e transferon kontrollin në një thread tjetër (Thread.Yeild) ose bie në gjumë (Thread.Sleep).

Por kjo zgjidhje nuk funksionon, sepse flukset shpejt (për mua brenda një sekonde) bllokohen: të gjithë filozofët marrin pirunin e majtë, por jo atë të djathtën. Më pas, grupi forks ka vlerat: 1 2 3 4 5.

Filozofë të ushqyer mirë ose programim konkurrues .NET

Në figurë, bllokimi i fijeve (bllokimi). E gjelbër - ekzekutimi, e kuqe - sinkronizimi, gri - filli po fle. Rombet tregojnë kohën e fillimit të Detyrave.

Uria e filozofëve

Edhe pse nuk është e nevojshme të mendosh veçanërisht shumë ushqim, por uria e bën këdo që të heqë dorë nga filozofia. Le të përpiqemi të simulojmë situatën e urisë së fijeve në problemin tonë. Uria është kur një fije shkon, por pa punë të rëndësishme, me fjalë të tjera, ky është i njëjti bllokim, vetëm tani filli nuk po fle, por po kërkon në mënyrë aktive diçka për të ngrënë, por nuk ka ushqim. Për të shmangur bllokimin e shpeshtë, do ta kthejmë përsëri pirunin nëse nuk mund të marrim një tjetër.

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

Gjëja e rëndësishme për këtë kod është se dy nga katër filozofë harrojnë të vendosin pirunin e majtë. Dhe rezulton se ata hanë më shumë ushqim, ndërsa të tjerët fillojnë të vdesin nga uria, megjithëse fijet kanë të njëjtin prioritet. Këtu ata nuk po vdesin plotësisht nga uria, sepse. filozofët e këqij i kthejnë përsëri pirunët ndonjëherë. Rezulton se njerëzit e mirë hanë rreth 5 herë më pak se të këqijtë. Pra, një gabim i vogël në kod çon në një rënie të performancës. Këtu vlen të theksohet gjithashtu se një situatë e rrallë është e mundur kur të gjithë filozofët marrin pirun e majtë, nuk ka të djathtë, vendosin të majtën, presin, marrin përsëri të majtën etj. Kjo situatë është gjithashtu një uri, më shumë si një bllokim. Nuk arrita ta përsërisja. Më poshtë është një foto për një situatë ku dy filozofë të këqij kanë marrë të dy pirunët dhe dy të mirë po vdesin nga uria.

Filozofë të ushqyer mirë ose programim konkurrues .NET

Këtu mund të shihni se temat zgjohen ndonjëherë dhe përpiqen të marrin burimin. Dy nga katër bërthamat nuk bëjnë asgjë (grafiku i gjelbër më lart).

Vdekja e një Filozofi

Epo, një problem tjetër që mund të ndërpresë një darkë të lavdishme filozofësh është nëse njëri prej tyre vdes papritur me pirunë në duar (dhe ata do ta varrosin ashtu). Pastaj fqinjët do të mbeten pa darkë. Ju mund të gjeni vetë një kod shembull për këtë rast, për shembull, ai është hedhur jashtë NullReferenceException pasi filozofi merr pirunët. Dhe, nga rruga, përjashtimi nuk do të trajtohet dhe kodi i thirrjes nuk do ta kapë thjesht atë (për këtë AppDomain.CurrentDomain.UnhandledException dhe etj.). Prandaj, përpunuesit e gabimeve nevojiten në vetë fijet dhe me përfundim të këndshëm.

kamerier

Mirë, si ta zgjidhim këtë problem të bllokimit, urisë dhe vdekjes? Ne do të lejojmë vetëm një filozof të arrijë pirunët, të shtojmë një përjashtim të ndërsjellë të fijeve për këtë vend. Si ta bëjmë atë? Supozoni se pranë filozofëve qëndron një kamerier, i cili i jep leje çdo filozofi të marrë pirunët. Si ta bëjmë këtë kamarier dhe si do ta bëjnë filozofët, pyetjet janë interesante.

Mënyra më e thjeshtë është kur filozofët thjesht do t'i kërkojnë vazhdimisht kamerierit qasje në pirunët. Ato. tani filozofët nuk do të presin një pirun afër, por do të presin ose do të pyesin kamerierin. Në fillim, ne përdorim vetëm hapësirën e përdoruesit për këtë, në të nuk përdorim ndërprerje për të thirrur ndonjë procedurë nga kerneli (rreth tyre më poshtë).

Zgjidhjet në hapësirën e përdoruesit

Këtu do të bëjmë njësoj siç bënim me një pirun dhe dy filozofë, do të rrotullohemi në një cikël dhe do të presim. Por tani do të jenë të gjithë filozofë dhe, si të thuash, vetëm një pirun, d.m.th. mund të thuhet se do të hajë vetëm filozofi që ia mori këtë “pirun të artë” kamerierit. Për këtë ne përdorim 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 ky është një bllokues, me, përafërsisht, i njëjtë while(true) { if (!lock) break; }, por me edhe më shumë “magji” se sa në SpinWait (që përdoret atje). Tani ai di t'i numërojë ata që presin, t'i vërë në gjumë pak dhe më shumë. etj. Në përgjithësi, bën gjithçka që është e mundur për të optimizuar. Por duhet të kujtojmë se ky është ende i njëjti cikël aktiv që ha burimet e procesorit dhe ruan rrjedhën, gjë që mund të çojë në uri nëse njëri prej filozofëve bëhet më prioritar se të tjerët, por nuk ka një pirun të artë (problemi i përmbysjes së përparësisë) . Prandaj, ne e përdorim atë vetëm për ndryshime shumë shumë të shkurtra në memorien e përbashkët, pa asnjë thirrje nga palët e treta, bravë të ndërthurur dhe surpriza të tjera.

Filozofë të ushqyer mirë ose programim konkurrues .NET

Vizatim për SpinLock. Përrenjtë vazhdimisht “luftojnë” për pirunin e artë. Ka dështime - në figurë, zona e zgjedhur. Bërthamat nuk janë përdorur plotësisht: vetëm rreth 2/3 nga këto katër fije.

Një zgjidhje tjetër këtu do të ishte përdorimi vetëm Interlocked.CompareExchange me të njëjtën pritje aktive siç tregohet në kodin e mësipërm (te filozofët e uritur), por kjo, siç u tha tashmë, teorikisht mund të çojë në bllokim.

Interlocked Duhet të theksohet se nuk ka vetëm CompareExchange, por edhe metoda të tjera për leximin dhe shkrimin atomik. Dhe përmes përsëritjes së ndryshimit, në rast se një fije tjetër ka kohë për të bërë ndryshimet e saj (lexo 1, lexo 2, shkruaj 2, shkruaj 1 është e keqe), mund të përdoret për ndryshime komplekse në një vlerë të vetme (model i ndërlidhur çdo gjë) .

Zgjidhjet e modalitetit të kernelit

Për të shmangur humbjen e burimeve në një lak, le të shohim se si mund të bllokojmë një fije. Me fjalë të tjera, duke vazhduar shembullin tonë, le të shohim se si kamarieri e vë në gjumë filozofin dhe e zgjon vetëm kur është e nevojshme. Së pari, le të shohim se si ta bëjmë këtë përmes mënyrës së kernelit të sistemit operativ. Të gjitha strukturat atje janë shpesh më të ngadalta se ato në hapësirën e përdoruesit. Për shembull, disa herë më ngadalë AutoResetEvent ndoshta 53 herë më ngadalë SpinLock [Rihter]. Por me ndihmën e tyre, ju mund të sinkronizoni proceset në të gjithë sistemin, të menaxhuara ose jo.

Konstrukti bazë këtu është semafori i propozuar nga Dijkstra mbi gjysmë shekulli më parë. Një semafor është, thënë thjesht, një numër i plotë pozitiv i menaxhuar nga sistemi dhe dy operacione në të, rritje dhe zvogëlim. Nëse nuk arrin të ulet, zero, atëherë filli i thirrjes bllokohet. Kur numri shtohet nga ndonjë fill/proces tjetër aktiv, atëherë thread-et anashkalohen dhe semafori zvogëlohet përsëri me numrin e kaluar. Dikush mund të imagjinojë trenat në një fyell të ngushtë me një semafor. .NET ofron disa konstruksione me funksionalitet të ngjashëm: AutoResetEvent, ManualResetEvent, Mutex dhe veten time Semaphore. ne do të përdorim AutoResetEvent, kjo është më e thjeshta nga këto ndërtime: vetëm dy vlera 0 dhe 1 (false, e vërtetë). Metoda e saj WaitOne() bllokon thread-in thirrës nëse vlera ishte 0, dhe nëse 1, e ul atë në 0 dhe e anashkalon atë. Një metodë Set() ngre në 1 dhe lë një kamerier të kalojë, i cili përsëri ul në 0. Vepron si një rrotullues metroje.

Le ta komplikojmë zgjidhjen dhe ta përdorim bllokimin për çdo filozof, dhe jo për të gjithë menjëherë. Ato. tani mund të ketë disa filozofë njëherësh, dhe jo një. Por ne përsëri bllokojmë hyrjen në tavolinë në mënyrë që, në mënyrë korrekte, duke shmangur garat (kushtet e garës), të marrim surebete.

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

Për të kuptuar se çfarë po ndodh këtu, merrni parasysh rastin kur filozofi nuk arriti të merrte pirunët, atëherë veprimet e tij do të jenë si më poshtë. Ai është duke pritur për qasje në tryezë. Pasi e ka marrë, ai përpiqet të marrë pirunët. Nuk funksionoi. Ai jep akses në tabelë (përjashtim reciprok). Dhe kalon "turnstilet" e tij (AutoResetEvent) (fillimisht janë të hapura). Hyn sërish në cikël, sepse ai nuk ka pirunë. Përpiqet t'i marrë dhe ndalet në "turnstilet" e tij. Një fqinj më me fat djathtas ose majtas, pasi ka mbaruar së ngrëni, zhbllokon filozofin tonë, duke "hapur rrotullën e tij". Filozofi ynë e kalon (dhe mbyllet pas saj) për herë të dytë. Ai përpiqet për të tretën herë të marrë pirunët. Paç fat. Dhe ai kalon rrotullën e tij për të ngrënë.

Kur ka gabime të rastësishme në një kod të tillë (ato ekzistojnë gjithmonë), për shembull, një fqinj është specifikuar gabimisht ose krijohet i njëjti objekt AutoResetEvent per te gjithe (Enumerable.Repeat), atëherë filozofët do të presin zhvilluesit, sepse Gjetja e gabimeve në një kod të tillë është një detyrë mjaft e vështirë. Një problem tjetër me këtë zgjidhje është se nuk garanton që ndonjë filozof nuk do të vdesë nga uria.

Zgjidhje hibride

Ne kemi parë dy qasje për kohën, kur qëndrojmë në modalitetin e përdoruesit dhe loop, dhe kur bllokojmë thread-in përmes kernelit. Metoda e parë është e mirë për flokët e shkurtër, e dyta për ato të gjata. Shpesh është e nevojshme që fillimisht të pritet shkurtimisht që një variabël të ndryshojë në një lak, dhe më pas të bllokohet thread kur pritja është e gjatë. Kjo qasje zbatohet në të ashtuquajturat. strukturat hibride. Këtu janë të njëjtat konstruksione si për modalitetin e kernelit, por tani me një lak të modalitetit të përdoruesit: SemaphorSlim, ManualResetEventSlim etj. Dizajni më popullor këtu është Monitor, sepse në C# ka një të njohur lock sintaksë. Monitor ky është i njëjti semafor me një vlerë maksimale prej 1 (mutex), por me mbështetje për pritjen në një lak, rekursion, modelin Condition Variable (më shumë për këtë më poshtë), etj. Le të shohim një zgjidhje me të.

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

Këtu ne përsëri po bllokojmë të gjithë tryezën për të hyrë në pirunët, por tani po zhbllokojmë të gjitha temat menjëherë, dhe jo fqinjët kur dikush mbaron së ngrëni. Ato. së pari, dikush ha dhe bllokon fqinjët, dhe kur ky dikush mbaron, por dëshiron të hajë përsëri menjëherë, ai futet në bllokim dhe zgjon fqinjët e tij, sepse. koha e tij e pritjes është më e vogël.

Kështu shmangim ngërçet dhe urinë e ndonjë filozofi. Ne përdorim një lak për një pritje të shkurtër dhe bllokojmë fillin për një kohë të gjatë. Zhbllokimi i të gjithëve në të njëjtën kohë është më i ngadalshëm se sa të zhbllokohej vetëm fqinji, si në zgjidhjen me AutoResetEvent, por ndryshimi nuk duhet të jetë i madh, sepse fillesat duhet të qëndrojnë në modalitetin e përdoruesit së pari.

У lock sintaksa ka surpriza të pakëndshme. Rekomandohet të përdoret Monitor drejtpërdrejt [Richter] [Eric Lippert]. Një prej tyre është se lock gjithmonë jashtë Monitor, edhe nëse do të kishte një përjashtim, në këtë rast një thread tjetër mund të ndryshojë gjendjen e kujtesës së përbashkët. Në raste të tilla, shpesh është më mirë të shkosh në bllokim ose disi ta ndërpresësh programin në mënyrë të sigurt. Një surprizë tjetër është se Monitor përdor blloqe sinkronizimi (SyncBlock), të cilat janë të pranishme në të gjitha objektet. Prandaj, nëse zgjidhet një objekt i papërshtatshëm, mund të merrni lehtësisht një bllokim (për shembull, nëse bllokoni një varg të internuar). Ne përdorim objektin gjithmonë të fshehur për këtë.

Modeli i variablit të gjendjes ju lejon të zbatoni në mënyrë më koncize pritshmërinë e disa kushteve komplekse. Në .NET është e paplotë, për mendimin tim, sepse në teori, duhet të ketë disa radhë në disa variabla (si në Posix Threads), dhe jo në një lok. Atëherë dikush mund t'i bëjë ato për të gjithë filozofët. Por edhe në këtë formë, ju lejon të zvogëloni kodin.

shumë filozofë ose async / await

Mirë, tani ne mund të bllokojmë në mënyrë efektive temat. Po sikur të kemi shumë filozofë? 100? 10000? Për shembull, ne kemi marrë 100000 kërkesa në serverin e uebit. Krijimi i një thread për çdo kërkesë do të jetë i ngarkuar, sepse kaq shumë fije nuk do të ecin paralelisht. Do të funksionojë vetëm aq sa ka bërthama logjike (kam 4). Dhe të gjithë të tjerët thjesht do të heqin burimet. Një zgjidhje për këtë problem është modeli i asinkronizimit / pritjes. Ideja e tij është që funksioni nuk e mban thread-in nëse duhet të presë që diçka të vazhdojë. Dhe kur bën diçka, ai rifillon ekzekutimin e tij (por jo domosdoshmërisht në të njëjtën fillesë!). Në rastin tonë, ne do të presim pirunin.

SemaphoreSlim ka për këtë WaitAsync() metodë. Këtu është një zbatim duke përdorur këtë model.

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

Metoda me async / await përkthehet në një makineri të ndërlikuar të gjendjes që kthen menjëherë të brendshmen e saj Task. Nëpërmjet tij, ju mund të prisni për përfundimin e metodës, ta anuloni atë dhe gjithçka tjetër që mund të bëni me Task. Brenda metodës, makina shtetërore kontrollon ekzekutimin. Në fund të fundit është se nëse nuk ka vonesë, atëherë ekzekutimi është sinkron, dhe nëse ka, atëherë filli lëshohet. Për një kuptim më të mirë të kësaj, është më mirë të shikoni këtë makinë shtetërore. Ju mund të krijoni zinxhirë prej tyre async / await metodat.

Le të testojmë. Puna e 100 filozofëve në një makinë me 4 bërthama logjike, 8 sekonda. Zgjidhja e mëparshme me Monitor ekzekutoi vetëm 4 temat e para dhe pjesa tjetër nuk funksionoi fare. Secila prej këtyre 4 fijeve ishte e papunë për rreth 2 ms. Dhe zgjidhja e asinkronizimit / pritjes funksionoi të gjitha 100, me një pritje mesatare prej 6.8 sekondash secila. Sigurisht, në sistemet reale, papunësia për 6 sekonda është e papranueshme dhe është më mirë të mos përpunohen kaq shumë kërkesa si kjo. Zgjidhja me Monitor doli të mos ishte aspak e shkallëzueshme.

Përfundim

Siç mund ta shihni nga këta shembuj të vegjël, .NET mbështet shumë konstruksione sinkronizimi. Sidoqoftë, nuk është gjithmonë e qartë se si t'i përdorni ato. Shpresoj se ky artikull ishte i dobishëm. Për momentin, ky është fundi, por kanë mbetur ende shumë gjëra interesante, për shembull, koleksione të sigurta për thread, TPL Dataflow, Programimi Reaktiv, Modeli i Transaksionit me Software, etj.

burime

Burimi: www.habr.com

Shto një koment