
Vamos analisar como funciona a programação concorrente e paralela no .Net, usando o exemplo do problema dos filósofos à mesa. O plano é abordar desde a sincronização de threads/processos até o modelo de atores (nas próximas partes). Este artigo pode ser útil para uma primeira familiarização ou para refrescar seus conhecimentos.
Por que é importante saber isso? Os transistores atingem seu tamanho mínimo, a Lei de Moore encontra um limite na velocidade da luz e, portanto, o crescimento se dá em quantidade, podendo se fazer mais transistores. Ao mesmo tempo, a quantidade de dados aumenta, e os usuários esperam uma resposta imediata dos sistemas. Nesse cenário, a programação "normal", onde temos um único thread em execução, já não é eficiente. É preciso encontrar maneiras de resolver o problema da execução simultânea ou concorrente. Essa questão existe em diferentes níveis: no nível de threads, no nível de processos, e no nível de máquinas em rede (sistemas distribuídos). No .NET, existem tecnologias de qualidade, testadas ao longo do tempo, para resolver rapidamente e de forma eficaz tais problemas.
Tarefa
Edsger Dijkstra já apresentava esse problema a seus alunos em 1965. A formulação clássica é a seguinte. Existe um número determinado (geralmente cinco) de filósofos e o mesmo número de garfos. Eles se sentam ao redor de uma mesa redonda, com garfos entre eles. Os filósofos podem comer de seus pratos com comida infinita, pensar ou esperar. Para comer, um filósofo precisa pegar dois garfos (o último compartilha um garfo com o primeiro). Pegar e colocar um garfo são duas ações distintas. Todos os filósofos são silenciosos. O desafio é encontrar um algoritmo que permita que todos pensem e fiquem satisfeitos, mesmo após 54 anos.
Primeiro, tentaremos resolver esse problema através do uso de um espaço compartilhado. Os garfos estão sobre a mesa comum e os filósofos os pegam quando disponíveis e os colocam de volta. Aqui surgem problemas de sincronização: quando exatamente pegar os garfos? O que fazer se não houver garfos disponíveis? etc. Mas primeiro, vamos iniciar os filósofos.
Para iniciar as threads, usamos um pool de threads através do Task.Run método:
var cancelTokenSource = new CancellationTokenSource();
Action create = (i) => RunPhilosopher(i, cancelTokenSource.Token);
for (int i = 0; i create(icopy), cancelTokenSource.Token);
}O pool de threads foi criado para otimizar a criação e a remoção de threads. Este pool possui uma fila de tarefas e o CLR cria ou remove threads dependendo do número dessas tarefas. Um pool para todas as AppDomain. Este pool deve ser utilizado quase sempre, pois não é necessário se preocupar com a criação, remoção de threads, suas filas, etc. Também é possível operar sem pool, mas nesse caso será necessário utilizar diretamente Thread, isso é conveniente para casos em que é necessário mudar a prioridade de uma thread, quando temos uma operação longa, para threads em primeiro plano, entre outros.
Em outras palavras, System.Threading.Tasks.Task a classe é a mesma Thread, mas com várias conveniências: a capacidade de executar uma tarefa após um bloco de outras tarefas, retorná-las de funções, interrompê-las de forma conveniente e muito mais. Elas são necessárias para suportar construções async/await (Task-based Asynchronous Pattern, açúcar sintático para esperar operações de I/O). Vamos falar mais sobre isso.
CancelationTokenSource é necessário aqui, para que a thread possa se encerrar pelo sinal da thread chamadora.
Problemas com sincronização
Filósofos bloqueados
Bem, sabemos como criar threads, vamos tentar almoçar:
// Кто какие вилки взял. К примеру: 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();
}
}Aqui tentamos primeiro pegar o garfo da esquerda e depois o da direita, e se conseguimos, comemos e os colocamos de volta. Pegar um garfo é atômico, ou seja, duas threads não podem pegar um ao mesmo tempo (incorreto: o primeiro lê que o garfo está livre, o segundo também, o primeiro pega, o segundo pega). Para isso Interlocked.CompareExchange, que deve ser implementado com uma instrução do processador (TSL, XCHG), que bloqueia uma área de memória para leitura e escrita atômica sequencial. O SpinWait é equivalente à construção while(true) somente com um pouco de "mágica" — a thread ocupa o processador (Thread.SpinWait), mas às vezes cede o controle a outra thread (Thread.Yeild) ou dorme (Thread.Sleep).
Mas essa solução não funciona, pois as threads em breve (em um segundo) ficam bloqueadas: todos os filósofos pegam seu garfo da esquerda, mas não o da direita. O array de forks então tem os valores: 1 2 3 4 5.

Na figura, o bloqueio das threads (deadlock). Em verde, a execução, em vermelho, a sincronização, em cinza, a thread está dormindo. Os losangos indicam o tempo de execução das Tasks.
Fome dos filósofos
Embora não seja preciso muito alimento para pensar, a fome pode fazer qualquer um abandonar a filosofia. Vamos tentar simular uma situação de fome de threads em nossa tarefa. Fome é quando uma thread está ativa, mas não realiza trabalho significativo; em outras palavras, é como um deadlock, apenas que agora a thread não está dormindo, mas ativamente tenta encontrar algo para comer, mas a comida não está disponível. Para evitar bloqueios frequentes, devemos devolver o garfo se não conseguimos pegar o 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)
);
}Neste código, o importante é que dois dos quatro filósofos esquecem de devolver seu garfo à esquerda. Assim, eles acabam comendo mais comida, enquanto os outros começam a passar fome, embora as threads tenham a mesma prioridade. Aqui, eles não estão realmente passando fome, pois os filósofos ruins às vezes devolvem seus garfos. Eu percebo que os bons comem cerca de cinco vezes menos do que os ruins. Portanto, um pequeno erro no código resulta em queda de desempenho. Vale a pena notar que há uma situação rara em que todos os filósofos pegam o garfo esquerdo, não têm o direito, e devolvem o esquerdo, esperam, pegam o esquerdo novamente, etc. Essa situação também é uma fome, mais parecida com um deadlock. Não consegui reproduzi-la. Abaixo está uma imagem da situação em que dois filósofos ruins pegaram ambos os garfos, enquanto dois bons estão passando fome.

Aqui é possível ver que as threads às vezes acordam e tentam obter o recurso. Dois núcleos de quatro não fazem nada (gráfico verde no topo).
A morte do filósofo
E mais um problema que pode interromper o glorioso almoço dos filósofos — é se um deles morrer repentinamente com garfos nas mãos (e ele será enterrado assim). Então, os vizinhos ficarão sem almoço. Você pode imaginar um exemplo de código para esse caso, por exemplo, uma exceção é lançada NullReferenceException depois que o filósofo pega os garfos. E, a propósito, a exceção não será tratada e o código chamador não a capturará assim (para isso AppDomain.CurrentDomain.UnhandledException etc.). Portanto, os manipuladores de erro são necessários nas próprias threads e com um encerramento apropriado.
Garçom
Bem, como podemos resolver o problema da espera mútua, fome e mortes? Vamos permitir que apenas um filósofo tenha acesso aos talheres, adicionando a exclusão mútua (mutual exclusion) para este recurso. Como vamos fazer isso? Suponhamos que ao lado dos filósofos está um garçom, que dá permissão a um único filósofo para pegar os talheres. Como vamos implementar esse garçom e como os filósofos vão ele pedir, são questões interessantes.
A maneira mais simples é fazer com que os filósofos peçam constantemente ao garçom acesso aos talheres. Ou seja, agora os filósofos não vão esperar pelo talher ao lado, mas esperar ou pedir ao garçom. Inicialmente, usaremos apenas o Espaço do Usuário, onde não utilizamos interrupções para chamar quaisquer procedimentos do núcleo (sobre isso abaixo).
Soluções no espaço do usuário
Aqui, faremos o mesmo que antes com um talher e dois filósofos, giraremos em um loop e aguardaremos. Mas agora serão todos os filósofos e, de certa forma, apenas um talher, ou seja, podemos dizer que só irá comer o filósofo que pegar este "talher de ouro" do garçom. Para isso, usaremos SpinLock.
private static SpinLock spinLock = new SpinLock(); // Nosso "garçom"
private void RunSpinLock(int i, CancellationToken token)
{
while (true)
{
// Exclusão mútua através de espera ativa. Chamamos até try, para
// lançar uma exceção em caso de erro no próprio SpinLock.
bool hasLock = false;
spinLock.Enter(ref hasLock);
try
{
// Aqui pode haver apenas um thread (exclusão mútua).
forks[Left(i)] = i + 1; // Pegamos o talher imediatamente, sem esperar.
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(); // Evitamos o problema da morte do filósofo.
}
Think(i);
if (token.IsCancellationRequested)
break;
}
}SpinLock é um bloqueador, com, grosso modo, a mesma while(true) { if (!lock) break; }, mas com ainda mais "mágica" do que em SpinWait (que é usado lá). Agora ele pode contar os esperadores, adormecê-los um pouco e muito mais. Em resumo, faz tudo o que é possível para otimização. Mas é importante lembrar que ainda é o mesmo ciclo ativo, que consome recursos de CPU e mantém um fluxo que pode levar à fome, se um dos filósofos se tornar prioritário em relação aos outros, mas não tiver um garfo dourado (Problema de Inversão de Prioridade). Portanto, usamos isso apenas para alterações muito, muito curtas na memória compartilhada, sem chamadas externas, bloqueios aninhados e outras surpresas.

Figuração para SpinLock. Os fluxos estão constantemente "lutando" pelo garfo dourado. Ocorrem falhas — na figura, a área destacada. Os núcleos não são utilizados completamente: apenas cerca de 2/3 por esses quatro fluxos.
Outra solução aqui seria usar apenas Interlocked.CompareExchange com a mesma espera ativa, como mostrado no código acima (nos filósofos famintos), mas isso, como já foi mencionado, pode teoricamente levar a bloqueios.
Sobre Interlocked vale a pena mencionar que lá não estão apenas CompareExchange, mas também outros métodos para leitura e gravação atômicas. E através da repetição de alterações no caso de outro fluxo conseguir fazer suas modificações (leitura 1, leitura 2, gravação 2, gravação 1 ruim), ele pode ser usado para alterações complexas de um único valor (padrão Interlocked Anything).
Soluções em modo kernel
Para evitar a perda de recursos no ciclo, vamos ver como podemos bloquear um fluxo. Em outras palavras, continuando nosso exemplo, vamos ver como o garçom adormece o filósofo e o acorda apenas quando necessário. Primeiro, vamos considerar como fazer isso através do modo kernel do sistema operacional. Todas as estruturas lá costumam ser mais lentas do que aquelas no espaço do usuário. Mais lentas várias vezes, por exemplo AutoResetEvent pode ser até 53 vezes mais lento SpinLock [Richter]. Mas com eles é possível sincronizar processos em todo o sistema, sejam eles gerenciados ou não.
A estrutura principal aqui é o semáforo, proposto por Dijkstra há mais de meio século. O semáforo é, simplificando, um número inteiro positivo gerenciável pelo sistema, com duas operações nele: aumentar e diminuir. Se não for possível diminuir, como no caso de zero, a thread chamadora é bloqueada. Quando o número é aumentado por outra thread/processo ativo, as threads são liberadas e o semáforo é reduzido pelo número de threads que passaram. Pode-se imaginar trens em um local estreito com um semáforo. O .NET oferece várias construções com funções semelhantes: AutoResetEvent, ManualResetEvent, Mutex e mesmo Semaphore. Vamos usar AutoResetEvent, esta é a mais simples dessas construções: apenas dois valores, 0 e 1 (falso, verdadeiro). Seu método WaitOne() bloqueia a thread chamadora se o valor era 0, e se era 1, reduz para 0 e a permite passar. O método Set() aumenta para 1 e libera uma que estava esperando, que então reduz novamente para 0. Funciona como um catraca no metrô.
Vamos complicar a solução e usar um bloqueio para cada filósofo, e não para todos ao mesmo tempo. Ou seja, agora pode haver vários filósofos ao mesmo tempo, e não apenas um. Mas novamente bloqueamos o acesso à mesa para pegar os garfos de forma correta, evitando condições de corrida.
// Для блокирования отдельного философа.
// Инициализируется: 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á acontecendo aqui, consideremos o caso em que o filósofo não conseguiu pegar os garfos; suas ações serão as seguintes. Ele aguarda acesso à mesa. Ao obtê-la, tenta pegar os garfos. Não conseguiu. Ele libera o acesso à mesa (exclusão mútua). E passa pela sua “catraca” (AutoResetEvent) (no início, elas estão abertas). Entra novamente no ciclo, pois não tem garfos. Tenta pegá-los e para em sua “catraca”. Algum vizinho mais sortudo à direita ou à esquerda, após terminar de comer, desbloqueia nosso filósofo, “abrindo sua catraca”. Nosso filósofo passa por ela (e ela se fecha atrás dele) pela segunda vez. Tenta pela terceira vez pegar os garfos. Com sucesso. E passa pela sua catraca para almoçar.
Quando há erros aleatórios nesse código (eles sempre existem), por exemplo, se um vizinho for indicado incorretamente ou se o mesmo objeto AutoResetEvent for criado para todos (Enumerable.Repeat), então os filósofos ficarão esperando os desenvolvedores, pois encontrar erros nesse tipo de código é uma tarefa muito difícil. Outro problema com essa solução é que ela não garante que algum filósofo não comece a passar fome.
Soluções híbridas
Analisamos duas abordagens para sincronização, quando permanecemos no modo de usuário e entramos em um loop, e quando bloqueamos o thread através do núcleo. O primeiro método é bom para bloqueios curtos, o segundo para bloqueios prolongados. Muitas vezes, é necessário inicialmente esperar brevemente a alteração de uma variável em um loop e depois bloquear o thread quando a espera é longa. Essa abordagem está implementada nas chamadas construções híbridas. Aqui estão as mesmas construções que existiam para o modo núcleo, mas agora com um loop em modo de usuário: SemaphorSlim, ManualResetEventSlim e outros. A construção mais popular aqui é Monitor, pois em C# existe o conhecido lock sintaxe. Monitor é o mesmo semáforo com um valor máximo de 1 (mutex), mas com suporte para espera em loop, recursão, padrão de Variável de Condição (sobre o qual falaremos abaixo) e outros. Vamos examinar uma solução com ele.
// Спрячем объект для Монитора от всех, чтобы без дедлоков.
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);
}
}Aqui novamente bloqueamos toda a mesa para acesso aos garfos, mas agora desbloqueamos todos os threads de uma só vez, e não apenas os vizinhos, quando alguém termina de comer. Ou seja, inicialmente, alguém come e bloqueia os vizinhos, e quando essa pessoa termina, mas quer comer novamente imediatamente, ela entra em bloqueio e acorda seus vizinhos, já que seu tempo de espera é menor.
Assim, evitamos deadlocks e a fome de algum filósofo. Usamos um loop para espera breve e bloqueamos o thread para largas esperas. O desbloqueio de todos de uma só vez é mais lento do que se apenas o vizinho fosse desbloqueado, como na solução com AutoResetEvent, mas a diferença não deve ser grande, já que os threads devem permanecer no modo usuário inicialmente.
Na lock A sintaxe possui surpresas desagradáveis. Recomenda-se utilizar Monitor diretamente [Richter] [Eric Lippert]. Uma delas é que lock sempre sai do Monitor, mesmo se houve uma exceção, e então outro thread pode alterar o estado da memória compartilhada. Nesses casos, muitas vezes é melhor entrar em deadlock ou encerrar o programa de maneira segura. Outra surpresa é que o Monitor usa blocos de sincronização (SyncBlock), que existem em todos os objetos. Portanto, se um objeto inadequado for escolhido, pode-se facilmente obter um deadlock (por exemplo, se fizer lock em uma string internada). Sempre usamos um objeto oculto para isso.
O padrão Condition Variable permite implementar de forma mais concisa a espera por uma condição complexa. No .NET, ele é incompleto, na minha opinião, pois deveria haver várias filas para várias variáveis (como nas Posix Threads), em vez de uma única fila. Assim, seria possível aplicá-las para todos os filósofos. Mas mesmo assim, ele permite reduzir o código.
Muitos filósofos ou assíncrono / esperar
Bem, agora conseguimos bloquear threads de maneira eficiente. Mas e se tivermos muitos filósofos? 100? 10.000? Por exemplo, recebemos 100.000 solicitações em um servidor web. Criar uma thread para cada solicitação seria dispendioso, pois muitas threads não podem ser executadas paralelamente. Somente tantas quanto o número de núcleos lógicos (eu tenho 4) serão executadas. Todas as outras simplesmente consomem recursos. Uma das soluções para esse problema é o padrão async / await. A ideia é que a função não mantém a thread se precisar esperar por algo. Quando esse algo ocorre, ela retoma sua execução (mas não necessariamente na mesma thread!). No nosso caso, aguardaremos os garfos.
SemaphoreSlim possui isso WaitAsync() método. Aqui está uma implementação utilizando esse padrão.
// Запуск такой же, как раньше. Где-нибудь в программе:
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();
}O método com assíncrono / esperar é traduzido em um autômato finito engenhoso, que imediatamente retorna seu interno Tarefa. Através dele, você pode aguardar a conclusão do método, cancelá-lo e tudo mais que pode ser feito com Task. Dentro do método, o autômato finito controla a execução. A essência é que, se não há atraso, a execução é síncrona, e se houver, a thread é liberada. Para uma melhor compreensão disso, é melhor observar esse autômata finito. É possível criar cadeias a partir desses assíncrono / esperar métodos.
Vamos testar. Funcionamento de 100 filósofos em uma máquina com 4 núcleos lógicos, 8 segundos. A solução anterior com Monitor executava apenas as 4 primeiras threads, enquanto as demais não eram executadas. Cada uma dessas 4 threads ficou inativa por cerca de 2 ms. Já a solução com async / await executou todas as 100, com uma espera média de 6,8 segundos cada. Claro, em sistemas reais, uma inatividade de 6 segundos é inaceitável e é melhor não processar tantas solicitações dessa forma. A solução com Monitor, por sua vez, provou não ser escalável de forma alguma.
Conclusão
Como pode ser visto nestes pequenos exemplos, .NET suporta muitas construções de sincronização. No entanto, nem sempre é óbvio como usá-las. Espero que este artigo tenha sido útil. Por enquanto é isso, mas ainda há muito mais interessante, como coleções thread-safe, TPL Dataflow, programação reativa, o modelo de transação de software entre outros.
Fontes
- Visualização de Fluxos:
- MSDN: , e muito mais.
- [Richter] — CLR via C#, Jeffrey Richter
- [Eric Lippert] —
- Imagem — 'Dança entre Espadas', G. Semiradski
Fonte: habr.com
