Run "async" method on a background thread
Asked Answered
A

3

44

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();
}
Amberjack answered 30/11, 2013 at 18:51 Comment(0)
H
66

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.

Hostess answered 30/11, 2013 at 18:57 Comment(5)
Thank you, this clears up my confusion. Actually the real source of my pain was an async method with return new Task instead of return Task.Factory.StartNew.Amberjack
Also, be very careful about async void methods, they're usually the wrong way to go.Anglophobe
Please review my answer, Task.Factory.StartNew wont execute on the thread pool. It will just execute asynchronously on the UI thread...Eggert
await does not automatically come back to the thread that started the operation. There is no way to know if an await will result in a separate thread being used, or the executing thread being used (for the continuation). Here's a look at two runs of a section of code that calls GetFileAsync() { Thd Id 28228 LoadBytecodeAsync() started 29420 after GetFileAsync() 29420 LoadBytecodeAsync() Started 29420 after GetFileAsync() } The first call to GetFileAsync spawns a separate thread which runs the rest of the build code, including the second run of that section.Transport
I end up with error of "An asynchronous module or handler completed while an asynchronous operation was still pending." when I try the method proposed here.Tameika
E
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");
    }
}
  1. Form1:ThreadUI! - OK
  2. TestA.Begin:ThreadUI! - OK
  3. TestA.End:ThreadUI! - OK
  4. Form1:ThreadUI! - OK
  5. TestA.StartNew:ThreadPool - OK
  6. TestA.ContinuWith:ThreadUI! - OK
  7. TestB.Begin:ThreadUI! - OK
  8. TestB.End:ThreadUI! - OK
  9. TestB.StartNew - expected ThreadPool:ThreadUI! - COULD BE UNEXPECTED!
  10. TestB.ContinueWith1 should be ThreadPool:ThreadUI! - COULD BE UNEXPECTED!
  11. TestB.ContinueWith2:ThreadUI! - OK

Please notice, that tasks returned by:

  1. async method,
  2. Task.Fatory.StartNew,
  3. Task.Run,

can not be started! They are already hot tasks...

Eggert answered 9/4, 2017 at 3:6 Comment(2)
StartNew and ContinueWith will use the default (thread-pool) scheduler if there is no current Task executing. FromCurrentSynchronizationContext is never the default. Your overall message is correct, though, that if you do not know where your code is executing it is best to be explicit about asking for the default scheduler if that is what you desire.Hostess
OK, you are right. StartNew and ContinueWith go with Current, if null, then Default, so ThreadPool, unless specified like FromCurrentSynchronizationContext. The danger about anything touching FromCurrentSynchronizationContext is that all subsequent code has now Current which is not null, even when ContinueWith is running on UI Thread and touching anything can start another StartNew same one, on different and and it wont run on ThreadPool anymore... Example in the answer...Eggert
H
2

Based on previous answers, I used an async method to run in the background, then the code is slightly different:

_ = Task.Run(async  ()  => await BWSMailLogic.CheckTaskStatus(task, user));

It will rund the CheckTaskStatus in the background. It will immediately continu the process. Be sure to add good exception handling in the method that you run in the background. Since the process doesn't wait for the result, it only makes sense that you do not need the result in the process to follow.

Heiress answered 13/12, 2023 at 9:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.