Filósofos ben alimentados ou programación .NET competitiva

Filósofos ben alimentados ou programación .NET competitiva

Vexamos como funciona a programación simultánea e paralela en .Net, usando como exemplo o problema de comedor de Philosophers. O plan é este, desde a sincronización de fíos/procesos, ata o modelo de actor (nas seguintes partes). O artigo pode ser útil para o primeiro coñecido ou para refrescar os seus coñecementos.

Por que facelo? Os transistores alcanzan o seu tamaño mínimo, a lei de Moore descansa na limitación da velocidade da luz e polo tanto obsérvase un aumento do número, pódense facer máis transistores. Ao mesmo tempo, a cantidade de datos está crecendo e os usuarios esperan unha resposta inmediata dos sistemas. En tal situación, a programación "normal", cando temos un fío de execución, xa non é efectiva. Debe resolver dalgún xeito o problema da execución simultánea ou simultánea. Ademais, este problema existe a diferentes niveis: a nivel de fíos, a nivel de procesos, a nivel de máquinas na rede (sistemas distribuídos). .NET dispón de tecnoloxías de alta calidade e probadas no tempo para resolver estes problemas de forma rápida e eficiente.

Tarefa

Edsger Dijkstra expúxolles este problema aos seus estudantes xa en 1965. A formulación establecida é a seguinte. Hai un certo número (xeralmente cinco) de filósofos e o mesmo número de garfos. Séntanse nunha mesa redonda, con garfos entre eles. Os filósofos poden comer dos seus pratos de comida sen fin, pensar ou esperar. Para comer un filósofo hai que coller dous garfos (o último comparte o garfo co primeiro). Coller e deixar un garfo son dúas accións separadas. Todos os filósofos calan. A tarefa é atopar un algoritmo tal que todos pensen e estean completos mesmo despois de 54 anos.

En primeiro lugar, intentemos resolver este problema mediante o uso dun espazo compartido. Os garfos atópanse sobre a mesa común e os filósofos simplemente cóllenos cando están e volven colocalos. Aquí hai problemas coa sincronización, cando exactamente facer apostas seguras? e se non hai garfo? etc. Pero primeiro, imos comezar os filósofos.

Para iniciar fíos, usamos un grupo de fíos Task.Run método:

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

O grupo de fíos está deseñado para optimizar a creación e eliminación de fíos. Este grupo ten unha cola con tarefas e o CLR crea ou elimina fíos dependendo do número destas tarefas. Un grupo para todos os AppDomains. Esta piscina debería usarse case sempre, porque. non hai que preocuparse en crear, eliminar fíos, as súas filas, etc. É posible sen un pool, pero entón tes que usalo directamente Thread, isto é útil para os casos nos que se precisa cambiar a prioridade dun fío, cando temos unha operación longa, para un fío en primeiro plano, etc.

Noutras palabras, System.Threading.Tasks.Task clase é a mesma Thread, pero con todo tipo de comodidades: a capacidade de executar unha tarefa despois dun bloque doutras tarefas, devolvelas desde funcións, interrompelas convenientemente e moito máis. etc. Son necesarios para soportar construcións asincrónicas/await (Patrón asincrónico baseado en tarefas, azucre sintáctico para esperar as operacións de E/S). Disto falaremos máis adiante.

CancelationTokenSource aquí é necesario para que o fío poida rematar por si mesmo ao sinal do fío de chamada.

Problemas de sincronización

Filósofos bloqueados

Vale, sabemos como crear fíos, imos tentar xantar:

// Кто какие вилки взял. К примеру: 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í intentamos coller primeiro o garfo esquerdo, e despois o da dereita, e se funciona, despois comemos e poñémolos de novo. Coller un garfo é atómico, é dicir. dous fíos non poden tomar un ao mesmo tempo (incorrecto: o primeiro di que o garfo está libre, o segundo tamén, o primeiro toma, o segundo toma). Para isto Interlocked.CompareExchange, que debe implementarse cunha instrución do procesador (TSL, XCHG), que bloquea un anaco de memoria para a lectura e escritura secuenciais atómicas. E SpinWait é equivalente á construción while(true) só cun pouco de "maxia": o fío leva o procesador (Thread.SpinWait), pero ás veces transfire o control a outro fío (Thread.Yeild) ou queda durmido (Thread.Sleep).

Pero esta solución non funciona, porque os fluxos en breve (para min dentro dun segundo) están bloqueados: todos os filósofos collen o seu garfo esquerdo, pero non o dereito. A matriz de forks ten entón os valores: 1 2 3 4 5.

Filósofos ben alimentados ou programación .NET competitiva

Na figura, fíos de bloqueo (deadlock). Verde - execución, vermello - sincronización, gris - o fío está durmindo. Os rombos indican a hora de inicio das Tarefas.

A fame dos filósofos

Aínda que non hai que pensar especialmente en comida, pero a fame fai que calquera abandone a filosofía. Imos tentar simular a situación de inanición de fíos no noso problema. A fame é cando se está a executar un fío, pero sen traballo significativo, noutras palabras, este é o mesmo punto morto, só que agora o fío non está durmindo, senón que busca activamente algo para comer, pero non hai comida. Para evitar bloqueos frecuentes, volveremos poñer o garfo se non podemos coller outro.

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

O importante deste código é que dous de cada catro filósofos esquecen deixar o garfo esquerdo. E resulta que comen máis alimentos, mentres que outros comezan a morrer de fame, aínda que os fíos teñen a mesma prioridade. Aquí non están pasando de fame, porque. os malos filósofos volven os garfos ás veces. Resulta que as persoas boas comen unhas 5 veces menos que as malas. Polo tanto, un pequeno erro no código leva a unha caída no rendemento. Tamén cabe sinalar aquí que é posible unha situación rara cando todos os filósofos collen a bifurcación esquerda, non hai unha dereita, poñen a esquerda, agardan, volven tomar a esquerda, etc. Esta situación tamén é unha fame, máis como un punto morto. Non conseguín repetilo. A continuación móstrase unha imaxe dunha situación na que dous filósofos malos tomaron os dous garfos e dous bos están morrendo de fame.

Filósofos ben alimentados ou programación .NET competitiva

Aquí podes ver que os fíos espertan ás veces e tentan conseguir o recurso. Dous dos catro núcleos non fan nada (gráfico verde arriba).

Morte dun filósofo

Pois outro problema que pode interromper unha gloriosa cea de filósofos é se un deles morre de súpeto co garfo nas mans (e así o enterrarán). Entón os veciños quedarán sen cear. Podes crear un código de exemplo para este caso ti mesmo, por exemplo, bótase NullReferenceException despois de que o filósofo colle os garfos. E, por certo, a excepción non se manexará e o código de chamada non só a atrapará (por iso AppDomain.CurrentDomain.UnhandledException e etc.). Polo tanto, necesítanse controladores de erros nos propios fíos e cunha terminación graciosa.

Camareiro

Está ben, como resolvemos este problema de bloqueo, fame e morte? Permitiremos que só un filósofo chegue ás bifurcacións, engadir unha exclusión mutua de fíos para este lugar. Como facelo? Supoñamos que hai un camareiro ao lado dos filósofos que lle dá permiso a calquera filósofo para tomar os garfos. Como facemos este camareiro e como lle preguntarán os filósofos, as preguntas son interesantes.

O xeito máis sinxelo é cando os filósofos simplemente piden constantemente ao camareiro acceso aos garfos. Eses. agora os filósofos non esperarán un garfo preto, senón que agardarán ou preguntarán ao camareiro. Ao principio, usamos só o espazo de usuario para iso, nel non usamos interrupcións para chamar a ningún procedemento desde o núcleo (sobre eles a continuación).

Solucións no espazo de usuario

Aquí faremos o mesmo que antes facíamos cun garfo e dous filósofos, xiraremos nun ciclo e agardaremos. Pero agora serán todos filósofos e, por así dicir, só un garfo, é dicir. pódese dicir que só comerá o filósofo que lle quitou este “garfo de ouro” ao camareiro. Para iso usamos 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 este é un bloqueador, con, grosso modo, o mesmo while(true) { if (!lock) break; }, pero con aínda máis "maxia" que en SpinWait (que se usa alí). Agora sabe contar os que agardan, durmir un pouco e moito máis. etc En xeral, fai todo o posible para optimizar. Pero hai que lembrar que este segue a ser o mesmo ciclo activo que consume os recursos do procesador e mantén o fluxo, o que pode levar á fame se un dos filósofos pasa a ser máis prioritario que outros, pero non ten un garfo de ouro (problema de inversión prioritaria) . Polo tanto, usámolo só para cambios moi moi curtos na memoria compartida, sen chamadas de terceiros, bloqueos anidados e outras sorpresas.

Filósofos ben alimentados ou programación .NET competitiva

Debuxo para SpinLock. Os regatos están constantemente "loitando" polo garfo dourado. Hai fallos: na figura, a área seleccionada. Os núcleos non se utilizan completamente: só uns 2/3 por estes catro fíos.

Outra solución aquí sería usar só Interlocked.CompareExchange coa mesma espera activa que se mostra no código anterior (nos filósofos famélicos), pero isto, como xa se dixo, teoricamente podería levar ao bloqueo.

en Interlocked Hai que ter en conta que non só hai CompareExchange, pero tamén outros métodos de lectura e escritura atómicas. E mediante a repetición de cambios, no caso de que outro fío teña tempo para facer os seus cambios (le 1, ler 2, escribir 2, escribir 1 é malo), pódese usar para cambios complexos nun único valor (patrón Interlocked Anything).

Solucións do modo kernel

Para evitar perder recursos nun bucle, vexamos como podemos bloquear un fío. É dicir, seguindo o noso exemplo, vexamos como o camareiro adormece ao filósofo e o esperta só cando é necesario. En primeiro lugar, vexamos como facelo a través do modo kernel do sistema operativo. Todas as estruturas alí son moitas veces máis lentas que as do espazo de usuario. Varias veces máis lento, por exemplo AutoResetEvent quizais 53 veces máis lento SpinLock [Richter]. Pero coa súa axuda, pode sincronizar procesos en todo o sistema, xestionados ou non.

A construción básica aquí é o semáforo proposto por Dijkstra hai máis de medio século. Un semáforo é, simplemente, un número enteiro positivo xestionado polo sistema, e dúas operacións sobre el, incrementar e decrementar. Se non pode diminuír, cero, entón o fío de chamada está bloqueado. Cando o número é incrementado por algún outro fío/proceso activo, os fíos sáltanse e o semáforo decreméntase de novo co número pasado. Pódese imaxinar trens nun pescozo de botella cun semáforo. .NET ofrece varias construcións con funcionalidades similares: AutoResetEvent, ManualResetEvent, Mutex e a min mesmo Semaphore. Usaremos AutoResetEvent, esta é a máis sinxela destas construcións: só dous valores 0 e 1 (falso, verdadeiro). O seu Método WaitOne() bloquea o fío de chamada se o valor era 0 e, se é 1, redúceo a 0 e sáltao. Un método Set() sobe a 1 e deixa pasar un camareiro, que volve baixar a 0. Actúa como un torniquete de metro.

Complicamos a solución e usemos o bloqueo para cada filósofo, e non para todos á vez. Eses. agora pode haber varios filósofos á vez, e nin un. Pero volvemos bloquear o acceso á táboa para poder facer apostas seguras correctamente, evitando carreiras (condicións da carreira).

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

Para entender o que está a suceder aquí, considere o caso en que o filósofo non conseguiu tomar os garfos, entón as súas accións serán as seguintes. Está á espera de acceder á mesa. Recibido, tenta coller os garfos. Non funcionou. Dá acceso á mesa (exclusión mutua). E pasa o seu "torniquete" (AutoResetEvent) (inicialmente están abertos). Entra de novo no ciclo, porque non ten garfos. Tenta collelos e detense no seu "torniquete". Algún veciño máis afortunado da dereita ou da esquerda, rematado de comer, desbloquea o noso filósofo, "abrindo o seu torniquete". O noso filósofo pasa (e péchase detrás) por segunda vez. Tenta por terceira vez coller os garfos. Moita sorte. E pasa o seu torniquete para cear.

Cando hai erros aleatorios neste código (sempre existen), por exemplo, un veciño se especifica incorrectamente ou se crea o mesmo obxecto AutoResetEvent para todos (Enumerable.Repeat), entón os filósofos estarán agardando polos desenvolvedores, porque Buscar erros neste código é unha tarefa bastante difícil. Outro problema con esta solución é que non garante que algún filósofo non vaia morrer de fame.

Solucións Híbridas

Observamos dous enfoques para a sincronización, cando permanecemos no modo de usuario e en bucle, e cando bloqueamos o fío a través do núcleo. O primeiro método é bo para os bloqueos curtos, o segundo para os longos. Moitas veces é necesario esperar brevemente a que unha variable cambie nun bucle e despois bloquear o fío cando a espera sexa longa. Este enfoque está implementado no chamado. estruturas híbridas. Aquí están as mesmas construcións que para o modo kernel, pero agora cun bucle de modo de usuario: SemaphorSlim, ManualResetEventSlim etc. O deseño máis popular aquí é Monitor, porque en C# hai unha coñecida lock sintaxe. Monitor este é o mesmo semáforo cun valor máximo de 1 (mutex), pero con soporte para a espera nun bucle, a recursividade, o patrón de variable de condición (máis sobre iso a continuación), etc. Vexamos unha solución con el.

// Спрячем объект для Монитора от всех, чтобы без дедлоков.
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í volvemos bloquear toda a mesa para o acceso aos garfos, pero agora estamos desbloqueando todos os fíos á vez, e non os veciños cando alguén acaba de comer. Eses. primeiro, alguén come e bloquea aos veciños, e cando isto remata alguén, pero quere volver comer enseguida, entra en bloqueo e esperta aos seus veciños, porque. o seu tempo de espera é menor.

Así evitamos os bloqueos e a fame dalgún filósofo. Usamos un bucle para unha breve espera e bloqueamos o fío durante un longo. Desbloquear a todos á vez é máis lento que se só se desbloquease o veciño, como na solución con AutoResetEvent, pero a diferenza non debe ser grande, porque primeiro os fíos deben permanecer no modo de usuario.

У lock sintaxe ten desagradables sorpresas. Recoméndase usar Monitor directamente [Richter] [Eric Lippert]. Unha delas é esa lock sempre fóra Monitor, aínda que houbese unha excepción, nese caso outro fío podería cambiar o estado da memoria compartida. Nestes casos, moitas veces é mellor ir ao punto morto ou finalizar o programa de forma segura. Outra sorpresa é que Monitor usa bloques de sincronización (SyncBlock), que están presentes en todos os obxectos. Polo tanto, se se selecciona un obxecto inadecuado, pode obter facilmente un punto morto (por exemplo, se bloquea unha cadea internada). Usamos o obxecto sempre oculto para iso.

O patrón Variable de condición permítelle implementar de forma máis concisa a expectativa dalgunha condición complexa. En .NET, está incompleto, na miña opinión, porque en teoría, debería haber varias colas en varias variables (como en Posix Threads), e non nun lok. Entón podería facelos para todos os filósofos. Pero mesmo nesta forma, permítelle reducir o código.

moitos filósofos ou async / await

Está ben, agora podemos bloquear os fíos de forma efectiva. Pero e se temos moitos filósofos? 100? 10000? Por exemplo, recibimos 100000 solicitudes ao servidor web. Será unha sobrecarga crear un fío para cada solicitude, porque tantos fíos non funcionarán en paralelo. Só executará tantos como núcleos lóxicos haxa (teño 4). E todos os demais só quitarán recursos. Unha solución a este problema é o patrón asíncrono/await. A súa idea é que a función non manteña o fío se ten que esperar a que continúe algo. E cando fai algo, retoma a súa execución (pero non necesariamente no mesmo fío!). No noso caso, agardaremos pola bifurcación.

SemaphoreSlim ten para iso WaitAsync() método. Aquí tes unha implementación usando este patrón.

// Запуск такой же, как раньше. Где-нибудь в программе:
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étodo con async / await tradúcese nunha máquina de estado complicada que devolve inmediatamente o seu interno Task. A través del, podes esperar a que se complete o método, cancelalo e todo o que poidas facer con Task. Dentro do método, a máquina de estado controla a execución. A conclusión é que se non hai atraso, entón a execución é sincrónica e, se o hai, o fío é liberado. Para unha mellor comprensión disto, é mellor mirar esta máquina de estado. Podes crear cadeas a partir destes async / await métodos.

Imos probar. Traballo de 100 filósofos nunha máquina con 4 núcleos lóxicos, 8 segundos. A solución anterior con Monitor só executaba os primeiros 4 fíos e o resto non funcionaba en absoluto. Cada un destes 4 fíos estivo inactivo durante uns 2 ms. E a solución asíncrona/await executou as 100, cunha espera media de 6.8 segundos cada unha. Por suposto, nos sistemas reais, inactividade durante 6 segundos é inaceptable e é mellor non procesar tantas solicitudes como esta. A solución con Monitor resultou non ser escalable en absoluto.

Conclusión

Como podes ver nestes pequenos exemplos, .NET admite moitas construcións de sincronización. Non obstante, non sempre é obvio como usalos. Espero que este artigo fose útil. Polo momento, este é o final, pero aínda quedan moitas cousas interesantes, por exemplo, coleccións thread-safe, TPL Dataflow, programación reactiva, modelo de transacción de software, etc.

Fontes

Fonte: www.habr.com

Engadir un comentario