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"; 
    }
});
Greg Sansom
fonte
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.

Greg Sansom
fonte
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.
Johan Larsson
fonte
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());
Simon_Weaver
fonte
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"; }));
      });
reitor
fonte
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) ...
Dbl
fonte
Muito esperto! Bastante pouco intuitivo. Eu sugiro fazer statica aula UI.
Theodor Zoulias