Precisa entender o uso de SemaphoreSlim

95

Aqui está o código que tenho, mas não entendo o que SemaphoreSlimestá fazendo.

async Task WorkerMainAsync()
{
    SemaphoreSlim ss = new SemaphoreSlim(10);
    List<Task> trackedTasks = new List<Task>();
    while (DoMore())
    {
        await ss.WaitAsync();
        trackedTasks.Add(Task.Run(() =>
        {
            DoPollingThenWorkAsync();
            ss.Release();
        }));
    }
    await Task.WhenAll(trackedTasks);
}

void DoPollingThenWorkAsync()
{
    var msg = Poll();
    if (msg != null)
    {
        Thread.Sleep(2000); // process the long running CPU-bound job
    }
}

O que espera ss.WaitAsync();e ss.Release();faz?

Eu acho que se eu executar 50 threads de uma vez, então escrevo o código, SemaphoreSlim ss = new SemaphoreSlim(10);então ele será forçado a executar 10 threads ativas por vez.

Quando um dos 10 tópicos for concluído, outro será iniciado. Se eu não estiver certo, então me ajude a entender com uma situação de amostra.

Por que é awaitnecessário junto com ss.WaitAsync();? O que ss.WaitAsync();fazer?

Mou
fonte
3
Uma coisa a observar é que você realmente deve embrulhar "DoPollingThenWorkAsync ();" em um "try {DoPollingThenWorkAsync ();} finally {ss.Release ();}", caso contrário, as exceções irão privar permanentemente esse semáforo.
Austin Salgat
Eu me sinto um pouco estranho que adquirimos e liberamos o semáforo fora / dentro da tarefa, respectivamente. Mover o "await ss.WaitAsync ()" dentro da tarefa fará alguma diferença?
Shane Lu

Respostas:

77

Eu acho que se eu executar 50 threads de uma vez, codifique como SemaphoreSlim ss = new SemaphoreSlim (10); forçará a execução de 10 tópicos ativos por vez

Está correto; o uso do semáforo garante que não haverá mais de 10 trabalhadores fazendo este trabalho ao mesmo tempo.

Chamar WaitAsynco semáforo produz uma tarefa que será concluída quando o thread tiver "acesso" ao token. await-ing essa tarefa permite que o programa continue a execução quando for "permitido" fazê-lo. Ter uma versão assíncrona, em vez de chamar Wait, é importante tanto para garantir que o método permaneça assíncrono, em vez de síncrono, quanto para lidar com o fato de que um asyncmétodo pode estar executando código em vários threads, devido aos retornos de chamada e assim a afinidade natural do fio com semáforos pode ser um problema.

Uma nota lateral: DoPollingThenWorkAsyncnão deve ter o Asyncpostfix porque não é realmente assíncrono, é síncrono. Basta ligar DoPollingThenWork. Isso reduzirá a confusão para os leitores.

Servy
fonte
obrigado, mas por favor me diga o que acontece quando especificamos não de thread a ser executado, digamos 10. quando um de 10 thread termina, então novamente esse thread pula para terminar outro trabalho ou volta ao pool? isso não está muito claro para .... então explique o que acontece nos bastidores.
Mou
@Mou O que não está claro sobre isso? O código espera até que haja menos de 10 tarefas em execução; quando houver, adiciona outro. Quando uma tarefa é concluída, ela indica que foi concluída. É isso aí.
Servy
qual é a vantagem de especificar não de thread a ser executado. se muitos tópicos podem prejudicar o desempenho? se sim, então por que dificultar ... se eu executar 50 threads em vez de 10 threads, então por que o desempenho será importante ... você pode explicar. obrigado
Thomas
4
@Thomas Se você tiver muitos encadeamentos simultâneos, os encadeamentos gastam mais tempo trocando de contexto do que fazendo trabalho produtivo. A taxa de transferência diminui à medida que os threads aumentam à medida que você passa mais e mais tempo gerenciando threads em vez de trabalhar, pelo menos, uma vez que a contagem de threads vai muito além do número de núcleos na máquina.
Servy
3
@Servy Isso faz parte do trabalho do agendador de tarefas. Tarefas! = Tópicos. O Thread.Sleepno código original devastaria o agendador de tarefas. Se você não é assíncrono com o núcleo, você não é assíncrono.
Joseph Lennox
67

No jardim de infância ao virar da esquina, eles usam um SemaphoreSlim para controlar quantas crianças podem brincar na sala de educação física.

Pintaram no chão, fora da sala, 5 pares de pegadas.

Conforme as crianças chegam, elas deixam seus sapatos em um par de pegadas livres e entram na sala.

Assim que terminarem de jogar, eles saem, pegam seus sapatos e "liberam" uma vaga para outra criança.

Se uma criança chega e não há pegadas restantes, ela vai brincar em outro lugar ou apenas fica por um tempo e verifica de vez em quando (ou seja, sem prioridades FIFO).

Quando uma professora está por perto, ela "libera" uma fileira extra de 5 pegadas do outro lado do corredor, de modo que mais 5 crianças possam brincar na sala ao mesmo tempo.

Ele também tem as mesmas "armadilhas" do SemaphoreSlim ...

Se uma criança termina de jogar e sai da sala sem recolher os sapatos (não aciona o "desbloqueio"), a vaga permanece bloqueada, mesmo que teoricamente haja uma vaga vazia. O garoto geralmente é repreendido.

Às vezes, uma ou duas crianças sorrateiras escondem seus sapatos em outro lugar e entram na sala, mesmo que todas as pegadas já tenham sido tiradas (ou seja, o SemaphoreSlim não controla "realmente" quantas crianças estão na sala).

Isso geralmente não termina bem, já que a superlotação da sala tende a acabar com o choro das crianças e o professor fechando totalmente a sala.

dandiez
fonte
9
Esses tipos de respostas são minhas favoritas.
Minha pilha estourou em
7

Embora eu aceite que essa pergunta realmente se relaciona a um cenário de bloqueio de contagem regressiva, achei que valeria a pena compartilhar este link que descobri para aqueles que desejam usar um SemaphoreSlim como um bloqueio assíncrono simples. Ele permite que você use a instrução using, que pode tornar a codificação mais organizada e segura.

http://www.tomdupont.net/2016/03/how-to-release-semaphore-with-using.html

Eu fiz troca _isDisposed=truee _semaphore.Release()em torno de sua Descarte embora no caso de alguma forma foi chamado várias vezes.

Também é importante observar que SemaphoreSlim não é um bloqueio reentrante, o que significa que se o mesmo encadeamento chamar WaitAsync várias vezes, a contagem do semáforo será diminuída todas as vezes. Em suma, SemaphoreSlim não reconhece Thread.

Com relação às questões de qualidade de código, é melhor colocar o Release dentro de uma tentativa para garantir que ele sempre seja lançado.

andrew pate
fonte
6
Não é aconselhável postar respostas apenas com links, pois os links tendem a morrer com o tempo, tornando a resposta inútil. Se possível, é melhor resumir os pontos-chave ou o bloco de código-chave em sua resposta.
João