All about Async/Await, System.Threading.Tasks, System.Collections.Concurrent, System.Linq, and more…
“What does Task.Wait do?”
Simple question, right? At a high-level, yes, the method achieves what its name implies, preventing the current thread from making forward progress past the call to Wait until the target Task has completed, one way or another. If the Task ran to completion, Wait will return successfully. If the Task completed due to faulting or being canceled, it will throw an exception indicating the relevant details (since the waiting code likely expects that all work to be completed by the task ran successfully if Wait returns successfully).
The details are a bit more interesting. Wait could simply block on some synchronization primitive until the target Task completed, and in some cases that’s exactly what it does. But blocking threads is an expensive venture, in that a thread ties up a good chunk of system resources, and a blocked thread is dead weight until it’s able to continue executing useful work. Instead, Wait prefers to execute useful work rather than blocking, and it has useful work at its finger tips: the Task being waited on. If the Task being Wait’d on has already started execution, Wait has to block. However, if it hasn’t started executing, Wait may be able to pull the target task out of the scheduler to which it was queued and execute it inline on the current thread.
To do that, Wait asks for assistance from the target scheduler, by making a call to the TaskScheduler’s TryExecuteTaskInline method. TryExecuteTaskInline can do whatever logic the scheduler needs in order to validate that the current context is acceptable for inlining the Task. If it is, the scheduler tries to execute the task then and there as part of the call to TryExecuteTaskInline (using the base TaskScheduler’s TryExecuteTask method) and returns whether the Task could be executed. If it couldn’t be executed in TryExecuteTaskInline, the scheduler returns false, and Wait will need to block until the Task completes. A Task may not be able to be executed if it’s already been or is currently being executed elsewhere. As an example of this, the default scheduler (based on the ThreadPool) is aggressive about inlining, but only if the task can be efficiently removed from the data structures that hold tasks internally in the ThreadPool (such as if the task is living in the local queue associated with the thread attempting to inline it).
This is also very similar to what happens when Task.RunSynchronously is used to execute a Task (RunSynchronously may be used instead of Task.Start to run a Task currently in the TaskStatus.Created state). RunSynchronously results in the Framework calling the target scheduler’s TryExecuteTaskInline, allowing the scheduler to decide whether to inline or not. If the scheduler chooses not to inline, the Task will instead be queued to the scheduler, and the system will block until the Task is completed.
Of course, schedulers may want to behave differently depending on whether the code is explicitly choosing to inline (e.g. RunSynchronously) or whether the inlining is happening implicitly (e.g. Wait). To support this difference, in addition to accepting as a parameter the Task to be inlined, TryExecuteTaskInline also accepts as a parameter a Boolean taskWasPreviouslyQueued. This value will be false if it is known that the specified task has not yet been submitted to the scheduler, and true otherwise. If RunSynchronously is being used, taskWasPreviouslyQueued will be false; if Wait, it will be true. The default scheduler does, in fact, differentiate between these two cases, in that it always supports explicit inlining through RunSynchronously.
I am wondering if we can set the thread affinity.
There's no built-in support for thread affinity in the default scheduler, which executes using ThreadPool threads. However, you could write your own TaskScheduler that provides thread affinity support.
Doesnt Task.wait behave like synchronous call. What's the advantage of using Task.Wait over synchronous?
Task.Wait does not return until the task has completed, and in that sense it is a synchronous call. However, you don't have to use wait immediately after launching a task... you can launch a task and then later wait on it, and this enables multiple scenarios, like parallel execution, ensuring all asynchronously launched work has shutdown, etc. In other words, you can do other things between the time you launch the task asynchronously and the time you block waiting for it to complete.
Wow! Didn't know that... Thanks Steve
Could I use TryExecuteTaskInline or RunSynchronously instead of thread affinity?
@Kyle Chang: What are you trying to accomplish?
@ Stephen Toub - MSFT:My first step is to create a task for downloading a file from internet. My second step is to create a task for decoding the file from the first step. These two steps will be executed repeatedly with different files. If the first step take TaskCreationOptions.LongRunning, how should I make sure that these two tasks could share the same cache to speed up?
@Kyle Chang: Thanks for the additional details. Note that just because two things run on the same OS thread doesn't mean they're going to run on the same core or share a cache; the OS could move the thread to a different core. Additionally, just because the data was downloaded by code running on one thread doesn't mean that the data from the file will still be in the processor's cache. And if you're downloading data, you probably want to be using asynchronous I/O rather than synchronous I/O and blocking the thread waiting for more data to arrive. But, if your question is just about getting the two tasks to run on the same thread, that would very likely happen if you create the second task as a continuation of the first and use the ExecuteSynchronously option on the second task; it's not 100% guaranteed that it'll run on the same thread as the first task (e.g. if the first task completes before you call ContinueWith), but it's fairly likely. That doesn't say anything, though, about what core the thread must run on. If you really need athread affinitized to a particular core, you'll need to do so manually using the threading APIs, at which point you'd probably want to create a custom TaskScheduler to wrap that affinitized thread and then schedule any tasks you want on that thread to that TaskScheduler. Hope that helps.
@Stephen Toub - MSFT:
Thank you very much. It is helpful.
Could successful Inlining make sure two tasks share the same core, thread, and cache?
@Kyle Chang: Inlining can make it very likely that two tasks will use the same thread, but it's not guaranteed, since there's no guarantee that inlining will be successful. Even if it is successful, unless you affinitize the thread to a particular core, there's no guarantee that the thread will remain on the same core as it processes the two tasks.
@Stephen Toub - MSFT:
Could I conclude that the only benefit of successful inline is to reduce blocking the system resource?
@Kyle Chang: Yes, in general inlining is about reducing consumption of system resources, which includes blocking of threads unnecessarily. Such blocking avoidance can then have secondary benefits, like avoidance of potential deadlocks, e.g. if you have a fixed number of threads on which to run tasks and they're all blocked waiting for tasks to complete but those tasks can't complete until the threads become available.
Do await support inline?
@Kyle Chang: The continuation created by the await will by default be executed synchronously if possible when the awaited task completes.