Filòsofs ben alimentats o programació .NET competitiva

Filòsofs ben alimentats o programació .NET competitiva

Vegem com funciona la programació simultània i paral·lela a .Net, utilitzant com a exemple el problema del menjador de Philosophers. El pla és aquest, des de la sincronització de fils / processos, fins al model d'actor (en les parts següents). L'article pot ser útil per al primer conegut o per refrescar els vostres coneixements.

Per què fer-ho en absolut? Els transistors arriben a la seva mida mínima, la llei de Moore es basa en la limitació de la velocitat de la llum i per tant s'observa un augment del nombre, es poden fer més transistors. Al mateix temps, la quantitat de dades creix i els usuaris esperen una resposta immediata dels sistemes. En aquesta situació, la programació "normal", quan tenim un fil en execució, ja no és efectiva. Heu de resoldre d'alguna manera el problema de l'execució simultània o concurrent. A més, aquest problema existeix a diferents nivells: a nivell de fils, a nivell de processos, a nivell de màquines a la xarxa (sistemes distribuïts). .NET disposa de tecnologies d'alta qualitat i provades en el temps per resoldre aquests problemes de manera ràpida i eficient.

Tasca

Edsger Dijkstra va plantejar aquest problema als seus estudiants ja l'any 1965. La formulació establerta és la següent. Hi ha un cert nombre (normalment cinc) de filòsofs i el mateix nombre de forquilles. S'asseuen a una taula rodona, amb forquilles entre ells. Els filòsofs poden menjar dels seus plats de menjar interminable, pensar o esperar. Per menjar-se un filòsof cal agafar dues forquilles (l'últim comparteix la forquilla amb la primera). Recollir i deixar una forquilla són dues accions diferents. Tots els filòsofs callen. La tasca és trobar un algorisme tal que tots pensin i estiguin plens fins i tot després de 54 anys.

Primer, intentem resoldre aquest problema mitjançant l'ús d'un espai compartit. Les forquilles es troben a la taula comuna i els filòsofs simplement les prenen quan estan i les tornen a posar. Aquí hi ha problemes amb la sincronització, quan exactament fer les apostes segures? i si no hi ha forquilla? etc. Però primer, comencem els filòsofs.

Per iniciar fils, fem servir un grup de fils a través Task.Run mètode:

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

El grup de fils està dissenyat per optimitzar la creació i la supressió de fils. Aquest grup té una cua amb tasques i el CLR crea o elimina fils en funció del nombre d'aquestes tasques. Un grup per a tots els AppDomains. Aquesta piscina s'ha d'utilitzar gairebé sempre, perquè. no cal preocupar-se de crear, esborrar fils, les seves cues, etc. És possible sense un grup, però després l'has d'utilitzar directament Thread, això és útil per als casos en què cal canviar la prioritat d'un fil, quan tenim una operació llarga, per a un fil de primer pla, etc.

En altres paraules, System.Threading.Tasks.Task la classe és la mateixa Thread, però amb tota mena de comoditats: la possibilitat d'executar una tasca després d'un bloc d'altres tasques, retornar-les de funcions, interrompre-les convenientment i molt més. etc. Són necessaris per donar suport a les construccions asíncrones/await (Patró asíncron basat en tasques, sucre sintàctic per a l'espera d'operacions d'IO). D'això en parlarem més endavant.

CancelationTokenSource aquí es necessita perquè el fil pugui acabar amb el senyal del fil que crida.

Problemes de sincronització

Filòsofs bloquejats

D'acord, sabem com crear fils, provem de dinar:

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

Aquí primer intentem agafar la bifurcació de l'esquerra, i després la de la dreta, i si surt bé, aleshores les mengem i les tornem a posar. Prendre una forquilla és atòmic, és a dir. dos fils no poden agafar un alhora (incorrecte: el primer diu que la forquilla està lliure, el segon també, el primer agafa, el segon pren). Per això Interlocked.CompareExchange, que s'ha d'implementar amb una instrucció del processador (TSL, XCHG), que bloqueja un tros de memòria per a la lectura i l'escriptura seqüencials atòmiques. I SpinWait és equivalent a la construcció while(true) només amb una mica de "màgia": el fil agafa el processador (Thread.SpinWait), però de vegades transfereix el control a un altre fil (Thread.Yeild) o s'adorm (Thread.Sleep).

Però aquesta solució no funciona, perquè els fluxos aviat (per a mi en un segon) estan bloquejats: tots els filòsofs prenen la seva bifurcació esquerra, però no la dreta. Aleshores, la matriu de forks té els valors: 1 2 3 4 5.

Filòsofs ben alimentats o programació .NET competitiva

A la figura, fils de bloqueig (bloqueig). Verd - execució, vermell - sincronització, gris - el fil està dormint. Els rombes indiquen l'hora d'inici de les Tasques.

La fam dels filòsofs

Encara que no cal pensar especialment en menjar, però la fam fa que qualsevol renunciï a la filosofia. Intentem simular la situació d'inanició de fils en el nostre problema. La fam és quan s'executa un fil, però sense treball important, és a dir, aquest és el mateix punt mort, només que ara el fil no dorm, sinó que busca activament alguna cosa per menjar, però no hi ha menjar. Per tal d'evitar bloquejos freqüents, tornarem a posar la bifurcació si no en podríem agafar una altra.

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

L'important d'aquest codi és que dos de cada quatre filòsofs s'obliden de deixar la forquilla esquerra. I resulta que mengen més menjar, mentre que altres comencen a morir de gana, encara que els fils tenen la mateixa prioritat. Aquí no es moren de gana del tot, perquè. els mals filòsofs tornen les forquilles de vegades. Resulta que les persones bones mengen unes 5 vegades menys que les dolentes. Per tant, un petit error al codi provoca una caiguda del rendiment. També val la pena assenyalar aquí que una situació rara és possible quan tots els filòsofs prenen la bifurcació de l'esquerra, no n'hi ha de dreta, posen l'esquerra, esperen, tornen a agafar l'esquerra, etc. Aquesta situació també és una inanició, més aviat un bloqueig. No he pogut repetir-ho. A continuació es mostra una imatge d'una situació en què dos filòsofs dolents han agafat les dues forquilles i dos de bons es moren de gana.

Filòsofs ben alimentats o programació .NET competitiva

Aquí podeu veure que els fils es desperten de vegades i intenten obtenir el recurs. Dos dels quatre nuclis no fan res (gràfic verd a dalt).

Mort d'un filòsof

Bé, un altre problema que pot interrompre un sopar gloriós de filòsofs és si un d'ells mor de cop amb una forquilla a les mans (i així l'enterraran). Aleshores els veïns es quedaran sense dinar. Podeu crear un codi d'exemple per a aquest cas vosaltres mateixos, per exemple, es llença NullReferenceException després que el filòsof prengui les forquilles. I, per cert, l'excepció no es gestionarà i el codi de trucada no només l'agafarà (per això AppDomain.CurrentDomain.UnhandledException i etc.). Per tant, es necessiten gestors d'errors en els mateixos fils i amb una terminació elegant.

El cambrer

D'acord, com resolem aquest problema de bloqueig, fam i mort? Permetrem que només un filòsof arribi a les bifurcacions, afegirem una exclusió mútua de fils per a aquest lloc. Com fer-ho? Suposem que hi ha un cambrer al costat dels filòsofs, que dóna permís a qualsevol filòsof per agafar les forquilles. Com fem aquest cambrer i com li faran els filòsofs, les preguntes són interessants.

La manera més senzilla és quan els filòsofs simplement demanaran constantment al cambrer l'accés a les forquilles. Aquells. ara els filòsofs no esperaran una forquilla a prop, sinó que esperen o preguntaran al cambrer. Al principi, utilitzem només l'espai d'usuari per a això, en ell no fem servir interrupcions per cridar cap procediment des del nucli (sobre ells a continuació).

Solucions a l'espai d'usuari

Aquí farem el mateix que fèiem abans amb una forquilla i dos filòsofs, girarem en cicle i esperarem. Però ara seran tots filòsofs i, per dir-ho, només una forquilla, és a dir. es pot dir que només menjarà el filòsof que va treure aquesta “forquilla daurada” al cambrer. Per a això fem servir 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 això és un bloquejador, amb, aproximadament, el mateix while(true) { if (!lock) break; }, però amb més "màgia" encara que en SpinWait (que s'utilitza allà). Ara sap comptar els que esperen, posar-los a dormir una mica i més. etc. En general, fa tot el possible per optimitzar. Però hem de recordar que aquest segueix sent el mateix cicle actiu que consumeix els recursos del processador i manté el flux, cosa que pot portar a la fam si un dels filòsofs esdevé més prioritari que els altres, però no té una forquilla daurada (problema d'inversió de prioritat) . Per tant, només l'utilitzem per a canvis molt breus a la memòria compartida, sense cap trucada de tercers, bloquejos imbricats i altres sorpreses.

Filòsofs ben alimentats o programació .NET competitiva

Dibuix per SpinLock. Els rierols estan constantment "lluitant" per la forquilla daurada. Hi ha errors: a la figura, l'àrea seleccionada. Els nuclis no s'utilitzen completament: només uns 2/3 d'aquests quatre fils.

Una altra solució aquí seria utilitzar només Interlocked.CompareExchange amb la mateixa espera activa que es mostra al codi anterior (en els filòsofs famolencs), però això, com ja s'ha dit, teòricament podria conduir al bloqueig.

Про Interlocked Cal tenir en compte que no només n'hi ha CompareExchange, però també altres mètodes de lectura i escriptura atòmiques. I mitjançant la repetició de canvis, en cas que un altre fil tingui temps de fer els seus canvis (llegir 1, llegir 2, escriure 2, escriure 1 és dolent), es pot utilitzar per a canvis complexos a un sol valor (patró Interlocked Anything).

Solucions en mode nucli

Per evitar malgastar recursos en un bucle, vegem com podem bloquejar un fil. És a dir, seguint el nostre exemple, vegem com el cambrer adorm el filòsof i el desperta només quan cal. Primer, mirem com fer-ho mitjançant el mode del nucli del sistema operatiu. Totes les estructures allà són sovint més lentes que les de l'espai d'usuari. Diverses vegades més lent, per exemple AutoResetEvent potser 53 vegades més lent SpinLock [Richter]. Però amb la seva ajuda, podeu sincronitzar processos a tot el sistema, gestionats o no.

La construcció bàsica aquí és el semàfor proposat per Dijkstra fa més de mig segle. Un semàfor és, simplement, un nombre enter positiu gestionat pel sistema i dues operacions sobre ell, incrementar i disminuir. Si no disminueix, zero, llavors el fil cridant es bloqueja. Quan el nombre s'incrementa per algun altre fil/procés actiu, els fils es salten i el semàfor es torna a disminuir pel nombre passat. Un pot imaginar trens en un coll d'ampolla amb un semàfor. .NET ofereix diverses construccions amb una funcionalitat similar: AutoResetEvent, ManualResetEvent, Mutex i jo mateix Semaphore. Farem servir AutoResetEvent, aquesta és la més senzilla d'aquestes construccions: només dos valors 0 i 1 (fals, vertader). El seu mètode WaitOne() bloqueja el fil de crida si el valor era 0 i, si és 1, el redueix a 0 i l'omet. Un mètode Set() puja a 1 i deixa passar un cambrer, que torna a baixar a 0. Actua com un torniquet de metro.

Compliquem la solució i utilitzem el pany per a cada filòsof, i no per a tots alhora. Aquells. ara hi pot haver diversos filòsofs alhora, i no un. Però tornem a bloquejar l'accés a la taula per tal de fer correctament, evitant curses (condicions de cursa), apostes segures.

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

Per entendre què està passant aquí, considereu el cas en què el filòsof no va prendre les forquilles, llavors les seves accions seran les següents. Està esperant l'accés a la taula. Havent-ho rebut, intenta agafar les bifurcacions. No va funcionar. Dóna accés a la taula (exclusió mútua). I passa el seu "torniquet" (AutoResetEvent) (inicialment estan oberts). Torna a entrar al cicle, perquè no té forquilles. Intenta agafar-los i s'atura al seu "torniquet". Algun veí més afortunat de dreta o esquerra, acabat de menjar, desbloqueja el nostre filòsof, "obrint el seu torniquet". El nostre filòsof la passa (i es tanca darrere) per segona vegada. Intenta per tercera vegada agafar les bifurcacions. Bona sort. I passa el seu torniquet per sopar.

Quan hi ha errors aleatoris en aquest codi (sempre existeixen), per exemple, s'especifica incorrectament un veí o es crea el mateix objecte AutoResetEvent per a tot (Enumerable.Repeat), llavors els filòsofs estaran esperant els desenvolupadors, perquè Trobar errors en aquest codi és una tasca bastant difícil. Un altre problema d'aquesta solució és que no garanteix que algun filòsof no passi gana.

Solucions híbrides

Hem analitzat dos enfocaments de la temporització, quan ens quedem en mode d'usuari i en bucle, i quan bloquegem el fil a través del nucli. El primer mètode és bo per a panys curts, el segon per a llargs. Sovint cal esperar breument que una variable canviï en un bucle i després bloquejar el fil quan l'espera sigui llarga. Aquest enfocament s'implementa en l'anomenat. estructures híbrides. Aquí hi ha les mateixes construccions que per al mode del nucli, però ara amb un bucle de mode d'usuari: SemaphorSlim, ManualResetEventSlim etc. El disseny més popular aquí és Monitor, perquè en C# hi ha un conegut lock sintaxi. Monitor aquest és el mateix semàfor amb un valor màxim d'1 (mutex), però amb suport per a l'espera en un bucle, recursivitat, el patró de la variable de condició (més informació a continuació), etc. Vegem-hi una solució.

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

Aquí tornem a bloquejar tota la taula per accedir a les forquilles, però ara desbloquegem tots els fils alhora, i no els veïns quan algú acaba de menjar. Aquells. primer, algú menja i bloqueja els veïns, i quan aquest algú acaba, però vol tornar a menjar de seguida, entra a bloquejar i desperta els seus veïns, perquè. el seu temps d'espera és menor.

Així és com evitem els bloquejos i la fam d'algun filòsof. Utilitzem un bucle per a una espera curta i bloquegem el fil durant una llarga. Desbloquejar tots alhora és més lent que si només es desbloquegés el veí, com en la solució amb AutoResetEvent, però la diferència no ha de ser gran, perquè primer els fils han de romandre en mode d'usuari.

У lock la sintaxi té sorpreses desagradables. Recomanem utilitzar Monitor directament [Richter] [Eric Lippert]. Un d'ells és això lock sempre fora Monitor, fins i tot si hi hagués una excepció, en aquest cas un altre fil podria canviar l'estat de la memòria compartida. En aquests casos, sovint és millor anar a un punt mort o, d'alguna manera, finalitzar el programa amb seguretat. Una altra sorpresa és que Monitor utilitza blocs de sincronització (SyncBlock), que estan presents en tots els objectes. Per tant, si es selecciona un objecte inadequat, podeu obtenir fàcilment un bloqueig (per exemple, si bloquegeu una cadena internada). Utilitzem l'objecte sempre ocult per a això.

El patró de variable de condició us permet implementar de manera més concisa l'expectativa d'alguna condició complexa. En .NET, és incomplet, al meu entendre, perquè en teoria, hi hauria d'haver diverses cues en diverses variables (com en Posix Threads), i no en un sol lok. Aleshores es podria fer per a tots els filòsofs. Però fins i tot en aquesta forma, us permet reduir el codi.

molts filòsofs o async / await

D'acord, ara podem bloquejar eficaçment els fils. Però, i si tenim molts filòsofs? 100? 10000? Per exemple, hem rebut 100000 sol·licituds al servidor web. Serà una sobrecàrrega crear un fil per a cada sol·licitud, perquè tants fils no funcionaran en paral·lel. Només s'executarà tants com hi hagi nuclis lògics (en tinc 4). I tots els altres només emportaran recursos. Una solució a aquest problema és el patró async/wait. La seva idea és que la funció no conté el fil si ha d'esperar que alguna cosa continuï. I quan fa alguna cosa, reprèn la seva execució (però no necessàriament en el mateix fil!). En el nostre cas, esperarem la bifurcació.

SemaphoreSlim té per això WaitAsync() mètode. Aquí hi ha una implementació que utilitza aquest patró.

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

Mètode amb async / await es tradueix a una màquina d'estat complicada que retorna immediatament el seu intern Task. A través d'ell, podeu esperar que finalitzi el mètode, cancel·lar-lo i tot el que podeu fer amb Task. Dins del mètode, la màquina d'estat controla l'execució. La conclusió és que si no hi ha retard, l'execució és sincrònica i, si n'hi ha, s'allibera el fil. Per a una millor comprensió d'això, és millor mirar aquesta màquina d'estats. Podeu crear cadenes a partir d'aquests async / await mètodes.

Anem a provar. Treball de 100 filòsofs en una màquina amb 4 nuclis lògics, 8 segons. La solució anterior amb Monitor només va executar els primers 4 fils i la resta no es va executar en absolut. Cadascun d'aquests 4 fils va estar inactiu durant uns 2 ms. I la solució async/wait va executar les 100, amb una espera mitjana de 6.8 segons cadascuna. Per descomptat, en sistemes reals, inactiu durant 6 segons és inacceptable i és millor no processar tantes sol·licituds com aquesta. La solució amb Monitor va resultar que no era gens escalable.

Conclusió

Com podeu veure en aquests petits exemples, .NET admet moltes construccions de sincronització. Tanmateix, no sempre és obvi com utilitzar-los. Espero que aquest article hagi estat útil. De moment, aquest és el final, però encara queden moltes coses interessants, per exemple, col·leccions de thread-safe, TPL Dataflow, programació reactiva, model de transacció de programari, etc.

Fonts

Font: www.habr.com

Afegeix comentari