Filosofi ben alimentati o Programmazione .NET cumpetitiva

Filosofi ben alimentati o Programmazione .NET cumpetitiva

Videmu cumu si travaglia a prugrammazione cuncurrente è parallela in .Net, utilizendu u Prublemu di Dining Philosophers com'è esempiu. U pianu hè questu, da a sincronizazione di filamenti / prucessi, à u mudellu attore (in i seguenti parti). L'articulu pò esse utile per a prima cunniscenza o per rinfriscà a vostra cunniscenza.

Perchè fà tuttu? Transistors righjunghjini a so taglia minima, a lege di Moore si basa nantu à a limitazione di a vitezza di a luce è per quessa un aumentu hè osservatu in u numeru, più transistor pò esse fattu. À u listessu tempu, a quantità di dati cresce, è l'utilizatori aspettanu una risposta immediata da i sistemi. In una tale situazione, a prugrammazione "normale", quandu avemu un filu esecutivu, ùn hè più efficace. Avete bisognu di risolve in qualchì modu u prublema di l'esekzione simultanea o simultanea. Inoltre, stu prublema esiste in diversi livelli: à u livellu di i filamenti, à u livellu di i prucessi, à u livellu di e macchine in a reta (sistemi distribuiti). .NET hà tecnulugii d'alta qualità, testati in u tempu per risolve rapidamente è efficacemente tali prublemi.

Objettivu

Edsger Dijkstra pusò stu prublema à i so studianti cum'è 1965. A formulazione stabilita hè a siguenti. Ci hè un certu nùmeru (di solitu cinque) di filòsufi è u listessu numeru di furchetti. Si ponenu à una tavola tonda, forche trà elli. I filòsufi ponu manghjà da i so piatti di manghjà senza fine, pensate o aspittà. Per manghjà un filòsufu, avete bisognu di piglià dui forchetti (l'ultimu cumparte a forchetta cù u primu). Piglià è mette una furchetta sò dui azzioni separati. Tutti i filòsufi tacenu. U compitu hè di truvà un algoritmu tali chì tutti pensanu è esse pienu ancu dopu à 54 anni.

Prima, pruvemu di risolve stu prublema per mezu di l'usu di un spaziu spartutu. I forchetti si trovanu nantu à a tavula cumuna è i filòsufi simpricimenti li piglianu quand'elli sò è li rimettenu. Quì ci sò prublemi cù a sincronizazione, quandu esattamente per piglià surebets? è s'ellu ùn ci hè micca forchetta? etc. Ma prima, cuminciamu à i filòsufi.

Per inizià i fili, usemu un pool di fili attraversu Task.Run mètudu:

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

U gruppu di fili hè pensatu per ottimisà a creazione è a eliminazione di fili. Questa piscina hà una fila cù e cumpetenze è u CLR crea o sguassate i fili secondu u numeru di sti travaglii. Un pool per tutti i AppDomains. Sta piscina deve esse usata quasi sempre, perchè. ùn ci hè micca bisognu di creà, sguassate i fili, i so fili, etc. Hè pussibule senza piscina, ma dopu avete aduprà direttamente. Thread, Questu hè utile per i casi quandu avete bisognu di cambià a priorità di un filu, quandu avemu una longa operazione, per un filu di primu pianu, etc.

In altre parolle, System.Threading.Tasks.Task a classa hè a stessa Thread, ma cù ogni tipu di cunvenzioni: a capacità di eseguisce un compitu dopu à un bloccu di altre attività, rinvià da e funzioni, interrompe convenientemente, è più. etc. Sò necessarii per sustene e custruzzioni async / await (Task-based Asynchronous Pattern, zuccaru sintatticu per aspittà per l'operazioni IO). Parleremu di questu dopu.

CancelationTokenSource quì hè necessariu per chì u filu pò finisce à u signale di u filu chjamatu.

Problemi di sincronia

Filosofi bluccati

Va bè, sapemu cumu creà fili, pruvemu à pranzu:

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

Quì avemu prima à pruvà à piglià u forchetta manca, e poi u forchetta dritta, è s'ellu si travaglia fora, tandu manghjemu è rimettimu. Piglià una forchetta hè atomica, i.e. dui fili ùn ponu micca piglià unu à u stessu tempu (incorrettu: u primu leghje chì a forchetta hè libera, u sicondu - ancu, u primu piglia, u sicondu piglia). Per questu Interlocked.CompareExchange, chì deve esse implementatu cù una struzzione di processore (TSL, XCHG), chì chjude un pezzu di memoria per a lettura è a scrittura sequenziale atomica. È SpinWait hè equivalente à a custruzzione while(true) solu cù un pocu "magia" - u filu piglia u processatore (Thread.SpinWait), ma qualchì volta trasferisce u cuntrollu à un altru filu (Thread.Yeild) o si addormenta (Thread.Sleep).

Ma sta suluzione ùn viaghja micca, perchè i flussi prestu (per mè in una seconda) sò bluccati: tutti i filòsufi piglianu a so furchetta manca, ma micca a diritta. L'array forks hà dunque i valori: 1 2 3 4 5.

Filosofi ben alimentati o Programmazione .NET cumpetitiva

In a figura, bluccà i fili (deadlock). Verde - esecuzione, rossu - sincronizazione, grisgiu - u filu dorme. I rombi indicanu l'ora di iniziu di Tasks.

A fame di i filòsufi

Ancu s'ellu ùn hè micca necessariu di pensà soprattuttu assai alimentu, ma a fame face à qualcunu rinunzià a filusufìa. Pruvemu di simule a situazione di fame di filamenti in u nostru prublema. A fame hè quandu un filu hè in esecuzione, ma senza travagliu significativu, in altri palori, questu hè u listessu impastu, solu avà u filu ùn hè micca dorme, ma hè attivamente cercandu qualcosa di manghjà, ma ùn ci hè micca alimentu. Per evità u bloccu frequente, metteremu a forchetta s'ellu ùn pudemu piglià un altru.

// То же что и в 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'impurtante di stu codice hè chì dui di quattru filòsufi si scurdanu di mette a so furchetta manca. È risulta chì manghjanu più cibo, mentri àutri cumincianu à mori di fame, ancu s'è i fili anu a listessa priorità. Quì ùn sò micca cumplettamente affamati, perchè. i filòsufi cattivi rimettanu a so furchetta qualchì volta. Risulta chì e persone boni manghjanu circa 5 volte menu di i cattivi. Allora un picculu errore in u codice porta à una calata di rendiment. Hè vale a pena nutà quì chì una situazione rara hè pussibule quandu tutti i filòsufi piglianu a forchetta di manca, ùn ci hè micca ghjustu, mettenu a manca, aspittà, pigliate dinò a manca, etc. Questa situazione hè ancu una fame, più cum'è un bloccu. Aghju fiascatu à ripetiri. A sottu hè una stampa per una situazione induve dui filòsufi cattivi anu pigliatu tramindui forche è dui boni sò affamati.

Filosofi ben alimentati o Programmazione .NET cumpetitiva

Quì pudete vede chì i fili si sveglianu qualchì volta è pruvate d'avè a risorsa. Dui di i quattru core ùn facenu nunda (graficu verde sopra).

Morte di un filosofu

Ebbè, un altru prublema chì pò interrompe una cena gloriosa di i filòsufi hè se unu di elli mori di colpu cù forchetta in manu (è l'anu intarratu cusì). Allora i vicini seranu lasciati senza cena. Pudete cullà cù un codice esempiu per stu casu sè stessu, per esempiu, hè ghjittatu fora NullReferenceException dopu chì u filòsufu piglia i forchetti. È, per via, l'eccezzioni ùn serà micca trattatu è u codice di chjama ùn hè micca solu catturà (per questu AppDomain.CurrentDomain.UnhandledException è ecc.). Dunque, i gestori d'errore sò necessarii in i fili stessi è cù una fine grazia.

Cambrero

Va bè, cumu risolvemu stu prublema di bloccu, fame è morte? Permettemu solu un filòsufu per ghjunghje à i forchetti, aghjunghje una esclusione mutuale di filamenti per questu locu. Cumu fà? Supposons qu'un serveur se trouve à côté des philosophes, qui donne l'autorisation à un philosophe de prendre les fourchettes. Cumu facemu stu servitore è cumu i filòsufi li dumandanu, e dumande sò interessanti.

A manera più sèmplice hè quandu i filòsufi solu dumandanu constantemente à u servitore l'accessu à i forchetti. Quelli. avà i filòsufi ùn aspittàranu micca una furchetta vicinu, ma aspettanu o dumandanu à u servitore. À u principiu, usemu solu User Space per questu, in questu ùn avemu micca aduprà interruzioni per chjamà qualsiasi prucedure da u kernel (circa quì sottu).

Soluzioni in u spaziu di l'utilizatori

Quì faremu u listessu cum'è no facia cù una forchetta è dui filòsufi, giraremu in un ciculu è aspitteremu. Ma avà seranu tutti i filòsufi è, per esse, solu una forchetta, i.e. si pò dì chì solu u filòsufu chì pigliò sta "forchetta d'oru" da u servitore manghjarà. Per questu avemu aduprà 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 questu hè un blocker, cù, grosso modo, u listessu while(true) { if (!lock) break; }, ma cù ancu più "magia" chè in SpinWait (chì hè aduprata quì). Avà sà cumu cuntà quelli chì aspettanu, mette à dorme un pocu, è più. etc. In generale, faci tuttu u pussibule per ottimisimu. Ma ci vole à ricurdà chì questu hè sempre u stessu ciculu attivu chì manghja i risorsi di u processatore è mantene u flussu, chì pò purtà à a fame se unu di i filòsufi diventa più priurità di l'altri, ma ùn hà micca una furchetta d'oru (prublema di Inversione di Priorità). . Per quessa, avemu aduprà solu per cambiamenti assai assai brevi in ​​memoria spartuta, senza alcunu chiama di terzu partitu, serratura nidificatu, è altre sorprese.

Filosofi ben alimentati o Programmazione .NET cumpetitiva

Disegnu per SpinLock. I flussi sò constantemente "luttanu" per a furchetta d'oru. Ci sò fallimenti - in a figura, l'area scelta. I nuclei ùn sò micca utilizati cumplettamente: solu circa 2/3 da questi quattru fili.

Una altra suluzione quì seria di utilizà solu Interlocked.CompareExchange cù a listessa aspittà attiva cum'è mostra in u codice sopra (in i filòsufi affamati), ma questu, cum'è digià dettu, puderia teoricamente guidà à u bluccatu.

nantu Interlocked Hè da nutà chì ùn ci hè micca solu CompareExchange, ma ancu altri metudi per leghje E scrive atomicu. È attraversu a ripetizione di u cambiamentu, in casu chì un altru filu hà u tempu di fà i so cambiamenti (leghjite 1, leghje 2, scrive 2, scrive 1 hè male), pò esse usatu per cambiamenti cumplessi à un valore unicu (Interlocked Anything pattern).

Soluzioni in Modu Kernel

Per evità di perdi risorse in un ciclu, vedemu cumu pudemu bluccà un filu. In autri paroli, cuntinuendu u nostru esempiu, andemu à vede cumu u servitore mette à dorme u filòsufu è u sveglia solu quandu hè necessariu. Prima, fighjemu cumu fà questu attraversu u modu kernel di u sistema operatore. Tutte e strutture sò spessu più lenti di quelli in u spaziu di l'utilizatori. Parechje volte più lento, per esempiu AutoResetEvent forse 53 volte più lento SpinLock [Richter]. Ma cù u so aiutu, pudete sincronizà i prucessi in tuttu u sistema, gestionatu o micca.

A custruzzione basica quì hè u semaforu prupostu da Dijkstra più di mezu seculu fà. Un semaforu hè, simpliciamente, un entero pusitivu gestitu da u sistema, è duie operazioni nantu à questu, incrementu è decrement. S'ellu ùn falla à diminuisce, cero, allura u filu chjamatu hè bluccatu. Quandu u numeru hè aumentatu da qualchì altru filu / prucessu attivu, allora i filamenti sò saltati è u semaforu hè di novu diminuitu da u numeru passatu. Un pò imaginà i treni in un collu di buttiglia cù un semaforu. .NET offre parechje custruzzioni cù funziunalità simili: AutoResetEvent, ManualResetEvent, Mutex è eiu stessu Semaphore. Avemu aduprà AutoResetEvent, questu hè u più simplice di sti custruzzioni: solu dui valori 0 è 1 (falsi, veru). U so Metudu WaitOne() blucca u filu di chjama se u valore era 0, è se 1, abbassa à 0 è salta. Un metudu Set() aumenta à 1 è lascia passà un servitore, chì torna à calà à 0. Agisce cum'è un turnstile di metro.

Cumplichemu a suluzione è aduprà a serratura per ogni filòsufu, è micca per tutti in una volta. Quelli. avà pò esse parechji filòsufi à tempu, è micca unu. Ma avemu di novu bluccà l'accessu à a tavula per esse currettamente, evitendu e razze (condizioni di razza), piglià 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();
}

Per capisce ciò chì succede quì, cunsiderà u casu quandu u filòsufu hà fiascatu à piglià i forchetti, allora e so azzioni seranu cusì. Hè aspittendu l'accessu à a tavula. Dopu avè ricivutu, prova à piglià i forchetti. Ùn hà micca travagliatu. Dà accessu à a tavula (exclusione mutuale). È passa u so "turnstile" (AutoResetEvent) (sò inizialmente aperti). Si mette in u ciculu di novu, perchè ùn hà micca forchetta. Pruva di piglialli è si ferma à u so "turnstile". Qualchidunu più furtunatu vicinu à a diritta o à a manca, dopu avè finitu di manghjà, sblocca u nostru filòsufu, "apre u so turnstile". U nostru filòsufu u passa (è si chjude daretu) per a seconda volta. Pruva per a terza volta à piglià i forchetti. Bona Furtuna. È passa u so turnstile per cena.

Quandu ci sò errori aleatorii in tali codice (sempre esistenu), per esempiu, un vicinu hè specificatu incorrectamente o u stessu ogettu hè creatu AutoResetEvent per tutti (Enumerable.Repeat), allora i filòsufi aspittàvanu i sviluppatori, perchè Truvà errori in tali codice hè abbastanza difficiule. Un altru prublema cù sta suluzione hè chì ùn guarantisci micca chì un filòsufu ùn morirà di fame.

Soluzioni ibride

Avemu vistu dui approcci à u timing, quandu stemu in u modu d'utilizatore è u ciclu, è quandu avemu bluccatu u filu attraversu u kernel. U primu metudu hè bonu per i chjusi brevi, u sicondu per i longhi. Hè spessu necessariu di prima brevemente aspittà per una variabile per cambià in un ciclu, è poi bluccà u filu quandu l'aspittà hè longa. Stu approcciu hè implementatu in u cusì chjamatu. strutture hibride. Eccu i stessi custruzzioni cum'è per u modu di kernel, ma avà cù un ciclu di modu d'utilizatore: SemaphorSlim, ManualResetEventSlim etc. U disignu più populari quì hè Monitor, perchè in C# ci hè un ben cunnisciutu lock sintassi. Monitor questu hè u stessu semaforu cù un valore massimu di 1 (mutex), ma cù supportu per aspittà in un ciclu, recursion, u mudellu Condition Variable (più nantu à quì sottu), etc. Fighjemu una suluzione cun ella.

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

Quì avemu di novu bluccatu tutta a tavola per l'accessu à i forchetti, ma avà sbloccamu tutti i fili in una volta, è micca vicini quandu qualchissia finisci di manghjà. Quelli. prima, qualcunu manghja è bluccà i vicini, è quandu questu qualchissia finisci, ma vole à manghjà di novu subitu, si mette in bluccatu è sveglia i so vicini, perchè. u so tempu d'attesa hè menu.

Hè cusì chì evitemu i blocchi è a fame di qualchì filòsufu. Utilizemu un ciclu per una corta aspittà è bluccà u filu per un longu. Unblocking tutti à una volta hè più lentu chè s'è solu u vicinu era unblocked, cum'è in a suluzione cù AutoResetEvent, ma a diffarenza ùn deve esse grande, perchè I fili devenu esse prima in modu d'utilizatore.

У lock sintassi hà sorprese brutte. Cunsigliu di utilizà Monitor direttamente [Richter] [Eric Lippert]. Unu di elli hè quellu lock sempre fora Monitor, ancu s'ellu ci era una eccezzioni, in quale casu un altru filu puderia cambià u statu di memoria spartuta. In tali casi, hè spessu megliu per andà in un bloccu di staccazione o finisce in modu sicuru u prugramma. Un'altra sorpresa hè chì Monitor usa blocchi di sincronizazione (SyncBlock), chì sò prisenti in tutti l'uggetti. Per quessa, se un ughjettu inappropriatu hè sceltu, pudete facilmente ottene un bloccu (per esempiu, se chjude nantu à una stringa internata). Utilizemu l'ughjettu sempre oculatu per questu.

U mudellu Condition Variable permette di implementà in modu più cuncisu l'aspettazione di qualchì cundizione cumplessa. In .NET, hè incompleta, in my opinion, perchè in teoria, ci deve esse parechje fila nantu à parechje variàbili (cum'è in Posix Threads), è micca in un lok. Allora si pudia fà per tutti i filòsufi. Ma ancu in questa forma, permette di riduce u codice.

tanti filòsufi o async / await

Va bè, avà pudemu bluccà in modu efficace i fili. Ma chì s'ellu avemu assai filòsufi ? 100 ? 10000 ? Per esempiu, avemu ricevutu 100000 4 dumande à u servitore web. Serà overhead per creà un filu per ogni dumanda, perchè tanti fili ùn correranu in parallelu. Eseguirà solu quanti ci sò nuclei lògichi (aghju XNUMX). È tutti l'altri piglianu solu risorse. Una suluzione à stu prublema hè u mudellu async / await. A so idea hè chì a funzione ùn mantene micca u filu s'ellu ci vole à aspittà chì qualcosa continuà. È quandu faci qualcosa, ripiglia a so esecuzione (ma micca necessariamente nantu à u listessu filu !). In u nostru casu, aspitteremu a furchetta.

SemaphoreSlim hà per questu WaitAsync() metudu. Eccu una implementazione chì usa stu mudellu.

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

Metudu cun async / await hè traduttu in una macchina di statu dilicatu chì torna immediatamente u so internu Task. Attraversu lu, vi ponu aspittà per u cumpletamentu di u metudu, annullà lu, è tuttu ciò chì vi pò fà cù Task. Dentru u metudu, a macchina statale cuntrolla l'esekzione. U fondu hè chì s'ellu ùn ci hè micca ritardu, allura l'esekzione hè sincrona, è s'ellu ci hè, u filu hè liberatu. Per un megliu capiscenu di questu, hè megliu guardà sta macchina statale. Pudete creà catene da queste async / await metudi.

Testemu. U travagliu di 100 filòsufi nantu à una macchina cù 4 nuclei lògichi, 8 seconde. A suluzione precedente cù Monitor hà solu currettu i primi 4 fili è u restu ùn hà micca currettu. Ognunu di sti 4 fili era inattivu per circa 2 ms. È a suluzione async / await curria tutte e 100, cù una aspetta media di 6.8 seconde ognunu. Di sicuru, in i sistemi veri, idle per 6 seconde hè inaccettabile è hè megliu ùn processà tante richieste cum'è questu. A suluzione cù u Monitor hè statu micca scalabile in tuttu.

cunchiusioni

Comu pudete vede da questi picculi esempi, .NET sustene parechje custruzzioni di sincronizazione. Tuttavia, ùn hè micca sempre evidenti cumu si usanu. Spergu chì questu articulu hè statu utile. Per avà, questu hè a fine, ma ci sò sempre assai cose interessanti, per esempiu, cullizzioni di thread-safe, TPL Dataflow, prugrammazione reattiva, mudellu di transazzione software, etc.

Fonti

Source: www.habr.com

Add a comment