O que exatamente acontece quando um thread aguarda uma tarefa dentro de um loop while?

10

Depois de lidar com o padrão assíncrono / aguardado do C # por um tempo, de repente percebi que não sabia realmente como explicar o que acontece no código a seguir:

async void MyThread()
{
    while (!_quit)
    {
        await GetWorkAsync();
    }
}

GetWorkAsync()é assumido como retornando um aguardável Taskque pode ou não causar uma troca de encadeamento quando a continuação é executada.

Eu não ficaria confuso se a espera não estivesse dentro de um loop. Eu naturalmente esperaria que o restante do método (ou seja, continuação) fosse executado em outro segmento, o que é bom.

No entanto, dentro de um loop, o conceito de "o resto do método" fica um pouco nebuloso para mim.

O que acontece com "o resto do loop" se o encadeamento é ativado na continuação vs. se não é trocado? Em qual thread a próxima iteração do loop é executada?

Minhas observações mostram (não verificado conclusivamente) que cada iteração começa no mesmo encadeamento (o original) enquanto a continuação é executada em outro. Isso pode realmente ser? Em caso afirmativo, esse é um grau de paralelismo inesperado que precisa ser considerado em relação à segurança de thread do método GetWorkAsync?

ATUALIZAÇÃO: Minha pergunta não é duplicada, como sugerido por alguns. O while (!_quit) { ... }padrão de código é apenas uma simplificação do meu código real. Na realidade, meu encadeamento é um loop de longa duração que processa sua fila de entrada de itens de trabalho em intervalos regulares (a cada 5 segundos por padrão). A verificação real da condição de encerramento também não é uma simples verificação de campo, conforme sugerido pelo código de amostra, mas sim uma verificação de manipulação de eventos.

aoven
fonte
1
Consulte também Como gerar e aguardar implementar o fluxo de controle no .NET? para obter ótimas informações sobre como tudo isso está conectado.
21417 John Wu
@ John Wu: Eu ainda não vi esse tópico. Muitas informações interessantes. Obrigado!
aoven

Respostas:

6

Você pode realmente conferir no Try Roslyn . Seu método wait é reescrito na void IAsyncStateMachine.MoveNext()classe assíncrona gerada.

O que você verá é algo como isto:

            if (this.state != 0)
                goto label_2;
            //set up the state machine here
            label_1:
            taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter);
            label_2:
            if (!OuterClass._quit)
            {
               taskAwaiter = GetWorkAsync().GetAwaiter();
               //state machine stuff here
            }
            goto label_1;

Basicamente, não importa em qual tópico você está; a máquina de estado pode retomar adequadamente substituindo seu loop por uma estrutura if / goto equivalente.

Dito isto, os métodos assíncronos não são necessariamente executados em um thread diferente. Veja a explicação de Eric Lippert "Não é mágico" para explicar como você pode trabalhar async/awaitem apenas um tópico.

Mason Wheeler
fonte
2
Parece que estou subestimando a extensão da reescrita que o compilador faz no meu código assíncrono. Em essência, não há "loop" após a reescrita! Essa foi a parte que faltava para mim. Impressionante e obrigado pelo link 'Try Roslyn' também!
aoven
GOTO é a construção de loop original . Não vamos esquecer isso.
2

Em primeiro lugar, Servy escreveu algum código em uma resposta a uma pergunta semelhante, na qual esta resposta se baseia:

/programming/22049339/how-to-create-a-cancellable-task-loop

A resposta de Servy inclui um ContinueWith()loop semelhante usando construções TPL sem uso explícito das palavras async- awaitchave e ; para responder sua pergunta, considere a aparência do seu código quando seu loop for desenrolado usandoContinueWith()

    private static Task GetWorkWhileNotQuit()
    {
        var tcs = new TaskCompletionSource<bool>();

        Task previous = Task.FromResult(_quit);
        Action<Task> continuation = null;
        continuation = t =>
        {
            if (!_quit)
            {
                previous = previous.ContinueWith(_ => GetWorkAsync())
                    .Unwrap()
                    .ContinueWith(_ => previous.ContinueWith(continuation));
            }
            else
            {
                tcs.SetResult(_quit);
            }
        };
        previous.ContinueWith(continuation);
        return tcs.Task;
    }

Isso leva algum tempo para você entender, mas em resumo:

  • continuationrepresenta um fechamento para a "iteração atual"
  • previousrepresenta o Taskestado da "iteração anterior" (isto é, ele sabe quando a 'iteração' termina e é usada para iniciar a próxima ..)
  • Supondo que GetWorkAsync()retorna a Task, isso significa ContinueWith(_ => GetWorkAsync())que retornará a, Task<Task>portanto, a chamada Unwrap()para obter a 'tarefa interna' (isto é, o resultado real de GetWorkAsync()).

Então:

  1. Inicialmente, não há iteração anterior ; portanto, é simplesmente atribuído um valor de Task.FromResult(_quit) - seu estado começa como Task.Completed == true.
  2. O continuationé executado pela primeira vez usandoprevious.ContinueWith(continuation)
  3. as continuationatualizações de fechamento previouspara refletir o estado de conclusão_ => GetWorkAsync()
  4. Quando _ => GetWorkAsync()concluído, "continua com" _previous.ContinueWith(continuation)- ou seja, chamando o continuationlambda novamente
    • Obviamente, neste ponto, previousfoi atualizado com o estado de _ => GetWorkAsync()modo que o continuationlambda é chamado quando GetWorkAsync()retorna.

O continuationlambda sempre verifica o estado de _quitque, se _quit == falsenão houver mais continuações, TaskCompletionSourceo valor definido é definido como _quite tudo está concluído.

Quanto à sua observação sobre a continuação sendo executada em um thread diferente, isso não é algo que a palavra-chave async/ awaitfaria por você, conforme este blog "As tarefas (ainda) não são threads e o assíncrono não é paralelo" . - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

Eu sugiro que, de fato, vale a pena dar uma olhada mais de perto no seu GetWorkAsync()método com relação à rosqueamento e segurança de threads. Se o seu diagnóstico estiver revelando que ele está sendo executado em um encadeamento diferente como consequência do código assíncrono / de espera repetido, algo dentro ou relacionado a esse método deve estar causando a criação de um novo encadeamento em outro local. (Se isso for inesperado, talvez haja .ConfigureAwaitalgum lugar?)

Ben Cottrell
fonte
2
O código que mostrei é (muito) simplificado. Dentro de GetWorkAsync (), existem várias outras esperas. Alguns deles acessam o banco de dados e a rede, o que significa E / S verdadeira. Pelo que entendi, a opção de encadeamento é uma consequência natural (embora não necessária) de tais esperas, porque o encadeamento inicial não estabelece nenhum contexto de sincronização que governaria onde as continuações deveriam ser executadas. Então, eles são executados em um thread do pool de threads. Meu raciocínio está errado?
aoven
@ aoven Bom ponto - não considerei os diferentes tipos de SynchronizationContext- isso é certamente importante, pois .ContinueWith()usa o SynchronizationContext para despachar a continuação; de fato, explicaria o comportamento que você está vendo se awaitfor chamado em um thread ThreadPool ou em um thread ASP.NET. Uma continuação certamente poderia ser despachada para um thread diferente nesses casos. Por outro lado, a chamada awaitem um contexto de thread único, como um WPF Dispatcher ou Winforms, deve ser suficiente para garantir que a continuação ocorra no original. thread
Ben Cottrell