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();
}
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.
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");
}
}
Veuillez noter que les tâches retournées par:
ne peut pas être démarré! Ce sont déjà des tâches brûlantes ...