问题
I'm trying to run an "async" method from an ordinary method:
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
}
The exception thrown is:
Start may not be called on a continuation task.
What does that mean, anyway? How can I simply run my async method on a background thread, dispatch the result back to the UI thread?
Edit
Also tried Task.Wait
, but the waiting never ends:
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();
}
回答1:
To fix your example specifically:
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());
}
This will work, but it's old-school.
The modern way to run something on a background thread and dispatch back to UI thread is to use Task.Run()
, async
, and await
:
async void Activate()
{
Prop = await Task.Run(() => GetSomething());
}
Task.Run
will start something in a thread pool thread. When you await
something, it automatically comes back in on the execution context which started it. In this case, your UI thread.
You should generally never need to call Start()
. Prefer async
methods, Task.Run
, and Task.Factory.StartNew
-- all of which start the tasks automatically. Continuations created with await
or ContinueWith
are also started automatically when their parent completes.
回答2:
WARNING about using FromCurrentSynchronizationContext:
Ok, Cory knows how to make me rewrite answer:).
So the main culprit is actually the FromCurrentSynchronizationContext! Any time StartNew or ContinueWith runs on this kind scheduler, it runs on the UI Thread. One may think:
OK, let's start subsequent operations on UI, change some controls, spawn some operations. But from now TaskScheduler.Current is not null and if any control has some events, that spawn some StartNew expecting to be running on ThreadPool, then from there it goes wrong. UI aps are usually complex, unease to maintain certainty, that nothing will call another StartNew operation, simple example here:
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");
}
}
- Form1:ThreadUI! - OK
- TestA.Begin:ThreadUI! - OK
- TestA.End:ThreadUI! - OK
- Form1:ThreadUI! - OK
- TestA.StartNew:ThreadPool - OK
- TestA.ContinuWith:ThreadUI! - OK
- TestB.Begin:ThreadUI! - OK
- TestB.End:ThreadUI! - OK
- TestB.StartNew - expected ThreadPool:ThreadUI! - COULD BE UNEXPECTED!
- TestB.ContinueWith1 should be ThreadPool:ThreadUI! - COULD BE UNEXPECTED!
- TestB.ContinueWith2:ThreadUI! - OK
Please notice, that tasks returned by:
- async method,
- Task.Fatory.StartNew,
- Task.Run,
can not be started! They are already hot tasks...
来源:https://stackoverflow.com/questions/20304258/run-async-method-on-a-background-thread