Goed gevoede filosowe of mededingende .NET-programmering

Goed gevoede filosowe of mededingende .NET-programmering

Kom ons kyk hoe gelyktydige en parallelle programmering in .Net werk, deur die Philosophers Dining Problem as voorbeeld te gebruik. Die plan is dit, van die sinchronisasie van drade / prosesse, tot die akteursmodel (in die volgende dele). Die artikel kan nuttig wees vir die eerste kennismaking of om jou kennis te verfris.

Hoekom dit enigsins doen? Transistors bereik hul minimum grootte, Moore se wet berus op die beperking van die spoed van lig en daarom word 'n toename in die aantal waargeneem, meer transistors kan gemaak word. Terselfdertyd groei die hoeveelheid data, en gebruikers verwag 'n onmiddellike reaksie van die stelsels. In so 'n situasie is "normale" programmering, wanneer ons een uitvoerdraad het, nie meer effektief nie. Jy moet op een of ander manier die probleem van gelyktydige of gelyktydige uitvoering oplos. Boonop bestaan ​​hierdie probleem op verskillende vlakke: op die vlak van drade, op die vlak van prosesse, op die vlak van masjiene in die netwerk (verspreide stelsels). .NET het hoë-gehalte, beproefde tegnologieë om sulke probleme vinnig en doeltreffend op te los.

Taak

Edsger Dijkstra het hierdie probleem reeds in 1965 aan sy studente gestel. Die gevestigde formulering is soos volg. Daar is 'n sekere (gewoonlik vyf) aantal filosowe en dieselfde aantal vurke. Hulle sit by 'n ronde tafel, vurk tussen hulle. Filosowe kan van hul borde van eindelose kos eet, dink of wag. Om 'n filosoof te eet, moet jy twee vurke neem (die laaste een deel die vurk met die eerste). Om 'n vurk op en neer te sit is twee afsonderlike aksies. Alle filosowe swyg. Die taak is om so 'n algoritme te vind dat almal van hulle sou dink en vol wees selfs na 54 jaar.

Kom ons probeer eers hierdie probleem oplos deur 'n gedeelde ruimte te gebruik. Die vurke lê op die gemeenskaplike tafel en die filosowe vat dit eenvoudig wanneer hulle is en sit dit terug. Hier is daar probleme met sinchronisasie, wanneer presies om seker te neem? wat as daar geen vurk is nie? ens. Maar eers, laat ons die filosowe begin.

Om drade te begin, gebruik ons ​​'n draadpoel deur 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);
}

Die draadpoel is ontwerp om draadskepping en -skrap te optimaliseer. Hierdie poel het 'n tou met take en die CLR skep of verwyder drade afhangende van die aantal van hierdie take. Een poel vir alle AppDomains. Hierdie swembad moet byna altyd gebruik word, want. jy hoef nie moeite te doen met die skep, uitvee van drade, hul toue, ens. Dit is moontlik sonder 'n swembad, maar dan moet jy dit direk gebruik Thread, dit is nuttig vir gevalle wanneer jy die prioriteit van 'n draad moet verander, wanneer ons 'n lang operasie het, vir 'n Voorgronddraad, ens.

Met ander woorde, System.Threading.Tasks.Task klas is dieselfde Thread, maar met allerhande geriewe: die vermoë om 'n taak na 'n blok ander take uit te voer, dit van funksies terug te stuur, dit gerieflik te onderbreek, en meer. ens. Hulle is nodig om asinchrone / wag-konstruksies te ondersteun (Taakgebaseerde asynchrone patroon, sintaktiese suiker vir wag vir IO-operasies). Ons sal later hieroor praat.

CancelationTokenSource hier is dit nodig sodat die draad homself kan beëindig by die sein van die roepende draad.

Sinkroniseer kwessies

Geblokkeerde Filosowe

Goed, ons weet hoe om drade te skep, kom ons probeer middagete eet:

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

Hier probeer ons eers die linkervurk vat, en dan die regtervurk, en as dit uitwerk, dan eet ons en sit hulle terug. Om een ​​vurk te vat is atoom, d.w.s. twee drade kan nie een op dieselfde tyd neem nie (verkeerd: die eerste lees dat die vurk vry is, die tweede - ook die eerste neem, die tweede neem). Vir dit Interlocked.CompareExchange, wat geïmplementeer moet word met 'n verwerkerinstruksie (TSL, XCHG), wat 'n stukkie geheue sluit vir atomiese opeenvolgende lees en skryf. En SpinWait is gelykstaande aan die konstruksie while(true) net met 'n bietjie "magic" - die draad neem die verwerker (Thread.SpinWait), maar dra soms beheer oor na 'n ander draad (Thread.Yeild) of raak aan die slaap (Thread.Sleep).

Maar hierdie oplossing werk nie, want die vloeie word binnekort (vir my binne 'n sekonde) geblokkeer: alle filosowe vat hul linkervurk, maar nie die regter een nie. Die vurke-skikking het dan die waardes: 1 2 3 4 5.

Goed gevoede filosowe of mededingende .NET-programmering

In die figuur, blokkeer drade (dooie punt). Groen - uitvoering, rooi - sinchronisasie, grys - die draad slaap. Die ruite dui die begintyd van Take aan.

Die honger van die filosowe

Alhoewel dit nie nodig is om veral baie kos te dink nie, maar honger laat enigiemand filosofie prysgee. Kom ons probeer om die situasie van verhongering van drade in ons probleem te simuleer. Hongersnood is wanneer 'n draad loop, maar sonder noemenswaardige werk, met ander woorde, dit is dieselfde dooiepunt, net nou slaap die draad nie, maar is aktief op soek na iets om te eet, maar daar is nie kos nie. Om gereelde blokkering te vermy, sal ons die vurk terugsit as ons nie nog een kon vat nie.

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

Die belangrike ding van hierdie kode is dat twee uit vier filosowe vergeet om hul linkervurk neer te sit. En dit blyk dat hulle meer kos eet, terwyl ander begin honger ly, hoewel die drade dieselfde prioriteit het. Hier is hulle nie heeltemal honger nie, want. slegte filosowe sit soms hul vurke terug. Dit blyk dat goeie mense ongeveer 5 keer minder eet as slegtes. So 'n klein fout in die kode lei tot 'n daling in prestasie. Dit is ook opmerklik hier dat 'n seldsame situasie moontlik is wanneer alle filosowe die linkervurk neem, daar is geen regter een nie, hulle sit die linkerkant, wag, neem weer links, ens. Hierdie situasie is ook 'n hongersnood, meer soos 'n dooie punt. Ek het versuim om dit te herhaal. Hieronder is 'n prentjie vir 'n situasie waar twee slegte filosowe albei vurke geneem het en twee goeies honger ly.

Goed gevoede filosowe of mededingende .NET-programmering

Hier kan jy sien dat die drade soms wakker word en probeer om die hulpbron te kry. Twee van die vier kerne doen niks (groen grafiek hierbo).

Dood van 'n filosoof

Wel, nog 'n probleem wat 'n heerlike ete van filosowe kan onderbreek, is as een van hulle skielik met vurke in sy hande sterf (en hulle sal hom so begrawe). Dan sal die bure sonder middagete gelaat word. U kan self 'n voorbeeldkode vir hierdie saak uitdink, dit word byvoorbeeld uitgegooi NullReferenceException nadat die filosoof die vurke vat. En terloops, die uitsondering sal nie hanteer word nie en die oproepkode sal dit nie net vang nie (hiervoor AppDomain.CurrentDomain.UnhandledException en ens.). Daarom is fouthanteerders nodig in die drade self en met grasieuse beëindiging.

kelner

Goed, hoe los ons hierdie dooiepunt, hongersnood en doodsprobleem op? Ons sal slegs een filosoof toelaat om die vurke te bereik, voeg 'n wedersydse uitsluiting van drade vir hierdie plek by. Hoe om dit te doen? Gestel daar staan ​​'n kelner langs die filosowe, wat aan enige filosoof toestemming gee om die vurke te vat. Hoe maak ons ​​hierdie kelner en hoe filosowe hom sal vra, die vrae is interessant.

Die eenvoudigste manier is wanneer die filosowe die kelner eenvoudig gedurig vir toegang tot die vurke sal vra. Dié. nou sal filosowe nie wag vir 'n vurk naby nie, maar wag of vra die kelner. Aanvanklik gebruik ons ​​slegs Gebruikersruimte hiervoor, daarin gebruik ons ​​nie onderbrekings om enige prosedures vanaf die kern (oor hulle hieronder) op te roep nie.

Oplossings in gebruikersruimte

Hier sal ons dieselfde doen as wat ons met een vurk en twee filosowe gedoen het, ons sal in 'n siklus draai en wag. Maar nou sal dit alle filosowe wees en as't ware net een vurk, m.a.w. daar kan gesê word dat slegs die filosoof wat hierdie “goue vurk” by die kelner geneem het, sal eet. Hiervoor gebruik ons ​​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 dit is 'n blokker, met, rofweg gesproke, dieselfde while(true) { if (!lock) break; }, maar met selfs meer "magic" as in SpinWait (wat daar gebruik word). Nou weet hy hoe om diegene wat wag te tel, hulle 'n bietjie te laat slaap, en meer. ens. In die algemeen, doen alles moontlik om te optimaliseer. Maar ons moet onthou dat dit steeds dieselfde aktiewe siklus is wat verwerkerhulpbronne opvreet en die vloei behou, wat tot hongersnood kan lei as een van die filosowe meer prioriteit as ander kry, maar nie 'n goue vurk het nie (Prioriteit Inversie probleem) . Daarom gebruik ons ​​dit net vir baie baie kort veranderinge in gedeelde geheue, sonder enige derdeparty-oproepe, geneste slotte en ander verrassings.

Goed gevoede filosowe of mededingende .NET-programmering

Tekening vir SpinLock. Die strome “baklei” gedurig vir die goue vurk. Daar is mislukkings - in die figuur, die geselekteerde area. Die kerne word nie ten volle benut nie: slegs ongeveer 2/3 by hierdie vier drade.

Nog 'n oplossing hier sou wees om slegs te gebruik Interlocked.CompareExchange met dieselfde aktiewe wag as getoon in die kode hierbo (by die uitgehongerde filosowe), maar dit, soos reeds gesê, kan teoreties tot blokkering lei.

op Interlocked Daar moet kennis geneem word dat daar nie net CompareExchange, maar ook ander metodes vir atomiese lees EN skryf. En deur die herhaling van die verandering, ingeval 'n ander draad tyd het om sy veranderinge aan te bring (lees 1, lees 2, skryf 2, skryf 1 is sleg), kan dit gebruik word vir komplekse veranderinge aan 'n enkele waarde (Interlocked Anything-patroon) .

Kernelmodus-oplossings

Om te verhoed dat hulpbronne in 'n lus gemors word, kom ons kyk hoe ons 'n draad kan blokkeer. Met ander woorde, om ons voorbeeld voort te sit, kom ons kyk hoe die kelner die filosoof laat slaap en hom net wakker maak wanneer dit nodig is. Kom ons kyk eers hoe om dit te doen deur die kernmodus van die bedryfstelsel. Alle strukture daar is dikwels stadiger as dié in gebruikersruimte. 'n Paar keer stadiger, byvoorbeeld AutoResetEvent miskien 53 keer stadiger SpinLock [Richter]. Maar met hul hulp kan jy prosesse regdeur die stelsel sinchroniseer, bestuur of nie.

Die basiese konstruk hier is die semafoor wat Dijkstra meer as 'n halfeeu gelede voorgestel het. 'n Semafoor is, eenvoudig gestel, 'n positiewe heelgetal wat deur die stelsel bestuur word, en twee bewerkings daarop, verhoog en verminder. As dit nie afneem nie, nul, dan word die oproepdraad geblokkeer. Wanneer die getal deur 'n ander aktiewe draad/proses verhoog word, word die drade oorgeslaan en die semafoor word weer verminder met die getal wat geslaag is. Mens kan jou treine in 'n bottelnek met 'n semafoor voorstel. .NET bied verskeie konstrukte met soortgelyke funksionaliteit: AutoResetEvent, ManualResetEvent, Mutex en myself Semaphore. Ons sal gebruik AutoResetEvent, dit is die eenvoudigste van hierdie konstruksies: slegs twee waardes 0 en 1 (onwaar, waar). Haar metode WaitOne() blokkeer die oproepdraad as die waarde 0 was, en as 1, verlaag dit na 0 en slaan dit oor. 'n Metode Set() verhoog tot 1 en laat een kelner deur, wat weer verlaag na 0. Tree op soos 'n moltrein-draaihek.

Kom ons kompliseer die oplossing en gebruik die slot vir elke filosoof, en nie vir almal op een slag nie. Dié. nou kan daar verskeie filosowe gelyktydig wees, en nie een nie. Maar ons blokkeer weer toegang tot die tafel om korrek, om wedrenne (wedrentoestande) te vermy, seker weddenskappe te neem.

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

Om te verstaan ​​wat hier gebeur, oorweeg die geval toe die filosoof versuim het om die vurke te vat, dan sal sy optrede soos volg wees. Hy wag vir toegang tot die tafel. Nadat hy dit ontvang het, probeer hy die vurke vat. Het nie uitgewerk nie. Dit gee toegang tot die tabel (wedersydse uitsluiting). En gaan verby sy "draaihek" (AutoResetEvent) (hulle is aanvanklik oop). Dit kom weer in die siklus, want hy het geen vurke nie. Hy probeer hulle vat en stop by sy "draaihek". Een of ander meer gelukkige buurman aan die regter- of linkerkant, nadat hy klaar geëet het, sluit ons filosoof oop, "maak sy draaihek oop." Ons filosoof slaag dit (en dit sluit agter dit) vir die tweede keer. Hy probeer vir die derde keer om die vurke te vat. Sterkte. En hy gaan sy draaihek verby om te eet.

Wanneer daar ewekansige foute in sulke kode is (hulle bestaan ​​altyd), word 'n buurman byvoorbeeld verkeerd gespesifiseer of dieselfde voorwerp word geskep AutoResetEvent vir alle (Enumerable.Repeat), dan sal die filosowe vir die ontwikkelaars wag, want Om foute in sulke kode te vind, is nogal 'n moeilike taak. Nog 'n probleem met hierdie oplossing is dat dit nie waarborg dat een of ander filosoof nie honger sal ly nie.

Hibriede oplossings

Ons het na twee benaderings tot tydsberekening gekyk, wanneer ons in gebruikersmodus en lus bly, en wanneer ons draad deur die kern blokkeer. Die eerste metode is goed vir kort slotte, die tweede vir langs. Dit is dikwels nodig om eers kortliks te wag vir 'n veranderlike om in 'n lus te verander, en dan die draad te blokkeer wanneer die wag lank is. Hierdie benadering word geïmplementeer in die sg. hibriede strukture. Hier is dieselfde konstrukte as vir kernmodus, maar nou met 'n gebruikersmoduslus: SemaphorSlim, ManualResetEventSlim ens. Die gewildste ontwerp hier is Monitor, omdat in C# is daar 'n bekende lock sintaksis. Monitor dit is dieselfde semafoor met 'n maksimum waarde van 1 (mutex), maar met ondersteuning vir wag in 'n lus, rekursie, die Condition Variable-patroon (meer daaroor hieronder), ens. Kom ons kyk na 'n oplossing daarmee.

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

Hier blokkeer ons weer die hele tafel vir toegang tot die vurke, maar nou ontblokkeer ons alle drade op een slag, en nie bure as iemand klaar geëet het nie. Dié. eers eet en blokkeer iemand die bure, en wanneer hierdie iemand klaar is, maar dadelik weer wil eet, gaan hy in blok en maak sy bure wakker, want. sy wagtyd is minder.

Dit is hoe ons dooiepunte en die hongersnood van een of ander filosoof vermy. Ons gebruik 'n lus vir 'n kort wag en blokkeer die draad vir 'n lang een. Om almal gelyktydig te ontblokkeer is stadiger as wanneer net die buurman gedeblokkeer is, soos in die oplossing met AutoResetEvent, maar die verskil moet nie groot wees nie, want drade moet eers in gebruikersmodus bly.

У lock sintaksis het nare verrassings. Beveel aan om te gebruik Monitor direk [Richter] [Eric Lippert]. Een van hulle is dit lock altyd uit Monitor, selfs al was daar 'n uitsondering, in welke geval 'n ander draad die gedeelde geheue-toestand kan verander. In sulke gevalle is dit dikwels beter om na dooiepunt te gaan of die program op een of ander manier veilig te beëindig. Nog 'n verrassing is dat Monitor sinchronisasieblokke gebruik (SyncBlock), wat in alle voorwerpe teenwoordig is. Daarom, as 'n onvanpaste voorwerp gekies word, kan jy maklik 'n dooie punt kry (byvoorbeeld as jy op 'n geïnterneerde string sluit). Ons gebruik die altyd verborge voorwerp hiervoor.

Die Condition Variable-patroon laat jou toe om die verwagting van een of ander komplekse toestand meer bondig te implementeer. In .NET is dit na my mening onvolledig, want in teorie moet daar verskeie toue op verskeie veranderlikes wees (soos in Posix Threads), en nie op een lok nie. Dan kon mens dit vir alle filosowe maak. Maar selfs in hierdie vorm laat dit jou toe om die kode te verminder.

baie filosowe of async / await

Goed, nou kan ons drade effektief blokkeer. Maar wat as ons baie filosowe het? 100? 10000 100000? Ons het byvoorbeeld 4 XNUMX versoeke aan die webbediener ontvang. Dit sal oorhoofs wees om 'n draad vir elke versoek te skep, want so baie drade sal nie parallel loop nie. Sal net soveel hardloop as wat daar logiese kerne is (ek het XNUMX). En almal anders sal net hulpbronne wegneem. Een oplossing vir hierdie probleem is die asinc/wag-patroon. Die idee daarvan is dat die funksie nie die draad hou as dit moet wag vir iets om voort te gaan nie. En wanneer dit iets doen, hervat dit die uitvoering daarvan (maar nie noodwendig op dieselfde draad nie!). In ons geval sal ons wag vir die vurk.

SemaphoreSlim het hiervoor WaitAsync() metode. Hier is 'n implementering wat hierdie patroon gebruik.

// Запуск такой же, как раньше. Где-нибудь в программе:
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 met async / await word vertaal in 'n moeilike staatsmasjien wat onmiddellik sy interne terugstuur Task. Daardeur kan u wag vir die voltooiing van die metode, dit kanselleer en alles anders wat u met Task kan doen. Binne die metode beheer die staatsmasjien die uitvoering. Die slotsom is dat as daar geen vertraging is nie, die uitvoering sinchronies is, en as daar is, dan word die draad vrygestel. Vir 'n beter begrip hiervan, is dit beter om na hierdie staatsmasjien te kyk. Jy kan hieruit kettings skep async / await metodes.

Kom ons toets. Werk van 100 filosowe op 'n masjien met 4 logiese kerns, 8 sekondes. Die vorige oplossing met Monitor het net die eerste 4 drade gehardloop en die res het glad nie geloop nie. Elkeen van hierdie 4 drade was vir ongeveer 2ms ledig. En die async / wag-oplossing het al 100 gehardloop, met 'n gemiddelde wag van 6.8 sekondes elk. Natuurlik, in regte stelsels is ledig vir 6 sekondes onaanvaarbaar en dit is beter om nie soveel versoeke soos hierdie te verwerk nie. Die oplossing met Monitor het geblyk glad nie skaalbaar te wees nie.

Gevolgtrekking

Soos u uit hierdie klein voorbeelde kan sien, ondersteun .NET baie sinchronisasiekonstrukte. Dit is egter nie altyd duidelik hoe om dit te gebruik nie. Ek hoop hierdie artikel was nuttig. Vir eers is dit die einde, maar daar is nog baie interessante dinge oor, byvoorbeeld draad-veilige versamelings, TPL Dataflow, Reaktiewe programmering, Sagteware Transaksie model, ens.

bronne

Bron: will.com

Voeg 'n opmerking