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(); }
No, it does not. It MAY start another thread internally and return that task, but the general idea is that it does not run on any thread.
init , the asynchronous code runs on the current actor, which, in this context is the main actor. This has the result of blocking the main thread. By calling Task.
You can start running a Task using Task. Run(Action action) . This will queue up the Task on the thread pool, which will run in the background on a different thread. The thread pool takes a queue of tasks, and assigns them to CPU threads for processing.
However, just to address "Call an async method in C# without await", you can execute the async method inside a Task. Run . This approach will wait until MyAsyncMethod finish. await asynchronously unwraps the Result of your task, whereas just using Result would block until the task had completed.
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.
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"); } }
Please notice, that tasks returned by:
can not be started! They are already hot tasks...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With