Gerai maitinami filosofai arba konkurencingas .NET programavimas

Gerai maitinami filosofai arba konkurencingas .NET programavimas

Pažiūrėkime, kaip veikia lygiagretusis ir lygiagretusis programavimas .Net, kaip pavyzdį naudodami Filosofų valgymo problemą. Planas yra toks, nuo gijų / procesų sinchronizavimo iki veikėjo modelio (tolimesnėse dalyse). Straipsnis gali būti naudingas pirmajai pažinčiai arba norint atnaujinti žinias.

Kodėl išvis tai daryti? Tranzistoriai pasiekia minimalų dydį, Moore'o dėsnis remiasi šviesos greičio apribojimu, todėl pastebimas jų skaičiaus padidėjimas, galima pagaminti daugiau tranzistorių. Tuo pačiu metu auga duomenų kiekis, o vartotojai tikisi greito atsako iš sistemų. Tokioje situacijoje „normalus“ programavimas, kai turime vieną vykdomąją giją, nebeveikia. Turite kažkaip išspręsti vienalaikio ar lygiagrečio vykdymo problemą. Be to, ši problema egzistuoja įvairiais lygiais: gijų, procesų, tinklo mašinų (paskirstytų sistemų) lygiu. .NET turi aukštos kokybės, laiko patikrintas technologijas greitai ir efektyviai išspręsti tokias problemas.

Užduotis

Edsgeris Dijkstra šią problemą iškėlė savo mokiniams dar 1965 m. Nustatyta formuluotė yra tokia. Yra tam tikras (dažniausiai penki) filosofų skaičius ir tiek pat šakių. Jie sėdi prie apvalaus stalo, tarp jų šakutės. Filosofai gali valgyti iš savo begalės maisto lėkščių, galvoti arba laukti. Norint suvalgyti filosofą, reikia paimti dvi šakutes (paskutinė dalijasi šakute su pirmąja). Šakės paėmimas ir nuleidimas yra du atskiri veiksmai. Visi filosofai tyli. Užduotis – surasti tokį algoritmą, kad visi susimąstytų ir būtų sotūs net po 54 metų.

Pirmiausia pabandykime išspręsti šią problemą naudodami bendrą erdvę. Šakės guli ant bendro stalo, o filosofai jas tiesiog paima ir padeda atgal. Čia yra problemų su sinchronizavimu, kada tiksliai imti tikruosius pinigus? o jei nėra šakutės? ir tt Bet pirmiausia pradėkime nuo filosofų.

Norėdami pradėti siūlus, naudojame siūlų telkinį Task.Run metodas:

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

Gijų telkinys sukurtas siekiant optimizuoti gijų kūrimą ir ištrynimą. Šiame telkinyje yra užduočių eilė, o CLR sukuria arba pašalina gijas, priklausomai nuo šių užduočių skaičiaus. Vienas baseinas visiems AppDomains. Šis baseinas turėtų būti naudojamas beveik visada, nes. nereikia sukti galvos su gijų kūrimu, trynimu, jų eilėmis ir pan. Galima ir be baseino, bet tuomet reikia jį naudoti tiesiogiai Thread, tai naudinga tais atvejais, kai reikia pakeisti gijos prioritetą, kai atliekame ilgą operaciją, priekinio plano gijai ir pan.

Kitaip tariant, System.Threading.Tasks.Task klasė ta pati Thread, bet su visais patogumais: galimybe paleisti užduotį po bloko kitų užduočių, grąžinti jas iš funkcijų, patogiai jas pertraukti ir kt. ir tt Jie reikalingi asinchronizavimo / laukimo konstrukcijoms palaikyti (užduočių pagrindu sukurtas asinchroninis šablonas, sintaksinis cukrus, skirtas laukti IO operacijų). Apie tai pakalbėsime vėliau.

CancelationTokenSource čia jis reikalingas, kad gija galėtų pati baigtis gavus skambinančios gijos signalą.

Sinchronizavimo problemos

Užblokuoti filosofai

Gerai, mes žinome, kaip kurti gijas, pabandykime papietauti:

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

Čia pirmiausia bandome paimti kairę šakutę, o po to dešinę, o jei pavyksta, tada valgome ir dedame atgal. Vienos šakutės paėmimas yra atomazga, t.y. du siūlai negali paimti vieno vienu metu (neteisingai: pirmasis rašo, kad šakutė laisva, antrasis - taip pat, pirmasis paima, antrasis). Už tai Interlocked.CompareExchange, kuris turi būti įgyvendintas su procesoriaus instrukcija (TSL, XCHG), kuris užrakina atminties dalį atominiam nuosekliam skaitymui ir rašymui. Ir SpinWait prilygsta konstrukcijai while(true) tik su šiek tiek „stebuklinga“ - gija paima procesorių (Thread.SpinWait), bet kartais perkelia valdymą į kitą giją (Thread.Yeild) arba užmiega (Thread.Sleep).

Tačiau šis sprendimas neveikia, nes srautai greitai (man per sekundę) užblokuojami: visi filosofai ima kairę šakę, bet ne dešinę. Tada šakių masyvo reikšmės yra: 1 2 3 4 5.

Gerai maitinami filosofai arba konkurencingas .NET programavimas

Paveiksle blokuojantys siūlai (aklavietė). Žalia - vykdymas, raudona - sinchronizacija, pilka - siūlas miega. Rombai rodo užduočių pradžios laiką.

Filosofų badas

Nors ir nebūtina ypač daug galvoti apie maistą, bet alkis bet ką priverčia atsisakyti filosofijos. Pabandykime imituoti mūsų problemos siūlų bado situaciją. Badavimas yra tada, kai bėga siūlas, bet be reikšmingo darbo, kitaip tariant, čia ta pati aklavietė, tik dabar siūlas nemiega, o aktyviai ieško, ko užkąsti, bet maisto nėra. Kad išvengtume dažno užsikimšimo, jei negalėsime paimti kitos, šakę grąžinsime atgal.

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

Šiame kodekse svarbu tai, kad du iš keturių filosofų pamiršta padėti kairę šakę. Ir pasirodo, kad jie valgo daugiau maisto, o kiti pradeda badauti, nors siūlai turi tą patį prioritetą. Čia jie visiškai nebadauja, nes. blogi filosofai kartais atideda šakutes. Pasirodo, geri žmonės valgo maždaug 5 kartus mažiau nei blogi. Taigi dėl nedidelės klaidos kode sumažėja našumas. Čia verta atkreipti dėmesį ir į tai, kad galima reta situacija, kai visi filosofai ima kairę šakę, dešinės nėra, deda kairę, laukia, vėl ima į kairę ir t.t. Ši situacija taip pat yra badas, labiau panašus į aklavietę. Man nepavyko to pakartoti. Žemiau pateikiamas paveikslas situacijai, kai du blogi filosofai paėmė abi šakutes, o du geri badauja.

Gerai maitinami filosofai arba konkurencingas .NET programavimas

Čia matote, kad gijos kartais pabunda ir bando gauti išteklių. Du iš keturių branduolių nieko nedaro (žalia diagrama aukščiau).

Filosofo mirtis

Na, o dar viena bėda, galinti nutraukti šlovingą filosofų vakarienę – jei vienas iš jų staiga miršta su šakėmis rankose (ir taip jį palaidos). Tada kaimynai liks be vakarienės. Galite patys sugalvoti pavyzdinį kodą šiam atvejui, pavyzdžiui, jis yra išmestas NullReferenceException filosofui paėmus šakutes. Ir, beje, išimtis nebus tvarkoma ir skambinimo kodas ne tik ją sugaus (už tai AppDomain.CurrentDomain.UnhandledException ir pan.). Todėl klaidų tvarkyklės reikalingos pačiose gijose ir su maloniu nutraukimu.

Padavėjas

Gerai, kaip išspręsti šią aklavietės, bado ir mirties problemą? Mes leisime tik vienam filosofui pasiekti šakes, pridėsime abipusį siūlų išskyrimą šiai vietai. Kaip tai padaryti? Tarkime, kad šalia filosofų stovi padavėjas, kuris duoda leidimą bet kuriam filosofui paimti šakutes. Kaip mes pagaminsime šį padavėją ir kaip jį užduos filosofai, klausimai įdomūs.

Paprasčiausias būdas yra tada, kai filosofai tiesiog nuolat prašys padavėjo prieigos prie šakių. Tie. dabar filosofai nelauks šakutės šalia, o lauks arba klaus padavėjo. Iš pradžių tam naudojame tik vartotojo erdvę, joje nenaudojame pertraukimų, kad iškviestume jokias branduolio procedūras (apie jas žemiau).

Sprendimai vartotojų erdvėje

Čia darysime taip pat, kaip darydavome su viena šakute ir dviem filosofais, suksime ciklu ir lauksime. Bet dabar tai bus visi filosofai ir tarsi tik viena šakutė, t.y. galima sakyti, kad valgys tik filosofas, paėmęs iš padavėjo šią „auksinę šakutę“. Tam naudojame 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 tai blokatorius, grubiai tariant, tas pats while(true) { if (!lock) break; }, bet su dar daugiau „stebuklingų“ nei in SpinWait (kuris ten naudojamas). Dabar jis moka suskaičiuoti laukiančius, šiek tiek užmigdyti ir dar daugiau. ir tt Apskritai daro viską, kas įmanoma, kad optimizuotų. Tačiau turime prisiminti, kad tai vis tiek yra tas pats aktyvus ciklas, kuris suryja procesoriaus išteklius ir palaiko srautą, o tai gali sukelti badą, jei vienas iš filosofų bus svarbesnis už kitus, bet neturi auksinės šakutės (Priority Inversion problema) . Todėl mes jį naudojame tik labai trumpiems bendrosios atminties pakeitimams, be jokių trečiųjų šalių skambučių, įdėtų spynų ir kitų netikėtumų.

Gerai maitinami filosofai arba konkurencingas .NET programavimas

Piešimas už SpinLock. Upeliai nuolat „kovoja“ dėl auksinės šakutės. Yra gedimų – paveikslėlyje pasirinkta sritis. Šerdys nėra visiškai išnaudotos: tik apie 2/3 šių keturių gijų.

Kitas sprendimas čia būtų naudoti tik Interlocked.CompareExchange su tokiu pat aktyviu laukimu, kaip parodyta aukščiau esančiame kode (badaujantiems filosofams), tačiau tai, kaip jau minėta, teoriškai gali sukelti blokavimą.

apie Interlocked Reikia pažymėti, kad yra ne tik CompareExchange, bet ir kiti atominio skaitymo IR rašymo metodai. Ir kartojant pakeitimą, jei kita gija turi laiko atlikti pakeitimus (skaityti 1, skaityti 2, rašyti 2, rašyti 1 yra blogai), ji gali būti naudojama sudėtingiems vienos reikšmės pakeitimams (Interlocked Anything modelis) .

Branduolio režimo sprendimai

Kad nebūtų švaistomi resursai cikle, pažiūrėkime, kaip galime blokuoti giją. Kitaip tariant, tęsdami savo pavyzdį, pažiūrėkime, kaip padavėjas užmigdo filosofą ir pažadina tik tada, kai reikia. Pirmiausia pažiūrėkime, kaip tai padaryti naudojant operacinės sistemos branduolio režimą. Visos ten esančios struktūros dažnai yra lėtesnės nei esančios vartotojo erdvėje. Pavyzdžiui, kelis kartus lėčiau AutoResetEvent gal 53 kartus lėčiau SpinLock [Richteris]. Tačiau su jų pagalba galite sinchronizuoti procesus visoje sistemoje, valdomus ar ne.

Pagrindinė konstrukcija čia yra Dijkstra daugiau nei prieš pusę amžiaus pasiūlytas semaforas. Paprasčiau tariant, semaforas yra teigiamas sveikasis skaičius, kurį valdo sistema, ir su juo atliekamos dvi operacijos: padidinimas ir mažinimas. Jei nepavyksta sumažėti, nulis, vadinasi, skambinanti gija užblokuojama. Kai skaičius padidinamas kita aktyvia gija/procesu, gijos praleidžiamos ir semaforas vėl sumažinamas pagal perduotą skaičių. Galima įsivaizduoti traukinius, esančius kamštyje su semaforu. .NET siūlo keletą konstrukcijų su panašiomis funkcijomis: AutoResetEvent, ManualResetEvent, Mutex ir aš pats Semaphore. Mes naudosime AutoResetEvent, tai pati paprasčiausia iš šių konstrukcijų: tik dvi reikšmės 0 ir 1 (netiesa, tiesa). Jos metodas WaitOne() blokuoja skambinimo giją, jei reikšmė buvo 0, o jei 1, sumažina ją iki 0 ir praleidžia. Metodas Set() pakelia iki 1 ir praleidžia vieną padavėją, kuris vėl nuleidžia iki 0. Veikia kaip metro turniketas.

Apsunkinkime sprendimą ir naudokime užraktą kiekvienam filosofui, o ne visiems iš karto. Tie. dabar gali būti keli filosofai vienu metu, ir ne vienas. Bet mes vėl blokuojame priėjimą prie stalo, kad teisingai, išvengdami lenktynių (lenktynių sąlygų), atliktume garantinius statymus.

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

Norėdami suprasti, kas čia vyksta, apsvarstykite atvejį, kai filosofas nesugebėjo paimti šakių, tada jo veiksmai bus tokie. Jis laukia prieigos prie stalo. Gavęs bando paimti šakutes. Nepavyko. Tai suteikia prieigą prie lentelės (abipusė išskyrimas). Ir praeina savo „turniketą“ (AutoResetEvent) (iš pradžių jie yra atidaryti). Jis vėl patenka į ciklą, nes jis neturi šakių. Jis bando juos paimti ir sustoja prie savo „turniketo“. Kažkoks labiau pasisekęs kaimynas dešinėje ar kairėje, baigęs valgyti, atrakina mūsų filosofą, „atidarydamas turniketą“. Mūsų filosofas ją praeina (ir užsidaro už jos) antrą kartą. Jis trečią kartą bando paimti šakutes. Sėkmės. Ir jis perduoda savo turniketą pietauti.

Kai tokiame kode yra atsitiktinių klaidų (jie visada egzistuoja), pavyzdžiui, neteisingai nurodytas kaimynas arba sukuriamas tas pats objektas AutoResetEvent visiems (Enumerable.Repeat), tada filosofai lauks kūrėjų, nes Rasti klaidas tokiame kode yra gana sudėtinga užduotis. Kita šio sprendimo problema yra ta, kad jis negarantuoja, kad koks nors filosofas nemirs badu.

Hibridiniai sprendimai

Išnagrinėjome du laiko nustatymo būdus: kai liekame vartotojo režimu ir cikle, ir kai blokuojame giją per branduolį. Pirmasis metodas tinka trumpoms spynoms, antrasis - ilgoms. Dažnai pirmiausia reikia trumpai palaukti, kol kilpoje pasikeis kintamasis, o tada užblokuoti giją, kai laukimas ilgas. Šis požiūris įgyvendinamas vadinamojoje. hibridinės struktūros. Čia yra tos pačios konstrukcijos, kaip ir branduolio režimui, bet dabar su vartotojo režimo kilpa: SemaphorSlim, ManualResetEventSlim tt Populiariausias dizainas čia yra Monitor, nes C# yra gerai žinomas lock sintaksė. Monitor tai tas pats semaforas, kurio didžiausia reikšmė yra 1 (mutex), bet palaiko laukimą cikle, rekursiją, sąlygos kintamojo šabloną (apie tai toliau) ir tt Pažvelkime į sprendimą su juo.

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

Čia mes vėl blokuojame visą stalą, kad galėtume pasiekti šakutes, bet dabar atblokuojame visas gijas iš karto, o ne kaimynus, kai kas nors baigia valgyti. Tie. pirma, kažkas valgo ir užblokuoja kaimynus, o kai šis baigia, bet tuoj pat nori vėl valgyti, jis eina į blokavimą ir pažadina kaimynus, nes. jo laukimo laikas yra trumpesnis.

Taip išvengiame aklavietės ir kokio nors filosofo bado. Mes naudojame kilpą trumpam laukimui ir užblokuojame siūlą ilgam. Visų atblokavimas iš karto vyksta lėčiau nei tuo atveju, jei būtų atblokuotas tik kaimynas, kaip ir sprendime AutoResetEvent, bet skirtumas neturėtų būti didelis, nes gijos pirmiausia turi likti vartotojo režimu.

У lock sintaksė turi nemalonių netikėtumų. Rekomenduoti naudoti Monitor tiesiogiai [Richteris] [Ericas Lippertas]. Vienas iš jų yra tas lock visada išeina Monitor, net jei būtų išimtis, tokiu atveju kita gija galėtų pakeisti bendros atminties būseną. Tokiais atvejais dažnai geriau išeiti į aklavietę arba kaip nors saugiai nutraukti programą. Kitas netikėtumas yra tai, kad monitorius naudoja sinchronizavimo blokus (SyncBlock), kurių yra visuose objektuose. Todėl, jei pasirenkamas netinkamas objektas, galite lengvai patekti į aklavietę (pavyzdžiui, jei užrakinate įterptą eilutę). Tam naudojame visada paslėptą objektą.

Sąlygos kintamo modelis leidžia glausčiau įgyvendinti tam tikros sudėtingos sąlygos lūkesčius. Mano nuomone, .NET jis yra neišsamus, nes teoriškai turėtų būti kelios eilės keliuose kintamuosiuose (kaip Posix Threads), o ne viename lok. Tada būtų galima juos sukurti visiems filosofams. Tačiau net ir šioje formoje tai leidžia sumažinti kodą.

daugelis filosofų ar async / await

Gerai, dabar galime efektyviai blokuoti gijas. Bet ką daryti, jei turime daug filosofų? 100? 10000 100000? Pavyzdžiui, žiniatinklio serveriui gavome 4 XNUMX užklausų. Kiekvienai užklausai sukurti giją reikės papildomų išlaidų, nes tiek daug gijų neveiks lygiagrečiai. Veiks tik tiek, kiek yra loginių branduolių (turiu XNUMX). O visi kiti tiesiog atims išteklius. Vienas iš šios problemos sprendimų yra asinchronizavimo / laukimo modelis. Jos idėja yra ta, kad funkcija nelaiko gijos, jei reikia laukti, kol kažkas tęsis. Ir kai jis ką nors padaro, jis atnaujina savo vykdymą (bet nebūtinai toje pačioje gijoje!). Mūsų atveju lauksime šakutės.

SemaphoreSlim turi už tai WaitAsync() metodas. Štai įgyvendinimas naudojant šį 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();
}

Metodas su async / await yra paverčiamas sudėtinga būsenos mašina, kuri iš karto grąžina savo vidinį Task. Per jį galite laukti, kol bus baigtas metodas, jį atšaukti ir visa kita, ką galite padaryti su užduotimi. Metodo viduje būsenos mašina kontroliuoja vykdymą. Esmė ta, kad jei nėra delsos, tada vykdymas yra sinchroninis, o jei yra, tada siūlas atleidžiamas. Norint geriau tai suprasti, geriau pažvelgti į šią būsenos mašiną. Iš jų galite sukurti grandines async / await metodus.

Išbandykime. 100 filosofų darbas mašinoje su 4 loginiais branduoliais, 8 sekundės. Ankstesnis sprendimas su monitoriumi paleido tik pirmąsias 4 gijas, o likusios iš viso neveikė. Kiekviena iš šių 4 gijų buvo neaktyvi maždaug 2 ms. Ir asinchronizavimo / laukimo sprendimas veikė visus 100, vidutiniškai laukdami po 6.8 sekundės. Žinoma, tikrose sistemose 6 sekundžių tuščioji eiga yra nepriimtina ir geriau neapdoroti tiek daug tokių užklausų. Sprendimas su monitoriumi pasirodė esąs visiškai nekeičiamas.

išvada

Kaip matote iš šių mažų pavyzdžių, .NET palaiko daug sinchronizavimo konstrukcijų. Tačiau ne visada aišku, kaip juos naudoti. Tikiuosi, kad šis straipsnis buvo naudingas. Kol kas tai pabaiga, bet dar liko daug įdomių dalykų, pavyzdžiui, saugios gijos kolekcijos, TPL duomenų srautas, reaktyvusis programavimas, programinės įrangos transakcijų modelis ir kt.

Informacijos šaltiniai

Šaltinis: www.habr.com

Добавить комментарий