web-dev-qa-db-fra.com

Exécutez la méthode "async" sur un thread d'arrière-plan

J'essaie d'exécuter une méthode "async" à partir d'une méthode ordinaire:

public string Prop
{
    get { return _prop; }
    set
    {
        _prop = value;
        RaisePropertyChanged();
    }
}

private async Task<string> GetSomething()
{
    return await new Task<string>( () => {
        Thread.Sleep(2000);
        return "hello world";
    });
}

public void Activate()
{
    GetSomething.ContinueWith(task => Prop = task.Result).Start();
    // ^ exception here
}

L'exception levée est:

Le démarrage ne peut pas être appelé sur une tâche de continuation.

Qu'est-ce que cela veut dire de toute façon? Comment puis-je simplement exécuter ma méthode asynchrone sur un thread d'arrière-plan, renvoyer le résultat au thread d'interface utilisateur?

Modifier

A également essayé Task.Wait, mais l'attente ne se termine jamais:

public void Activate()
{
    Task.Factory.StartNew<string>( () => {
        var task = GetSomething();
        task.Wait();

        // ^ stuck here

        return task.Result;
    }).ContinueWith(task => {
        Prop = task.Result;
    }, TaskScheduler.FromCurrentSynchronizationContext());
    GetSomething.ContinueWith(task => Prop = task.Result).Start();
}
23
McGarnagle

Pour corriger votre exemple spécifiquement:

public void Activate()
{
    Task.Factory.StartNew(() =>
    {
        //executes in thread pool.
        return GetSomething(); // returns a Task.
    }) // returns a Task<Task>.
    .Unwrap() // "unwraps" the outer task, returning a proxy
              // for the inner one returned by GetSomething().
    .ContinueWith(task =>
    {
        // executes in UI thread.
        Prop = task.Result;
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

Cela fonctionnera, mais c'est de la vieille école.

La manière moderne d'exécuter quelque chose sur un thread d'arrière-plan et de le renvoyer au thread d'interface utilisateur consiste à utiliser Task.Run(), async et await:

async void Activate()
{
    Prop = await Task.Run(() => GetSomething());
}

Task.Run Démarrera quelque chose dans un thread de pool de threads. Lorsque vous await quelque chose, il revient automatiquement dans le contexte d'exécution qui l'a démarré. Dans ce cas, votre thread d'interface utilisateur.

Vous ne devriez généralement jamais avoir besoin d'appeler Start(). Préférez les méthodes async, Task.Run Et Task.Factory.StartNew - qui démarrent tous les tâches automatiquement. Les suites créées avec await ou ContinueWith sont également démarrées automatiquement lorsque leur parent se termine.

38
Cory Nelson

AVERTISSEMENT sur l'utilisation de FromCurrentSynchronizationContext:

Ok, Cory sait comment me faire réécrire la réponse :).

Donc, le principal coupable est en fait le FromCurrentSynchronizationContext! Chaque fois que StartNew ou ContinueWith s'exécute sur ce type de planificateur, il s'exécute sur le thread d'interface utilisateur. On peut penser:

OK, commençons les opérations suivantes sur l'interface utilisateur, modifions certains contrôles, générons certaines opérations. Mais à partir de maintenant, TaskScheduler.Current n'est pas nul et si un contrôle a des événements, qui engendrent des StartNew qui s'attendent à être exécutés sur ThreadPool, alors à partir de là, ça tourne mal. Les Aps UI sont généralement complexes, mal à l'aise pour maintenir la certitude, que rien n'appellera une autre opération StartNew, exemple simple ici:

public partial class Form1 : Form
{
    public static int Counter;
    public static int Cnt => Interlocked.Increment(ref Counter);
    private readonly TextBox _txt = new TextBox();
    public static void WriteTrace(string from) => Trace.WriteLine($"{Cnt}:{from}:{Thread.CurrentThread.Name ?? "ThreadPool"}");

    public Form1()
    {
        InitializeComponent();
        Thread.CurrentThread.Name = "ThreadUI!";

        //this seems to be so Nice :)
        _txt.TextChanged += (sender, args) => { TestB(); };

        WriteTrace("Form1"); TestA(); WriteTrace("Form1");
    }
    private void TestA()
    {
        WriteTrace("TestA.Begin");
        Task.Factory.StartNew(() => WriteTrace("TestA.StartNew"))
        .ContinueWith(t =>
        {
            WriteTrace("TestA.ContinuWith");
            _txt.Text = @"TestA has completed!";
        }, TaskScheduler.FromCurrentSynchronizationContext());
        WriteTrace("TestA.End");
    }
    private void TestB()
    {
        WriteTrace("TestB.Begin");
        Task.Factory.StartNew(() => WriteTrace("TestB.StartNew - expected ThreadPool"))
        .ContinueWith(t => WriteTrace("TestB.ContinueWith1 should be ThreadPool"))
        .ContinueWith(t => WriteTrace("TestB.ContinueWith2"));
        WriteTrace("TestB.End");
    }
}
  1. Form1: ThreadUI! - D'ACCORD
  2. TestA.Begin: ThreadUI! - D'ACCORD
  3. TestA.End: ThreadUI! - D'ACCORD
  4. Form1: ThreadUI! - D'ACCORD
  5. TestA.StartNew: ThreadPool - OK
  6. TestA.ContinuWith: ThreadUI! - D'ACCORD
  7. TestB.Begin: ThreadUI! - D'ACCORD
  8. TestB.End: ThreadUI! - D'ACCORD
  9. TestB.StartNew - ThreadPool attendu: ThreadUI! - POURRAIT ÊTRE INATTENDU!
  10. TestB.ContinueWith1 devrait être ThreadPool: ThreadUI! - POURRAIT ÊTRE INATTENDU!
  11. TestB.ContinueWith2: ThreadUI! - D'ACCORD

Veuillez noter que les tâches retournées par:

  1. méthode asynchrone,
  2. Task.Fatory.StartNew,
  3. Task.Run,

ne peut pas être démarré! Ce sont déjà des tâches brûlantes ...

1
ipavlu