Bone nutritaj filozofoj aŭ konkurenciva programado en .NET

Bone nutritaj filozofoj aŭ konkurenciva programado en .NET

Ni rigardu kiel samtempa kaj paralela programado funkcias en .Net, uzante la ekzemplon de la lunĉaj filozofoj problemo. La plano estas kiel sekvas, de fadeno/proceza sinkronigo ĝis la aktormodelo (en la sekvaj partoj). La artikolo povas esti utila por unua konato aŭ por refreŝigi viajn sciojn.

Kial eĉ scii kiel fari ĉi tion? Transistoroj atingas sian minimuman grandecon, la leĝo de Moore trafas la limon de la lumrapideco, kaj tial kresko estas observita en nombroj; pli da transistoroj povas esti faritaj. Samtempe, la kvanto de datumoj kreskas, kaj uzantoj atendas tujan respondon de sistemoj. En tia situacio, "normala" programado, kiam ni havas unu ekzekutantan fadenon, ne plu efikas. Ni devas iel solvi la problemon de samtempa aŭ samtempa ekzekuto. Krome, ĉi tiu problemo ekzistas je malsamaj niveloj: ĉe la fadennivelo, ĉe la proceznivelo, ĉe la nivelo de maŝinoj en la reto (distribuitaj sistemoj). .NET havas altkvalitajn, tempelprovitajn teknologiojn por rapide kaj efike solvi tiajn problemojn.

Objektivo

Edsger Dijkstra demandis tiun problemon al siaj studentoj reen en 1965. La establita formuliĝo estas kiel sekvas. Estas certa (kutime kvin) nombro da filozofoj kaj sama nombro da forkoj. Ili sidas ĉe ronda tablo, forkoj inter ili. Filozofoj povas manĝi el siaj teleroj da senfina manĝaĵo, pensi aŭ atendi. Por manĝi, filozofo bezonas preni du forkojn (ĉi-lasta kunhavas forkon kun la unua). Preni kaj demeti forkon estas du apartaj agoj. Ĉiuj filozofoj silentas. La tasko estas trovi tian algoritmon, por ke ili ĉiuj pensu kaj estu bone nutrataj eĉ post 54 jaroj.

Unue, ni provu solvi ĉi tiun problemon uzante komunan spacon. La forkoj kuŝas sur la komuna tablo kaj la filozofoj simple prenas ilin kiam ili estas tie kaj remetas ilin. Ĉi tie aperas problemoj kun sinkronigo, kiam ĝuste preni forkojn? kion fari se mankas ŝtopilo? ktp Sed unue, ni komencu per la filozofoj.

Por komenci fadenojn ni uzas fadenon per Task.Run metodo:

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

La fadena naĝejo estas dizajnita por optimumigi la kreadon kaj forigon de fadenoj. Ĉi tiu aro havas vicon da taskoj kaj la CLR kreas aŭ forigas fadenojn depende de la nombro de ĉi tiuj taskoj. Unu naĝejo por ĉiuj AppDomains. Ĉi tiu naĝejo devas esti uzata preskaŭ ĉiam, ĉar... ne bezonas zorgi pri kreado kaj forigo de fadenoj, iliaj vostoj, ktp. Vi povas fari ĝin sen naĝejo, sed tiam vi devos uzi ĝin rekte. Thread, ĉi tio utilas por kazoj kiam ni bezonas ŝanĝi la prioritaton de fadeno, kiam ni havas longan operacion, por Antaŭplana fadeno, ktp.

Alivorte, System.Threading.Tasks.Task klaso estas la sama Thread, sed kun ĉiaj oportunoj: la kapablo lanĉi taskon post bloko de aliaj taskoj, resendi ilin de funkcioj, oportune interrompi ilin, kaj multe pli. ktp. Ili estas bezonataj por subteni nesinkronajn/atendi konstruojn (Task-bazita Asynchronous Pattern, sintaksa sukero por atendado de IO-operacioj). Pri tio ni parolos poste.

CancelationTokenSource ĉi tie necesas, ke la fadeno povas finiĝi per signalo de la alvokanta fadeno.

Sinkronigaj Problemoj

Blokitaj filozofoj

Bone, ni scias kiel krei fadenojn, ni provu tagmanĝon:

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

Ĉi tie ni unue provas preni la maldekstran kaj poste la dekstran forkon, kaj se ĝi funkcias, ni manĝas kaj remetas ilin. Preni unu forkon estas atoma, t.e. du fadenoj ne povas preni unu samtempe (malĝuste: la unua legas, ke la forko estas libera, la dua faras la samon, la unua prenas, la dua prenas). Por ĉi tio Interlocked.CompareExchange, kiu devas esti efektivigita uzante procesoran instrukcion (TSL, XCHG), kiu ŝlosas pecon de memoro por atoma sinsekva legado kaj skribo. Kaj SpinWait estas ekvivalenta al la konstruo while(true) nur kun iom da "magio" - la fadeno prenas la procesoron (Thread.SpinWait), sed foje pasas kontrolon al alia fadeno (Thread.Yeild) aŭ endormiĝas (Thread.Sleep).

Sed ĉi tiu solvo ne funkcias, ĉar... la fadenoj baldaŭ (en sekundo por mi) estas blokitaj: ĉiuj filozofoj prenas sian maldekstran forkon, sed ne ekzistas dekstra. La fork-tabelo tiam havas la valorojn: 1 2 3 4 5.

Bone nutritaj filozofoj aŭ konkurenciva programado en .NET

En la bildo, blokado de fadenoj (blokado). Verda indikas ekzekuton, ruĝa indikas sinkronigon, kaj griza indikas ke la fadeno dormas. Diamantoj indikas la lanĉtempon de Taskoj.

La Malsato de la Filozofoj

Kvankam vi ne bezonas multe da manĝaĵo por pensi, malsato povas devigi iun ajn forlasi filozofion. Ni provu simuli la situacion de fadena malsato en nia problemo. Malsato estas kiam fadeno funkcias, sed sen grava laboro, alivorte, ĝi estas la sama blokiĝo, nur nun la fadeno ne dormas, sed aktive serĉas ion por manĝi, sed mankas manĝaĵo. Por eviti oftan blokadon, ni remetos la forkon, se ni ne povus preni alian.

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

Gravas pri ĉi tiu kodo, ke du el kvar filozofoj forgesas demeti sian maldekstran forkon. Kaj rezultas, ke ili manĝas pli da manĝaĵo, kaj aliaj komencas malsati, kvankam la fadenoj havas la saman prioritaton. Ĉi tie ili ne tute malsatas, ĉar... malbonaj filozofoj foje remetas siajn forkojn. Montriĝas, ke la bonaj manĝas ĉirkaŭ 5 fojojn malpli ol la malbonaj. Do eta eraro en la kodo kondukas al malpliigo de rendimento. Ĉi tie ankaŭ indas rimarki, ke malofta situacio eblas, kiam ĉiuj filozofoj prenas la maldekstran forkon, ne ekzistas dekstra, ili submetas la maldekstran, atendas, prenas la maldekstran denove ktp. Ĉi tiu situacio ankaŭ estas malsato, pli kiel reciproka blokado. Mi ne povis ripeti ĝin. Malsupre estas bildo por situacio kie du malbonaj filozofoj prenis ambaŭ forkojn, kaj du bonaj malsatas.

Bone nutritaj filozofoj aŭ konkurenciva programado en .NET

Ĉi tie vi povas vidi, ke fadenoj foje vekiĝas kaj provas akiri rimedon. Du el kvar kernoj faras nenion (verda grafikaĵo supre).

Morto de Filozofo

Nu, unu plia problemo, kiu povas interrompi la gloran vespermanĝon de filozofoj, estas se unu el ili subite mortas kun forkoj en la manoj (kaj li estos enterigita tiel). Tiam la najbaroj restos sen tagmanĝo. Vi mem povas elpensi ekzemplokodon por ĉi tiu kazo, ekzemple ĝi estas forĵetita NullReferenceException post kiam la filozofo prenas la forkojn. Kaj, cetere, la escepto ne estos pritraktata kaj la alvokodo ne simple kaptos ĝin (por ĉi tio AppDomain.CurrentDomain.UnhandledException kaj ktp.). Tial, erartraktiloj estas bezonataj en la fadenoj mem kaj kun gracia finaĵo.

Kelnero

Bone, kiel ni solvas ĉi tiun problemon de blokiĝo, malsato kaj mortoj? Ni permesos nur unu filozofon al la forkoj, kaj ni aldonos reciprokan ekskludon de fadenoj por ĉi tiu loko. Kiel fari ĝin? Supozu, ke apud la filozofoj estas kelnero, kiu donas permeson al unu filozofo preni la forkojn. Kiel ni faru ĉi tiun kelneron kaj kiel filozofoj demandos lin estas interesaj demandoj.

La plej simpla maniero estas por filozofoj simple konstante peti al la kelnero aliron al la forkoj. Tiuj. Nun filozofoj ne atendos forkon apude, sed atendos aŭ demandos la kelneron. Komence ni uzas nur Uzantan Spacon por tio; en ĝi ni ne uzas interrompojn por voki iujn ajn procedurojn el la kerno (pli pri ili sube).

Solvoj de uzantspacaj

Ĉi tie ni faros la samon, kiun ni faris antaŭe per unu forko kaj du filozofoj, ni turniĝos en buklo kaj atendos. Sed nun estos ĉiuj filozofoj kaj kvazaŭ nur unu forko, t.e. ni povas diri, ke manĝos nur la filozofo, kiu prenis ĉi tiun “oran forkon” de la kelnero. Por fari tion ni uzas 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 ĉi tio estas blokilo, kun, proksimume, la sama while(true) { if (!lock) break; }, sed kun eĉ pli "magio" ol en SpinWait (kiu estas uzata tie). Nun li scias kalkuli tiujn atendantajn, endormigi ilin iomete, kaj multe pli. ktp Ĝenerale, ĝi faras ĉion eblan por optimumigi. Sed ni devas memori, ke ĉi tio estas ankoraŭ la sama aktiva buklo, kiu manĝas procesorajn rimedojn kaj konservas fadenon, kiu povas konduki al malsato se unu el la filozofoj fariĝas pli prioritata ol la aliaj, sed ne havas oran forkon (Problemo de Prioritata Inversio). ). Sekve, ni uzas ĝin nur por tre mallongaj ŝanĝoj en komuna memoro, sen iuj alvokoj de triaj, nestitaj seruroj aŭ aliaj surprizoj.

Bone nutritaj filozofoj aŭ konkurenciva programado en .NET

Desegno por SpinLock. Rojoj konstante "batalas" por la ora forko. Malsukcesoj okazas - la reliefigita areo en la figuro. La kernoj ne estas plene uzataj: nur ĉirkaŭ 2/3 per ĉi tiuj kvar fadenoj.

Alia solvo ĉi tie estus nur uzi Interlocked.CompareExchange kun la sama aktiva atendo kiel montrita en la supra kodo (ĉe malsatantaj filozofoj), sed ĉi tio, kiel jam dirite, povus teorie konduki al blokado.

pri Interlocked indas diri, ke ekzistas ne nur CompareExchange, sed ankaŭ aliaj metodoj por atomlegado KAJ skribo. Kaj ripetante la ŝanĝon, se alia fadeno sukcesas fari siajn ŝanĝojn (legu 1, legu 2, skribu 2, skribu 1 estas malbona), ĝi povas esti uzata por kompleksaj ŝanĝoj al unu valoro (Interlocked Anything-ŝablono).

Solvoj de Kernel-reĝimo

Por eviti malŝpari rimedojn en buklo, ni rigardu kiel bloki fadenon. Alivorte, daŭrigante nian ekzemplon, ni vidu, kiel la kelnero dormigas la filozofon kaj vekas lin nur kiam necese. Unue, ni rigardu kiel fari tion per la kerna reĝimo de la operaciumo. Ĉiuj strukturoj tie ofte finas esti pli malrapidaj ol tiuj en uzantspaco. Pli malrapide plurajn fojojn, ekzemple AutoResetEvent eble 53 fojojn pli malrapide SpinLock [Riĉjo]. Sed kun ilia helpo, vi povas sinkronigi procezojn tra la tuta sistemo, administrita aŭ ne.

La baza dezajno ĉi tie estas semaforo, proponita de Dijkstra antaŭ pli ol duonjarcento. Semaforo estas, simple dirite, pozitiva entjero kontrolita de la sistemo, kaj du operacioj sur ĝi - pliigo kaj malpliiĝo. Se ne eblas redukti nulon, tiam la voka fadeno estas blokita. Kiam la nombro estas pliigita per iu alia aktiva fadeno/procezo, tiam la fadenoj estas preterpasitaj kaj la semaforo denove estas malpliigita per la nombro pasita. Vi povas imagi trajnojn en botelkolo kun semaforo. .NET ofertas plurajn konstruaĵojn kun simila funkcieco: AutoResetEvent, ManualResetEvent, Mutex kaj mi mem Semaphore. Ni uzos AutoResetEvent, ĉi tiu estas la plej simpla el ĉi tiuj konstrukcioj: nur du valoroj 0 kaj 1 (malvera, vera). Ŝia metodo WaitOne() blokas la vokan fadenon se la valoro estis 0, kaj se 1, tiam malaltigas ĝin al 0 kaj preterlasas ĝin. Metodo Set() pliiĝas al 1 kaj lasas unu personon trairi, kiu denove malpliiĝas al 0. Agas kiel turnstile en la metroo.

Ni malfaciligu la solvon kaj uzu blokadon por ĉiu filozofo, kaj ne por ĉiuj samtempe. Tiuj. Nun pluraj filozofoj povas manĝi samtempe, kaj ne nur unu. Sed ni denove blokas aliron al la tablo por ĝuste preni forkojn, evitante vetkurajn kondiĉojn.

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

Por kompreni, kio okazas ĉi tie, konsideru la kazon, kiam la filozofo ne sukcesis preni la forkojn, tiam liaj agoj estos kiel sekvas. Li atendas aliron al la tablo. Ricevinte ĝin, li provas preni la forkojn. Ne funkciis. Ĝi fordonas aliron al la tablo (reciproka ekskludo). Kaj li preterpasas sian "turnketon" (AutoResetEvent) (komence ili estas malfermitaj). Ĝi denove falas en la ciklon, ĉar li ne havas forkojn. Li provas preni ilin kaj ĉesas ĉe sia "turnstile". Iu pli bonŝanca najbaro dekstre aŭ maldekstre, finmanĝinte, malblokos nian filozofon "malfermante sian turnikon". Nia filozofo trapasas ĝin (kaj ĝi fermiĝas malantaŭ li) duan fojon. Provas la trian fojon preni la forkojn. Sukcesa. Kaj li trapasas sian turnikon por tagmanĝi.

Kiam estas hazardaj eraroj en tia kodo (ili ĉiam ekzistas), ekzemple, najbaro estos specifita malĝuste aŭ la sama objekto estos kreita AutoResetEvent por ĉiuj (Enumerable.Repeat), tiam la filozofoj atendos la programistojn, ĉar Trovi erarojn en tia kodo estas sufiĉe malfacila tasko. Alia problemo kun ĉi tiu solvo estas, ke ĝi ne garantias, ke iu filozofo ne malsatos.

Hibridaj solvoj

Ni rigardis du alirojn al sinkronigado, kiam ni restas en uzantreĝimo kaj turniĝas en buklo kaj kiam ni blokas la fadenon tra la kerno. La unua metodo estas bona por mallongaj blokoj, la dua por longaj. Ofte vi devas unue atendi mallonge ke variablo ŝanĝiĝos en buklo, kaj poste bloki la fadenon kiam la atendo estas longa. Ĉi tiu aliro estas efektivigita en la tn. hibridaj dezajnoj. Ĝi havas la samajn konstruaĵojn kiel por kerna reĝimo, sed nun kun uzantreĝima buklo: SemaphorSlim, ManualResetEventSlim ktp La plej populara dezajno ĉi tie estas Monitor, ĉar en C# estas konata lock sintakso. Monitor ĉi tiu estas la sama semaforo kun maksimuma valoro de 1 (mutex), sed kun subteno por atendado en buklo, rekursio, Kondiĉa Variebla ŝablono (pli pri tio ĉi sube), ktp. Ni rigardu solvon kun ĝi.

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

Ĉi tie ni denove blokas la tutan tablon de aliro al la forkoj, sed nun ni malblokas ĉiujn fadenojn samtempe, prefere ol najbaroj kiam iu finas manĝi. Tiuj. unue, iu manĝas kaj baras la najbarojn, kaj kiam ĉi tiu iu finas, sed volas denove manĝi tuj, li iras en la blokon kaj vekas siajn najbarojn, ĉar ĝia atendotempo estas malpli granda.

Tiel ni evitas blokiĝon kaj la malsaton de iu filozofo. Ni uzas buklon por atendi mallongan tempon kaj bloki la fadenon dum longa tempo. Malbloki ĉiujn samtempe estas pli malrapida ol se nur la najbaro estus malblokita, kiel en la solvo kun AutoResetEvent, sed la diferenco ne estu granda, ĉar fadenoj devas resti en uzantreĝimo unue.

У lock sintakso havas kelkajn malagrablajn surprizojn. Rekomendita uzi Monitor rekte [Richter] [Eric Lippert]. Unu el ili estas tio lock ĉiam eliras Monitor, eĉ se estis escepto, kaj tiam alia fadeno povas ŝanĝi la staton de la komuna memoro. En tiaj kazoj, estas ofte pli bone iri en blokiĝon aŭ iel sekure ĉesigi la programon. Alia surprizo estas, ke Monitoro uzas horloĝblokojn (SyncBlock), kiuj ĉeestas en ĉiuj objektoj. Sekve, se nekonvena objekto estas elektita, vi povas facile akiri blokiĝon (ekzemple, se vi ŝlosas sur internata ŝnuro). Ni ĉiam uzas kaŝitan objekton por tio.

La Kondiĉa Variebla ŝablono permesas vin pli koncize efektivigi la atendon de iu kompleksa kondiĉo. En .NET ĝi estas nekompleta, laŭ mi, ĉar... En teorio, devus esti pluraj vostoj sur pluraj variabloj (kiel en Posix Threads), kaj ne sur unu seruro. Tiam eblus fari ilin por ĉiuj filozofoj. Sed eĉ en ĉi tiu formo ĝi permesas vin mallongigi la kodon.

Multaj filozofoj aŭ async / await

Bone, nun ni povas efike bloki fadenojn. Sed kio se ni havas multajn filozofojn? 100? 10000? Ekzemple, ni ricevis 100000 4 petojn al la retservilo. Krei fadenon por ĉiu peto estos multekosta, ĉar tiom da fadenoj ne estos ekzekutitaj paralele. Nur tiom da logikaj kernoj estos ekzekutitaj (mi havas XNUMX). Kaj ĉiuj aliaj simple forprenos rimedojn. Unu solvo al ĉi tiu problemo estas la nesinkrona/atenda ŝablono. Ĝia ideo estas, ke funkcio ne tenas fadenon se ĝi bezonas atendi ke io daŭros. Kaj kiam io okazas, ĝi rekomencas sian ekzekuton (sed ne nepre en la sama fadeno!). En nia kazo, ni atendos forkon.

SemaphoreSlim havas por ĉi tio WaitAsync() metodo. Jen efektivigo uzante ĉi tiun ŝablonon.

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

Metodo kun async / await estas tradukita en ruzan finhavan ŝtatmaŝinon, kiu tuj resendas sian internan Task. Per ĝi, vi povas atendi ke la metodo kompletiĝos, nuligi ĝin kaj ĉion alian, kion vi povas fari kun Task. Ene de la metodo, ŝtatmaŝino kontrolas ekzekuton. La fundo estas, ke se ne estas prokrasto, tiam la ekzekuto estas sinkrona, kaj se ekzistas, tiam la fadeno estas liberigita. Por pli bone kompreni ĉi tion, estas pli bone rigardi ĉi tiun ŝtatmaŝinon. Vi povas krei ĉenojn de ĉi tiuj async / await metodoj.

Ni provu ĝin. Verko de 100 filozofoj sur maŝino kun 4 logikaj kernoj, 8 sekundoj. La antaŭa solvo kun Monitor nur ekzekutis la unuajn 4 fadenojn kaj tute ne efektivigis la ceterajn. Ĉiu el ĉi tiuj 4 fadenoj estis neaktiva dum ĉirkaŭ 2 ms. Kaj la async/await solvo faris ĉiujn 100, kun mezumo de 6.8 sekundoj ĉiu atendado. Kompreneble, en realaj sistemoj, esti neakceptebla dum 6 sekundoj estas neakceptebla kaj estas pli bone ne procesi tiom da petoj tiamaniere. La solvo kun Monitoro montriĝis tute ne skalebla.

konkludo

Kiel vi povas vidi el ĉi tiuj malgrandaj ekzemploj, .NET subtenas multajn sinkronigajn konstrukciojn. Tamen, ne ĉiam estas evidente kiel uzi ilin. Mi esperas, ke ĉi tiu artikolo estis helpema. Ni envolvas ĉi tion nuntempe, sed ankoraŭ restas multe da interesaj aferoj, ekzemple, faden-sekuraj kolektoj, TPL-Datumo, Reaktiva programado, Programaro-Transakcia modelo, ktp.

Fontoj

fonto: www.habr.com

Aldoni komenton