Continuación de tarea en subproceso de interfaz de usuario

214

¿Existe una forma 'estándar' de especificar que una continuación de la tarea debe ejecutarse en el hilo desde el que se creó la tarea inicial?

Actualmente tengo el siguiente código: funciona, pero hacer un seguimiento del despachador y crear una segunda acción parece una sobrecarga innecesaria.

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

Task UITask= task.ContinueWith(() =>
{
    dispatcher.Invoke(new Action(() =>
    {
        this.TextBlock1.Text = "Complete"; 
    }
});
Greg Sansom
fuente
En el caso de su ejemplo, podría usar Control.Invoke(Action), es decir. TextBlock1.Invokeen lugar dedispatcher.Invoke
Coronel Panic
2
Gracias @ColonelPanic, pero estaba usando WPF (como etiquetado), no winforms.
Greg Sansom

Respuestas:

352

Llame a la continuación con TaskScheduler.FromCurrentSynchronizationContext():

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

Esto es adecuado solo si el contexto de ejecución actual está en el hilo de la interfaz de usuario.

Greg Sansom
fuente
39
Es válido solo si el contexto de ejecución actual está en el hilo de la IU. Si coloca este código dentro de otra Tarea, obtendrá InvalidOperationException (consulte la sección Excepciones )
stukselbax
3
En .NET 4.5, la respuesta de Johan Larsson debe usarse como forma estándar para la continuación de una tarea en el hilo de la interfaz de usuario. Simplemente escriba: aguarde Task.Run (DoLongRunningWork); this.TextBlock1.Text = "Completo"; Ver también: blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Marcel W
1
Gracias por salvarme la vida. Paso horas para descubrir cómo llamar a las cosas del hilo principal dentro de la espera / ContinueWith. Para todos los demás, ¿cómo está usando Google Firebase SDK para Unity y todavía tiene los mismos problemas? Este es un enfoque funcional.
CHAP
2
@MarcelW: awaites un buen patrón, pero solo si está dentro de un asynccontexto (como un método declarado async). Si no, aún es necesario hacer algo como esta respuesta.
ToolmakerSteve
33

Con async solo haces:

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);

Sin embargo:

await Task.Run(() => do some stuff).ConfigureAwait(false);
// continue doing stuff on the same thread as the task finished on.
Johan Larsson
fuente
2
El comentario en la falseversión me confunde. Pensé que falsesignifica que podría continuar en un hilo diferente .
ToolmakerSteve
1
@ToolmakerSteve Depende de qué hilo estás pensando. ¿El subproceso de trabajo utilizado por Task.Run o el subproceso de llamada? Recuerde, "mismo subproceso en el que finalizó la tarea" significa el subproceso de trabajo (evitando 'cambiar' entre subprocesos). Además, ConfigureAwait (verdadero) no garantiza que el control regrese al mismo hilo , solo al mismo contexto (aunque la distinción puede no ser significativa).
Max Barraclough
@MaxBarraclough - Gracias, leí mal el significado del "mismo hilo". evitando cambiar entre hilos en el sentido de maximizar el rendimiento utilizando cualquier hilo que se esté ejecutando [para realizar la tarea "hacer algunas cosas"], eso me lo aclara.
ToolmakerSteve
1
La pregunta no especifica estar dentro de un asyncmétodo (que es necesario usar await). ¿Cuál es la respuesta cuando awaitno está disponible?
ToolmakerSteve
22

Si tiene un valor de retorno que necesita enviar a la interfaz de usuario, puede usar la versión genérica como esta:

Esto se llama desde un MVVM ViewModel en mi 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
fuente
Supongo que el = antes de GenerateManifest es un error tipográfico.
Sebastien F.
Sí, se ha ido ahora! Gracias.
Simon_Weaver
11

Solo quería agregar esta versión porque es un hilo muy útil y creo que es una implementación muy simple. Lo he usado varias veces en varios tipos de aplicaciones multiproceso:

 Task.Factory.StartNew(() =>
      {
        DoLongRunningWork();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
              { txt.Text = "Complete"; }));
      });
Decano
fuente
2
No hacer downvoting ya que esta es una solución viable en algunos escenarios; Sin embargo, la respuesta aceptada es mucho mejor. Es independiente de la tecnología ( TaskScheduleres parte de BCL, Dispatcherno lo es) y se puede usar para componer cadenas complejas de tareas debido a que no tiene que preocuparse por las operaciones asíncronas de disparar y olvidar (como BeginInvoke).
Kirill Shlenskiy
@Kirill puede expandirse un poco, porque algunos subprocesos SO han declarado por unanimidad que el despachador es el método correcto si se utiliza WPF de WinForms: se puede invocar una actualización de la GUI de forma asincrónica (usando BeginInvoke) o sincrónicamente (Invoke), aunque generalmente es asíncrono se usa porque uno no querría bloquear un subproceso en segundo plano solo para una actualización de la GUI. ¿FromCurrentSynchronizationContext no coloca la tarea de continuación en la cola de mensajes del hilo principal de la misma manera que el despachador?
Dean
1
Correcto, pero el OP ciertamente pregunta acerca de WPF (y lo etiquetó así), y no quiere mantener una referencia a ningún despachador (y supongo que tampoco hay contexto de sincronización; solo puede obtener esto desde el hilo principal y debe almacenar una referencia a ella en alguna parte). Es por eso que me gusta la solución que publiqué: hay una referencia estática segura para subprocesos incorporada que no requiere nada de esto. Creo que esto es extremadamente útil en el contexto de WPF.
Dean
3
Solo quería reforzar mi último comentario: el desarrollador no solo tiene que almacenar el contexto de sincronización, sino que debe saber que solo está disponible desde el hilo principal; Este problema ha sido causa de confusión en docenas de preguntas SO: la gente siempre trata de obtener eso del hilo del trabajador. Si su código se ha movido a un subproceso de trabajo, falla debido a este problema. Entonces, debido a la prevalencia de WPF, esto definitivamente debería aclararse aquí en esta pregunta popular.
Dean
1
... sin embargo, es importante tener en cuenta la observación de Dean sobre [la respuesta aceptada] que necesita realizar un seguimiento del contexto de sincronización si el código no está en el hilo principal, y evitar eso es un beneficio de esta respuesta.
ToolmakerSteve
1

Llegué a través de google porque estaba buscando una buena manera de hacer cosas en el hilo de la interfaz de usuario después de estar dentro de una llamada de Task.Run: usando el siguiente código que puede usar awaitpara volver al hilo de la interfaz de usuario nuevamente.

Espero que esto ayude a alguien.

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
fuente
¡Muy inteligente! Aunque poco intuitivo. Sugiero hacer staticla clase UI.
Theodor Zoulias