76

Trying to understanding the difference between the TPL & async/await when it comes to thread creation.

I believe the TPL (TaskFactory.StartNew) works similar to ThreadPool.QueueUserWorkItem in that it queues up work on a thread in the thread pool. That's of course unless you use TaskCreationOptions.LongRunning which creates a new thread.

I thought async/await would work similarly so essentially:

TPL:

Factory.StartNew( () => DoSomeAsyncWork() )
.ContinueWith( 
    (antecedent) => {
        DoSomeWorkAfter(); 
    },TaskScheduler.FromCurrentSynchronizationContext());

Async/Await:

await DoSomeAsyncWork();  
DoSomeWorkAfter();

would be identical. From what I've been reading it seems like async/await only "sometimes" creates a new thread. So when does it create a new thread and when doesn't it create a new thread? If you were dealing with IO completion ports i can see it not having to create a new thread but otherwise I would think it would have to. I guess my understanding of FromCurrentSynchronizationContext always was a bit fuzzy also. I always throught it was, in essence, the UI thread.

Francesco B.
  • 2,729
  • 4
  • 25
  • 37
coding4fun
  • 8,038
  • 13
  • 58
  • 85
  • 4
    Actually, TaskCreationOptions.LongRunning does not guarantee a "new thread". Per MSDN, *the "LongRunning" option only provides a hint to the scheduler; it does not guarantee a dedicated thread.* I found that out the hard way. – eduncan911 Nov 03 '12 at 00:50
  • @eduncan911 although what you say about the documentation is correct, I looked up the TPL source code a while back and I'm pretty sure that in fact a new dedicated thread is always created when `TaskCreationOptions.LongRunning` is specified. – Zaid Masud Feb 27 '13 at 15:15
  • @ZaidMasud: You may want to take another look. I know it was pooling the threads because `Thread.CurrentThread.IsThreadPoolThread` was returning true for short-running threads of a few hundred milliseconds. not to mention the ThreadStatic variables I was using bleeding into multiple threads, causing all sorts of havok. I had to force my code to new up multiple Thread()s, the old-school way, to guarantee a dedicated thread. In other words, I could not use the TaskFactory for dedicated threads. Optionally, you could implement your own `TaskScheduler` that always returns a dedicated thread. – eduncan911 Mar 11 '13 at 17:49

3 Answers3

90

I believe the TPL (TaskFactory.Startnew) works similar to ThreadPool.QueueUserWorkItem in that it queues up work on a thread in the thread pool.

Pretty much.

From what i've been reading it seems like async/await only "sometimes" creates a new thread.

Actually, it never does. If you want multithreading, you have to implement it yourself. There's a new Task.Run method that is just shorthand for Task.Factory.StartNew, and it's probably the most common way of starting a task on the thread pool.

If you were dealing with IO completion ports i can see it not having to create a new thread but otherwise i would think it would have to.

Bingo. So methods like Stream.ReadAsync will actually create a Task wrapper around an IOCP (if the Stream has an IOCP).

You can also create some non-I/O, non-CPU "tasks". A simple example is Task.Delay, which returns a task that completes after some time period.

The cool thing about async/await is that you can queue some work to the thread pool (e.g., Task.Run), do some I/O-bound operation (e.g., Stream.ReadAsync), and do some other operation (e.g., Task.Delay)... and they're all tasks! They can be awaited or used in combinations like Task.WhenAll.

Any method that returns Task can be awaited - it doesn't have to be an async method. So Task.Delay and I/O-bound operations just use TaskCompletionSource to create and complete a task - the only thing being done on the thread pool is the actual task completion when the event occurs (timeout, I/O completion, etc).

I guess my understanding of FromCurrentSynchronizationContext always was a bit fuzzy also. I always throught it was, in essence, the UI thread.

I wrote an article on SynchronizationContext. Most of the time, SynchronizationContext.Current:

  • is a UI context if the current thread is a UI thread.
  • is an ASP.NET request context if the current thread is servicing an ASP.NET request.
  • is a thread pool context otherwise.

Any thread can set its own SynchronizationContext, so there are exceptions to the rules above.

Note that the default Task awaiter will schedule the remainder of the async method on the current SynchronizationContext if it is not null; otherwise it goes on the current TaskScheduler. This isn't so important today, but in the near future it will be an important distinction.

I wrote my own async/await intro on my blog, and Stephen Toub recently posted an excellent async/await FAQ.

Regarding "concurrency" vs "multithreading", see this related SO question. I would say async enables concurrency, which may or may not be multithreaded. It's easy to use await Task.WhenAll or await Task.WhenAny to do concurrent processing, and unless you explicitly use the thread pool (e.g., Task.Run or ConfigureAwait(false)), then you can have multiple concurrent operations in progress at the same time (e.g., multiple I/O or other types like Delay) - and there is no thread needed for them. I use the term "single-threaded concurrency" for this kind of scenario, though in an ASP.NET host, you can actually end up with "zero-threaded concurrency". Which is pretty sweet.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
9

async / await basically simplifies the ContinueWith methods ( Continuations in Continuation Passing Style )

It does not introduce concurrency - you still have to do that yourself ( or use the Async version of a framework method. )

So, the C# 5 version would be:

await Task.Run( () => DoSomeAsyncWork() );
DoSomeWorkAfter();
Nick Butler
  • 24,045
  • 4
  • 49
  • 70
  • so where is it running DoSomeAsyncWork (async/wait version) in my example above? If its running on the UI thread how does it not block? – coding4fun Apr 23 '12 at 17:35
  • 1
    Your await example won't compile if `DoSomeWorkAsync()` returns void or something that isn't awaitable. From your first example, I assumed that it is a sequential method that you want to run on a different thread. If you changed it to return a `Task`, without introducing concurrency, then yes it would block. In the sense that it would execute sequentially and be just like normal code on the UI thread. `await` only yields if the method returns an awaitable that hasn't yet completed. – Nick Butler Apr 23 '12 at 17:49
  • well, I wouldn't say it runs wherever it chooses to run. You've used Task.Run to execute the code in DoSomeAsyncWork, so in this case your work will be done on a threadpool thread. – Michael Ray Lovett Nov 09 '12 at 16:26
  • I loved the succinctness of your answer. – RBT Mar 05 '20 at 03:19
1

so, it's 2023 and seems like async/await is still not universally well-understood, as i just went through an effort to train a bunch of folks recently... i saw this SO Q & A and felt i needed to call out a correction from a million years ago.

@Nick Butler - apologies as i see you're a well respected SO member, but need to make a correction to your 11 year old comment, so those less-expert aren't confused ;)

Nick writes:

await Task.Run( () => DoSomeAsyncWork() );
DoSomeWorkAfter();

This should (sorta) actually be:

await Task.Run(async() => await DoSomeAsyncWorkAsync() ); // optional: .ConfigureAwait(false)
DoSomeWorkAfter();

a couple of notes:

  1. in the initial answer, it's not obvious that DoSomeAsyncWork() actually returns an 'awaitable' (a Task, for intent/purpsoses); obviously, your IDE and compiler would very quickly tell you what the method in fact returns... but human coders can't just look at that and know, which is why...
  2. Microsoft has created/recommended the suffix of 'Async' be appended to the method's 'natural' name. NOTE: this is not required, rather highly recommended for human dev's to more clearly see what the intent of the API is offering (without having to
  3. the previously offered lambda is intrinsically synchronous, even though it's presumed that a Task returned from the nested method is being returned by the lambda. lambdas can be confusing b/c you need to cancel-out the work that lambdas save you (at the expense of runtime inefficiencies) and make your, the human, manually verify/understand the return type of DoSomeAsyncWork() - this speaks to the rationale MS has for note #2. MS also recommends to always 'observe' (either 'await', keep a reference to, or 'discard') Tasks returned by any method (there's a static analyzer NuGet package to look for async-await anti-patterns that can help out with all this stuff) the 'async' modifier to the lamba allows for the 'await'-ing of the asynchronous (Task returning) method.
  4. ConfigureAwait(false)... https://devblogs.microsoft.com/dotnet/configureawait-faq/

i hope this is helpful to at least one person. i know @Nick Butler wrote his answer a long time ago, well before i even fully understood this stuff. i just wanted to offer better guidance/clarity for those seeking it in 2023+.

isandburn
  • 134
  • 5