Well-Fed Philosophen oder kompetitiv .NET Programméiere

Well-Fed Philosophen oder kompetitiv .NET Programméiere

Loosst d'gesinn wéi concurrent a parallel programméiere Wierker an .Net, mat de Philosophers Dining Problem als Beispill. De Plang ass dëst, vun der Synchroniséierung vun Threads / Prozesser, bis zum Schauspillermodell (an den folgenden Deeler). Den Artikel kann nëtzlech sinn fir déi éischt Bekannten oder fir Äert Wëssen z'erfrëschen.

Firwat et iwwerhaapt maachen? Transistoren erreechen hir Mindestgréisst, dem Moore säi Gesetz läit op d'Begrenzung vun der Liichtgeschwindegkeet an dofir gëtt eng Erhéijung vun der Zuel beobachtet, méi Transistoren kënne gemaach ginn. Zur selwechter Zäit wiisst d'Quantitéit un Daten, an d'Benotzer erwaarden eng direkt Äntwert vun de Systemer. An esou enger Situatioun ass "normal" Programméierung, wa mir een ausféierende Fuedem hunn, net méi effektiv. Dir musst iergendwéi de Problem vun der simultaner oder gläichzäiteger Ausféierung léisen. Ausserdeem existéiert dëse Problem op verschiddenen Niveauen: um Niveau vun de Threads, um Niveau vun de Prozesser, um Niveau vun de Maschinnen am Netz (verdeelt Systemer). .NET huet héichwäerteg, Zäit-getest Technologien fir séier an effizient esou Problemer ze léisen.

Objective

Den Edsger Dijkstra huet senge Schüler dëse Problem esou fréi wéi 1965. Déi etabléiert Formuléierung ass wéi follegt. Et gëtt eng gewëssen (normalerweis fënnef) Zuel vu Philosophen an déiselwecht Zuel vu Gabel. Si sëtze bei engem ronnen Dësch, Gabel tëscht hinnen. D'Philosophe kënnen vun hiren Telleren vun endlos Iessen iessen, denken oder waarden. Fir e Philosoph ze iessen, musst Dir zwee Gabel huelen (déi lescht deelt d'Gabel mat der éischter). Eng Gabel ophuelen an erofsetzen sinn zwou separat Aktiounen. All Philosophe si roueg. D'Aufgab ass esou en Algorithmus ze fannen, datt se all no 54 Joer denken a voll sinn.

Als éischt, loosst eis probéieren dëse Problem duerch d'Benotzung vun engem gemeinsame Raum ze léisen. D'Gabel leien um gemeinsamen Dësch an d'Philosophen huelen se einfach wann se sinn a setzen se zréck. Hei sinn et Problemer mat Synchroniséierung, wann genee surebets ze huelen? wat wann et keng Gabel ass? etc.. Mee fir d'éischt fänke mer d'Philosophen un.

Ze fänken thread, mir benotzen engem thread Pool duerch Task.Run Methode:

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

De Fuedempool ass entwéckelt fir d'Schafung an d'Läschen vun thread ze optimiséieren. Dëse Pool huet eng Schlaang mat Aufgaben an den CLR erstellt oder läscht Threads ofhängeg vun der Unzuel vun dësen Aufgaben. Ee Pool fir all AppDomains. Dëse Pool soll bal ëmmer benotzt ginn, well. keng Noutwendegkeet ze stéieren, Threads ze kreéieren, ze läschen, hir Schlaangen, etc. Et ass méiglech ouni Pool, awer da musst Dir se direkt benotzen Thread, Dëst ass nëtzlech fir Fäll wou Dir d'Prioritéit vun engem Fuedem ännere musst, wa mir eng laang Operatioun hunn, fir e Foreground thread, etc.

An anere Wierder, System.Threading.Tasks.Task Klass ass d'selwecht Thread, awer mat all Zorte vu Komfort: d'Fähigkeit fir eng Aufgab no engem Block vun aneren Aufgaben ze lafen, se vu Funktiounen zréckzebréngen, se bequem ze ënnerbriechen, a méi. etc.. Si sinn néideg async ze ënnerstëtzen / waarden Konstruktiounen (Task-baséiert Asynchronous Muster, syntaktesch Zocker fir IO Operatiounen waarden). Mir schwätzen iwwer dëst spéider.

CancelationTokenSource hei ass et néideg fir datt de Fuedem sech um Signal vum Urufffuedem ophalen kann.

Synchroniséiert Problemer

Blockéiert Philosophen

Okay, mir wësse wéi Dir Threads erstellt, loosst eis probéieren Mëttegiessen ze iessen:

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

Hei probéieren mir als éischt déi lénks Gabel ze huelen, an dann déi riets Gabel, a wann et klappt, da iesse mir a setzen se zréck. Eng Gabel huelen ass atomar, d.h. zwee thread kënnen net gläichzäiteg huelen (falsch: déi éischt liest datt d'Gabel fräi ass, déi zweet - och déi éischt hëlt, déi zweet hëlt). Dofir Interlocked.CompareExchange, déi mat enger Prozessorinstruktioun implementéiert muss ginn (TSL, XCHG), wat e Stéck Erënnerung fir atomar sequenziell Liesen a Schreiwen gespaart. A SpinWait entsprécht dem Konstrukt while(true) nëmme mat enger klenger "Magie" - de Fuedem hëlt de Prozessor (Thread.SpinWait), awer heiansdo iwwerdroen d'Kontroll op en anere Fuedem (Thread.Yeild) oder schléift (Thread.Sleep).

Awer dës Léisung funktionnéiert net, well d'Flëss geschwënn (fir mech bannent enger Sekonn) blockéiert: all Philosophen huelen hir lénks Gabel, awer net déi richteg. D'Gabel-Array huet dann d'Wäerter: 1 2 3 4 5.

Well-Fed Philosophen oder kompetitiv .NET Programméiere

An der Figur blockéieren thread (Deadlock). Gréng - Ausféierung, rout - Synchroniséierung, gro - de Fuedem schléift. D'Rhombusen weisen d'Startzäit vun den Aufgaben un.

Den Honger vun de Philosophen

Obwuel et net néideg ass besonnesch vill Iessen ze denken, awer den Honger mécht jidderengem opginn Philosophie. Loosst d'probéieren d'Situatioun vun Honger vun thread an eisem Problem ze simuléieren. Honger ass wann e Fuedem leeft, awer ouni bedeitend Aarbecht, an anere Wierder, dëst ass deeselwechten Deadlock, nëmmen elo schléift de Fuedem net, awer sicht aktiv no eppes ze iessen, awer et gëtt kee Iessen. Fir heefeg Blockéierungen ze vermeiden, setzen mir d'Gabel zréck wa mir net eng aner kéinte huelen.

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

Déi wichteg Saach iwwer dëse Code ass datt zwee vu véier Philosophe vergiessen hir lénks Gabel erofzesetzen. An et stellt sech eraus datt se méi Iessen iessen, anerer fänken un ze hongereg, obwuel d'Fiedem déiselwecht Prioritéit hunn. Hei sinn se net ganz hongereg, well. schlecht Philosophen setzen hir Gabel heiansdo zréck. Et stellt sech eraus datt gutt Leit ongeféier 5 Mol manner iessen wéi schlecht. Also e klenge Feeler am Code féiert zu engem Réckgang vun der Leeschtung. Et ass och derwäert ze notéieren hei datt eng selten Situatioun méiglech ass, wann all Philosophen déi lénks Gabel huelen, et gëtt kee richtege, se setzen déi lénks, waart, huelt erëm lénks, etc. Dës Situatioun ass och e Honger, méi wéi en Deadlock. Ech hunn et net widderholl. Drënner ass e Bild fir eng Situatioun wou zwee schlecht Philosophe béid Gabel geholl hunn an zwee gutt hongereg sinn.

Well-Fed Philosophen oder kompetitiv .NET Programméiere

Hei kënnt Dir gesinn datt d'Threads heiansdo erwächen a probéieren d'Ressource ze kréien. Zwee vun de véier Käre maachen näischt (gréng Grafik uewen).

Doud vun engem Philosoph

Gutt, en anere Problem deen e glorräichen Dinner vu Philosophen ënnerbrieche kann ass wann ee vun hinnen op eemol mat Gabel an den Hänn stierft (a si wäerten hien esou begruewen). Da bleiwen d'Noperen ouni Mëttegiessen. Dir kënnt mat engem Beispill Code fir dëse Fall selwer kommen an, zum Beispill, ass et erausgehäit NullReferenceException nodeems de Philosoph d'Gabel hëlt. An, iwwregens, d'Ausnam gëtt net gehandhabt an den Uruffcode wäert et net nëmmen fänken (fir dëst AppDomain.CurrentDomain.UnhandledException an etc.). Dofir sinn Fehlerhandterer an de Threads selwer gebraucht a mat graziéisen Ofschloss.

Kelner

Okay, wéi léise mir dësen Deadlock, Honger, an Doud Problem? Mir erlaben nëmmen ee Philosoph d'Gabel z'erreechen, füügt eng géigesäiteg Ausgrenzung vu Threads fir dës Plaz. Wéi maachen et? Ugeholl datt et e Keller nieft de Philosophe gëtt, deen engem Philosoph d'Erlaabnis gëtt d'Gabel ze huelen. Wéi maache mir dëse Kelner a wéi d'Philosophen him stellen, d'Froen sinn interessant.

Deen einfachste Wee ass wann d'Philosophen einfach dauernd de Kelner froen fir Zougang zu de Gabel. Déi. elo wäerten d'Philosophen net op eng Gabel an der Géigend waarden, awer waarden oder froen de Keller. Am Ufank benotze mir nëmmen User Space fir dëst, an et benotze mir keng Ënnerbriechungen fir Prozedure vum Kärel ze ruffen (iwwer hinnen hei ënnen).

Léisungen am Benotzerraum

Hei wäerte mir datselwecht maachen wéi mir mat enger Gabel an zwee Philosophe gemaach hunn, mir wäerten an engem Zyklus spin a waarden. Awer elo wäert et all Philosophe sinn a wéi et war nëmmen eng Gabel, d.h. Et kann gesot ginn, datt nëmmen de Philosoph, deen dës "Golden Gabel" vum Kelner geholl huet, iessen. Fir dës benotze mir 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 dëst ass e Blocker, mat ongeféier d'selwecht while(true) { if (!lock) break; }, awer mat nach méi "Magie" wéi an SpinWait (wat do benotzt gëtt). Elo weess hien, wéi een déi waarden zielt, se e bësse schlofen, a méi. etc.. Am Allgemengen, mécht alles méiglech ze optimiséieren. Awer mir mussen drun erënneren datt dëst nach ëmmer deeselwechten aktiven Zyklus ass deen d'Prozessorressourcen ësst an de Flux hält, wat zu Honger féiere kann wann ee vun de Philosophe méi Prioritéit gëtt wéi anerer, awer keng gëllen Gabel huet (Prioritéit Inversion Problem) . Dofir benotze mir et nëmme fir ganz ganz kuerz Ännerungen am gemeinsame Gedächtnis, ouni Drëtt-Partei-Uriff, nestéiert Spären an aner Iwwerraschungen.

Well-Fed Philosophen oder kompetitiv .NET Programméiere

Zeechnen fir SpinLock. D'Baachen "kämpfen" stänneg fir déi gëllen Gabel. Et gi Feeler - an der Figur, de gewielte Gebitt. D'Käre ginn net voll genotzt: nëmmen ongeféier 2/3 vun dëse véier Threads.

Eng aner Léisung hei wier nëmmen ze benotzen Interlocked.CompareExchange mat der selwechter aktiv wait wéi am Code uewen gewisen (an de hongereg Philosophen), mä dëst, wéi scho gesot, kéint theoretesch zu Blockéierung féieren.

op Interlocked Et soll feststellen, datt et net nëmmen CompareExchange, awer och aner Methoden fir atomarer liesen AN schreiwen. An duerch d'Widderhuelung vun der Ännerung, am Fall wou en anere Fuedem Zäit huet seng Ännerungen ze maachen (liesen 1, liesen 2, schreiwen 2, schreiwen 1 ass schlecht), kann et fir komplex Ännerungen un engem eenzege Wäert benotzt ginn (Interlocked Anything Muster) .

Kernel Mode Léisunge

Fir Ressourcen an enger Loop ze vermeiden, loosst eis kucken wéi mir e Fuedem blockéiere kënnen. An anere Wierder, weider mat eisem Beispill, loosst eis kucken wéi de Kellner de Philosoph schlofe léisst an hien nëmme wa néideg erwächt. Als éischt kucke mer wéi een dat duerch de Kernelmodus vum Betribssystem mécht. All Strukturen do sinn dacks méi lues wéi déi am Benotzer Raum. Puer mol méi lues, zum Beispill AutoResetEvent vläicht 53 mol méi lues SpinLock [Richter]. Awer mat hirer Hëllef kënnt Dir Prozesser am ganze System synchroniséieren, verwaltet oder net.

D'Basiskonstruktioun hei ass de Semaphore vum Dijkstra viru méi wéi engem halleft Joerhonnert proposéiert. Eng Semaphor ass, einfach gesot, e positiven ganzt Zuel, dee vum System geréiert gëtt, an zwou Operatiounen op et, eropgoen an erofsetzen. Wann et net geet erof, null, da gëtt den Uruff thread blockéiert. Wann d'Zuel vun engem aneren aktive Fuedem / Prozess eropgeet, da ginn d'Threads iwwersprangen an d'Semaphore gëtt erëm erofgesat duerch d'Zuel déi passéiert ass. Et kann ee sech Zich an engem Flaschenhals mat enger Semaphor virstellen. .NET bitt verschidde Konstrukter mat ähnlechen Funktionalitéit: AutoResetEvent, ManualResetEvent, Mutex an ech selwer Semaphore. Mir wäerten benotzen AutoResetEvent, dëst ass déi einfachst vun dëse Konstruktiounen: nëmmen zwee Wäerter 0 an 1 (falsch, richteg). Hir Method WaitOne() blockéiert den Uruff thread wann de Wäert 0 war, a wann 1, senkt et op 0 a spréngt et. Eng Method Set() erhéicht op 1 a léisst ee Kelner duerch, deen nees op 0 senkt. Wierkt wéi e Subway Turnstile.

Loosst eis d'Léisung komplizéiere a benotzen d'Schloss fir all Philosoph, an net fir all op eemol. Déi. elo kënnen et e puer Philosophe gläichzäiteg sinn, an net een. Mä mir Spär nees Zougang zu den Dësch fir richteg, Rennen vermeiden (Course Konditiounen), huelen surebets.

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

Fir ze verstoen wat hei geschitt ass, betruecht de Fall wann de Philosoph d'Gabel net geholl huet, da wäert seng Handlungen wéi follegt sinn. Hie waart op den Zougang zum Dësch. Nodeems hien et kritt huet, probéiert hien d'Gabel ze huelen. Huet net geklappt. Et gëtt Zougang zu den Dësch (géigesäitege Ausgrenzung). A passéiert säi "Turnstile" (AutoResetEvent) (si sinn am Ufank op). Et geet erëm an den Zyklus, well hien huet keng Gabel. Hie probéiert se ze huelen an hält bei sengem "Turnstile" op. E puer méi gléckleche Noper op der rietser oder lénkser Säit, nodeems se fäerdeg giess hunn, spärt eise Philosoph op, "säi Turnstile opmaachen." Eise Philosoph passéiert et (an et schléisst hannendrun) fir d'zweete Kéier. Hie probéiert fir d'drëtte Kéier d'Gabel ze huelen. Vill Gléck. An hie passéiert säi Turnstil fir ze iessen.

Wann et zoufälleg Feeler an esou Code sinn (se existéieren ëmmer), zum Beispill, ass en Noper falsch spezifizéiert oder dee selwechten Objet gëtt erstallt AutoResetEvent fir jiddereen (Enumerable.Repeat), da waarden d'Philosophen op d'Entwéckler, well Feeler an esou Code ze fannen ass eng zimlech schwéier Aufgab. En anere Problem mat dëser Léisung ass datt et net garantéiert datt e Philosoph net hongereg gëtt.

Hybrid Léisungen

Mir hunn zwou Approche fir Timing gekuckt, wa mir am Benotzermodus a Loop bleiwen, a wa mir de Fuedem duerch de Kernel blockéieren. Déi éischt Method ass gutt fir kuerz Schleisen, déi zweet fir laang. Et ass dacks néideg fir d'éischt kuerz op eng Variabel ze waarden fir an enger Loop z'änneren, an dann de Fuedem ze blockéieren wann d'Waarde laang ass. Dës Approche gëtt am sougenannte. Hybrid Strukturen. Hei sinn déiselwecht Konstruktioune wéi fir Kernel Modus, awer elo mat enger Benotzermodus Loop: SemaphorSlim, ManualResetEventSlim asw De beléifsten Design hei ass Monitor, well am C # gëtt et eng gutt-bekannt lock syntax. Monitor dat ass déi selwecht Semaphore mat engem Maximum Wäert vun 1 (mutex), mee mat Ënnerstëtzung fir waarden an enger Loop, Rekursioun, der Conditioun Variabel Muster (méi op dat ënnert), etc. Loosst eis eng Léisung mat et kucken.

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

Hei blockéiere mir erëm de ganzen Dësch fir Zougang zu de Gabel, awer elo späre mir all Threads op eemol, an net Noperen, wann een fäerdeg ass ze iessen. Déi. éischtens, een ësst a blockéiert d'Noperen, a wann dee fäerdeg ass, awer direkt erëm iessen wëll, geet en an d'Blockéierung an erwächt seng Noperen, well. seng Waardezäit ass manner.

Dëst ass wéi mir Doudesfäll an d'Hungerung vun engem Philosoph vermeiden. Mir benotzen eng Loop fir eng kuerz Waarde a blockéieren de Fuedem fir eng laang. Jiddereen op eemol opzemaachen ass méi lues wéi wann nëmmen de Noper opgehuewe wier, wéi an der Léisung mat AutoResetEvent, mä den Ënnerscheed soll net grouss ginn, well Threads musse fir d'éischt am Benotzermodus bleiwen.

У lock Syntax huet béis Iwwerraschungen. Recommandéiert ze benotzen Monitor direkt [Richter] [Eric Lippert]. Ee vun hinnen ass dat lock ëmmer aus Monitor, och wann et eng Ausnam war, an deem Fall kéint en anere Fuedem de gemeinsame Gedächtniszoustand änneren. An esou Fäll ass et dacks besser an d'Deadlock ze goen oder iergendwéi sécher de Programm ofzeschléissen. Eng aner Iwwerraschung ass datt Monitor Synchroniséierungsblocken benotzt (SyncBlock), déi an all Objete präsent sinn. Dofir, wann en onpassend Objet ausgewielt gëtt, kënnt Dir einfach en Deadlock kréien (zum Beispill wann Dir op eng intern String gespaart sidd). Mir benotzen der ëmmer verstoppt Objet fir dës.

D'Condition Variable Muster erlaabt Iech d'Erwaardung vun enger komplexer Konditioun méi präzis ëmzesetzen. Am .NET ass et onkomplett, menger Meenung no, well an Theorie, et soll e puer Schlaangen op e puer Verännerlechen ginn (wéi an Posix Threads), an net op engem lok. Da kéint een se fir all Philosophe maachen. Awer och an dëser Form erlaabt Iech de Code ze reduzéieren.

vill Philosophen oder async / await

Okay, elo kënne mir Themen effektiv blockéieren. Awer wat wa mir vill Philosophen hunn? 100? 10000? Zum Beispill hu mir 100000 Ufroen un de Webserver kritt. Et wäert iwwerhead sinn e Fuedem fir all Ufro ze kreéieren, well sou vill thread wäert net parallel lafen. Wäert nëmme sou vill lafen wéi et logesch Käre sinn (ech hunn 4). An all déi aner wäerten just Ressourcen ewechhuelen. Eng Léisung fir dëse Problem ass d'Async / Waart Muster. Seng Iddi ass datt d'Funktioun net de Fuedem hält wann et muss waarden bis eppes weider geet. A wann et eppes mécht, setzt se hir Ausféierung erëm (awer net onbedéngt um selwechte Fuedem!). An eisem Fall wäerte mir op d'Gabel waarden.

SemaphoreSlim huet fir dës WaitAsync() Method. Hei ass eng Implementatioun mat dësem Muster.

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

Method mat async / await gëtt an eng komplizéiert Staatsmaschinn iwwersat, déi direkt seng intern zréckkënnt Task. Duerch et kënnt Dir op d'Réalisatioun vun der Method waarden, se annuléieren, an alles wat Dir mat Task maache kënnt. Bannen an der Method kontrolléiert d'Staatsmaschinn d'Ausféierung. Déi ënnescht Linn ass datt wann et keng Verspéidung ass, ass d'Ausféierung synchron, a wann et ass, da gëtt de Fuedem verëffentlecht. Fir e bessere Verständnis vun dëser, ass et besser e Bléck op dëser Staat Maschinn. Dir kënnt Ketten aus dëse schafen async / await Methoden.

Loosst eis testen. Aarbecht vun 100 Philosophen op enger Maschinn mat 4 logesche Kär, 8 Sekonnen. Déi viregt Léisung mam Monitor huet nëmmen déi éischt 4 Threads gelaf an de Rescht huet guer net gelaf. Jiddereng vun dësen 4 Threads war fir ongeféier 2ms idle. An d'Async / Erwaart Léisung huet all 100 gelaf, mat enger Moyenne vun 6.8 Sekonnen all. Natierlech, a richtege Systemer, Idle fir 6 Sekonnen ass inakzeptabel an et ass besser net sou vill Ufroe wéi dëst ze veraarbecht. D'Léisung mam Monitor huet sech als guer net skalierbar erausgestallt.

Konklusioun

Wéi Dir aus dëse klenge Beispiller kënnt gesinn, ënnerstëtzt .NET vill Synchroniséierungskonstruktiounen. Wéi och ëmmer, et ass net ëmmer kloer wéi se se benotzen. Ech hoffen dësen Artikel war hëllefräich. Fir de Moment ass dëst d'Enn, awer et sinn nach vill interessant Saachen lénks, zum Beispill thread-sécher Sammlungen, TPL Dataflow, Reaktiv Programméierung, Software Transaktiounsmodell, etc.

Quellen vun Informatiounen

Source: will.com

Setzt e Commentaire