Run an async function in another thread
Asked Answered
K

3

20

I'm evaluating the Async CTP.

How can I begin execution of an async function on another thread pool's thread?

static async Task Test()
{
    // Do something, await something
}

static void Main( string[] args )
{
    // Is there more elegant way to write the line below?
    var t = TaskEx.Run( () => Test().Wait() );

    // Doing much more in this same thread
    t.Wait(); // Waiting for much more then just this single task, this is just an example
}
Krypton answered 11/2, 2011 at 19:52 Comment(0)
P
52

I'm new (my virginal post) to Stack Overflow, but I'm jazzed that you're asking about the Async CTP since I'm on the team working on it at Microsoft :)

I think I understand what you're aiming for, and there's a couple of things you're doing correctly, to get you there.

What I think you want:

static async Task Test()
{
    // Do something, await something
}

static void Main(string[] args)
{
    // In the CTP, use Task.RunEx(...) to run an Async Method or Async Lambda
    // on the .NET thread pool
    var t = TaskEx.RunEx(Test);
    // the above was just shorthand for
    var t = TaskEx.RunEx(new Func<Task>(Test));
    // because the C# auto-wraps methods into delegates for you.

    // Doing much more in this same thread
    t.Wait(); // Waiting for much more then just this single task, this is just an example
}

Task.Run vs. Task.RunEx

Because this CTP installs on top of .NET 4.0, we didn't want to patch the actual System.Threading.Tasks.Task type in mscorlib. Instead, the playground APIs are named FooEx when they conflicted.

Why did we name some of them Run(...) and some of the RunEx(...)? The reason is because of redesigns in method overloading that we hadn't completed yet by the time we released the CTP. In our current working codebase, we've actually had to tweak the C# method overloading rules slightly so that the right thing happens for Async Lambdas - which can return void, Task, or Task<T>.

The issue is that when async method or lambdas return Task or Task<T>, they actually don't have the outer task type in the return expression, because the task is generated for you automatically as part of the method or lambda's invocation. This strongly seems to us like the right experience for code clarity, though that does make things quite different before, since typically the expression of return statements is directly convertible to the return type of the method or lambda.

So thus, both async void lambdas and async Task lambdas support return; without arguments. Hence the need for a clarification in method overload resolution to decide which one to pick. Thus the only reason why you have both Run(...) and RunEx(...) was so that we would make sure to have higher quality support for the other parts of the Async CTP, by the time PDC 2010 hit.


How to think about async methods/lambdas

I'm not sure if this is a point of confusion, but I thought I'd mention it - when you are writing an async method or async lambda, it can take on certain characteristics of whoever is invoking it. This is predicated on two things:

  • The type on which you are awaiting
  • And possibly the synchronization context (depending on above)

The CTP design for await and our current internal design are both very pattern-based so that API providers can help flesh out a vibrant set of things that you can 'await' on. This can vary based on the type on which you're awaiting, and the common type for that is Task.

Task's await implementation is very reasonable, and defers to the current thread's SynchronizationContext to decide how to defer work. In the case that you're already in a WinForms or WPF message loop, then your deferred execution will come back on the same message loop (as if you used BeginInvoke() the "rest of your method"). If you await a Task and you're already on the .NET threadpool, then the "rest of your method" will resume on one of the threadpool threads (but not necessarily the same one exactly), since they were pooled to begin with and most likely you're happy to go with the first available pool thread.


Caution about using Wait() methods

In your sample you used: var t = TaskEx.Run( () => Test().Wait() );

What that does is:

  1. In the surrounding thread synchronously call TaskEx.Run(...) to execute a lambda on the thread pool.
  2. A thread pool thread is designated for the lambda, and it invokes your async method.
  3. The async method Test() is invoked from the lambda. Because the lambda was executing on the thread pool, any continuations inside Test() can run on any thread in the thread pool.
  4. The lambda doesn't actually vacate that thread's stack because it had no awaits in it. The TPL's behavior in this case depends on if Test() actually finished before the Wait() call. However, in this case, there's a real possibility that you will be blocking a thread pool thread while it waits for Test() to finish executing on a different thread.

That's the primary benefit of the 'await' operator is that it allows you to add code that executes later - but without blocking the original thread. In the thread pool case, you can achieve better thread utilization.

Let me know if you have other questions about the Async CTP for VB or C#, I'd love to hear them :)

Philan answered 12/2, 2011 at 13:39 Comment(7)
welcome to Stack Overflow, and thanks for excellent explanation! BTW I'm creating a highly-parallel cluster to serve 200k concurrent users using 100GB distributed DB. In my current design prototype, cluster nodes are communicating with each others (and with clients) using WCF over duplex net.tcp, and single call from the client can result in up to 3 calls between different cluster nodes. Currently, I'm evaluating the Async CTP, because I think it'll dramatically simplify our code leading to much cleaner implementation, compared to BeginXxx/EndXxx approach.Krypton
could you please take a look at my recent question on the subject? Thanks in advance!Krypton
thanks for the wonderful and concise explanation! Within a few paragraphs, you clarified a number of confusion points many people have with async/await -- namely where continuations run. My question is: if the thread calling await is the main thread of a console application (without a message pump), then will the continuation preempt the main thread? And at which point? If so, do we need to handle concurrency concerns?Drennan
I'm not 100% sure what you mean by pre-empt in the Main thread. By default, console applications have a null synchronization context pre-installed, which will cause the Task awaiter to schedule continuations to the threadpool, since the threadpool is capable of receiving scheduled work.Philan
This does cause your console app code to run on other threads, however, one of the big values of Async in .NET is that the order of evaluation is still respected at least within your async method. For example, if your method has multiple awaits, you don't have to worry about us suddenly parallelizing those parts of your method. Parallel execution is certainly supported, but we still require for you to code it explicitly.Philan
Note, that .NET/Windows Main methods are still an inherently synchronous contract. It is your responsibility to make sure that the Main() entry point is blocked until your app is effectively done executing. In other words, if you invoke an async method on the console app main thread, its continuations go to the thread pool, but you can still call .Wait() or .Result on the async methods to block until they fully complete (regardless if they are executing on the thread pool).Philan
Holy... This is what I was missing when I read the whitepapers and articles. Thanks.Kissel
W
6

It's usually up to the method returning the Task to determine where it runs, if it's starting genuinely new work instead of just piggy-backing on something else.

In this case it doesn't look like you really want the Test() method to be async - at least, you're not using the fact that it's asynchronous. You're just starting stuff in a different thread... the Test() method could be entirely synchronous, and you could just use:

Task task = TaskEx.Run(Test);
// Do stuff
t.Wait();

That doesn't require any of the async CTP goodness.

Wrestling answered 11/2, 2011 at 20:1 Comment(3)
TaskEx.Run(Test) doesn't even compile when Test() is an async method.Krypton
@Soonts: It should if it's a method returning void (which it can be)... but my suggestion is that it shouldn't be asynchronous in the first place.Wrestling
If it's a method returning void, then "t.Wait()" doesn't wait, and only the first portion of Test() body (the code before the first await keyword) is executed.Krypton
T
2

There would be, if this wasn't a console application. For example, if you do this in a Windows Forms application, you could do:

// Added to a button click event, for example
public async void button1_Click(object sender, EventArgs e)
{
    // Do some stuff
    await Test();
    // Do some more stuff
}

However, there is no default SynchronizationContext in a console, so that won't work the way you'd expect. In a console application, you need to explicitly grab the task and then wait at the end.

If you're doing this in a UI thread in Windows Forms, WPF, or even a WCF service, there will be a valid SynchronizationContext that will be used to marshal back the results properly. In a console application, however, when control is "returned" at the await call, the program continues, and just exits immediately. This tends to mess up everything, and produce unexpected behavior.

Tahr answered 11/2, 2011 at 19:58 Comment(4)
I'm using console application just for tests. I'm currently designing a highly-scalable cluster (where single WCF call from the client can result to up to 3 WCF calls between different cluster nodes), and I'm not going to ruin the scalability by spawning my own threads instead of just posting tasks to the CLR thread pool.Krypton
@Snoonts: In WCF, you'll have a synchronization context setup, so my code would work fine...Tahr
could you please tell more? I need to post the task in the thread pool and continue running. If I just write e.g. Task t = Test(); then the first portion of the task will be executed on the same thread, and I don't like that. I need the entire task to be executed by the thread pool, including the first portion of the task (before the first await keyword).Krypton
@Snoots: If you use my code above, and your "Task" is wrapped up in the Test method, everything inside Test will run in the background.Tahr

© 2022 - 2024 — McMap. All rights reserved.