Continuação da tarefa no encadeamento da interface do usuário


214

Existe uma maneira 'padrão' de especificar que uma continuação de tarefa deve ser executada no thread a partir do qual a tarefa inicial foi criada?

Atualmente, tenho o código abaixo - ele está funcionando, mas acompanhar o despachante e criar uma segunda ação parece uma sobrecarga desnecessária.

dispatcher = Dispatcher.CurrentDispatcher;
Task task = Task.Factory.StartNew(() =>
{
    DoLongRunningWork();
});

Task UITask= task.ContinueWith(() =>
{
    dispatcher.Invoke(new Action(() =>
    {
        this.TextBlock1.Text = "Complete"; 
    }
});

No caso do seu exemplo, você poderia usar Control.Invoke(Action), ie. TextBlock1.Invokeem vez dedispatcher.Invoke
Coronel Panic

2
Obrigado @ColonelPanic, mas eu estava usando WPF (como marcado), não winforms.
Greg Sansom

Respostas:


352

Ligue para a continuação com TaskScheduler.FromCurrentSynchronizationContext():

    Task UITask= task.ContinueWith(() =>
    {
     this.TextBlock1.Text = "Complete"; 
    }, TaskScheduler.FromCurrentSynchronizationContext());

Isso é adequado apenas se o contexto de execução atual estiver no encadeamento da interface do usuário.


39
É válido apenas se o contexto de execução atual estiver no encadeamento da interface do usuário. Se você colocar este código dentro de outra tarefa, então você obtém InvalidOperationException (olhada Exceções seção)
stukselbax

3
No .NET 4.5, a resposta de Johan Larsson deve ser usada como maneira padrão para a continuação de uma tarefa no thread da interface do usuário. Basta escrever: aguarde Task.Run (DoLongRunningWork); this.TextBlock1.Text = "Completo"; Veja também: blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Marcel W

1
Thx por salvar minha vida. Passo horas para descobrir como chamar as coisas do thread principal dentro do wait / ContinueWith. Para todos os outros, como está usando o Google Firebase SDK para Unity e ainda tem os mesmos problemas, esta é uma abordagem funcional.
CHaP 15/09/19

2
@MarcelW - awaité um bom padrão - mas apenas se você estiver dentro de um asynccontexto (como um método declarado async). Caso contrário, ainda é necessário fazer algo como esta resposta.
Home

33

Com o assíncrono, você apenas faz:

await Task.Run(() => do some stuff);
// continue doing stuff on the same context as before.
// while it is the default it is nice to be explicit about it with:
await Task.Run(() => do some stuff).ConfigureAwait(true);

Contudo:

await Task.Run(() => do some stuff).ConfigureAwait(false);
// continue doing stuff on the same thread as the task finished on.

2
O comentário na falseversão me confunde. Eu pensei que falsesignifica que pode continuar em um segmento diferente .
Home

1
@ToolmakerSteve Depende em qual tópico você está pensando. O segmento de trabalho usado por Task.Run ou o segmento de chamada? Lembre-se de que "o mesmo encadeamento em que a tarefa terminou" significa o encadeamento do trabalhador (evitando 'alternar' entre encadeamentos). Além disso, o ConfigureAwait (true) não garante que o controle retorne ao mesmo encadeamento , apenas ao mesmo contexto (embora a distinção possa não ser significativa).
precisa

@ MaxBarraclough - Obrigado, eu li errado qual "mesmo segmento" foi feito. evitando alternar entre threads no sentido de maximizar o desempenho usando qualquer thread que esteja em execução [para executar a tarefa "fazer algumas coisas"], que esclarece isso para mim.
Home

1
A pergunta não especifica estar dentro de um asyncmétodo (que é necessário, para usar await). Qual é a resposta quando awaitnão está disponível?
Home

22

Se você tiver um valor de retorno que precisará enviar para a interface do usuário, poderá usar a versão genérica como esta:

Isso está sendo chamado de um MVVM ViewModel no meu caso.

var updateManifest = Task<ShippingManifest>.Run(() =>
    {
        Thread.Sleep(5000);  // prove it's really working!

        // GenerateManifest calls service and returns 'ShippingManifest' object 
        return GenerateManifest();  
    })

    .ContinueWith(manifest =>
    {
        // MVVM property
        this.ShippingManifest = manifest.Result;

        // or if you are not using MVVM...
        // txtShippingManifest.Text = manifest.Result.ToString();    

        System.Diagnostics.Debug.WriteLine("UI manifest updated - " + DateTime.Now);

    }, TaskScheduler.FromCurrentSynchronizationContext());

Eu estou supondo o = antes GenerateManifest é um erro de digitação.
19415 Sebastien F.

Sim - se foi agora! THX.
Simon_Weaver

11

Eu só queria adicionar esta versão, porque este é um segmento tão útil e acho que é uma implementação muito simples. Eu usei isso várias vezes em vários tipos, se o aplicativo multithread:

 Task.Factory.StartNew(() =>
      {
        DoLongRunningWork();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
              { txt.Text = "Complete"; }));
      });

2
Não obtendo voto negativo, pois essa é uma solução viável em alguns cenários; no entanto, a resposta aceita é muito melhor. É independente de tecnologia ( TaskSchedulerfaz parte da BCL, Dispatchernão é) e pode ser usada para compor cadeias complexas de tarefas devido a não ter que se preocupar com nenhuma operação assíncrona de ignorar e esquecer (como BeginInvoke).
Kirill Shlenskiy 22/09

@Kirill, você pode expandir um pouco, porque alguns segmentos SO declararam por unanimidade que o despachante é o método correto ao usar o WPF do WinForms: Pode-se chamar uma atualização da GUI de forma assíncrona (usando BeginInvoke) ou de forma síncrona (Invoke), embora normalmente seja assíncrona é usado porque não se deseja bloquear um encadeamento em segundo plano apenas para uma atualização da GUI. FromCurrentSynchronizationContext não coloca a tarefa de continuação na fila de mensagens do encadeamento principal da mesma maneira que o expedidor?
Dean

1
Certo, mas o OP certamente está perguntando sobre o WPF (e o marcou assim), e não deseja manter uma referência a nenhum despachante (e eu assumo qualquer contexto de sincronização - você só pode obter isso no encadeamento principal e precisa armazene uma referência a ele em algum lugar). É por isso que eu gosto da solução que publiquei: existe uma referência estática segura para threads que não exige nada disso. Eu acho que isso é extremamente útil no contexto do WPF.
Dean

3
Só queria reforçar meu último comentário: O desenvolvedor não apenas precisa armazenar o contexto de sincronização, mas também precisa saber que isso está disponível apenas no thread principal; esse problema foi motivo de confusão em dezenas de perguntas do SO: as pessoas o tempo todo tentam obter isso do segmento de trabalho. Se o próprio código foi movido para um segmento de trabalho, ele falhará devido a esse problema. Portanto, devido à prevalência de WPF, isso definitivamente deve ser esclarecido aqui nesta questão popular.
Dean

1
... no entanto, é importante observar a observação de Dean sobre [a resposta aceita] para manter o controle do contexto de sincronização, se o código não estiver no segmento principal, e evitar que esse seja um benefício dessa resposta.
Home

1

Cheguei aqui no google porque estava procurando uma boa maneira de fazer coisas no thread awaitda interface do usuário depois de estar dentro de uma chamada Task.Run - Usando o código a seguir, você pode usar para voltar ao thread da interface do usuário novamente.

Espero que isso ajude alguém.

public static class UI
{
    public static DispatcherAwaiter Thread => new DispatcherAwaiter();
}

public struct DispatcherAwaiter : INotifyCompletion
{
    public bool IsCompleted => Application.Current.Dispatcher.CheckAccess();

    public void OnCompleted(Action continuation) => Application.Current.Dispatcher.Invoke(continuation);

    public void GetResult() { }

    public DispatcherAwaiter GetAwaiter()
    {
        return this;
    }
}

Uso:

... code which is executed on the background thread...
await UI.Thread;
... code which will be run in the application dispatcher (ui thread) ...

Muito esperto! Bastante pouco intuitivo. Eu sugiro fazer statica aula UI.
Theodor Zoulias
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.