I've searched a lot of information surrounding this topic and I understand the general premises of:
- Await is a handing off of control from the callee backer to the caller
- Most Modern I/O doesn't use real threading in underlying architecture
- Most async methods do not explicitly spin up their own threads (i.e. Web Requests)
The last bullet in particular is what I want to discuss. To future-proof this let's use an example as a medium for explanation. Let's assume this is the code block:
public async Task<int> LongOperationWithAnInt32ResultAsync(string input)
{
/// Section A
_ = int.TryParse(input, out var parsedInt)
parsedInt = SyncOperationWithAnInt32Result(parsedInt);
/// Section B
await MyCustomTaskThatIWantAwaited();
/// Section C
return parsedInt;
}
private Task MyCustomTaskThatIWantAwaited()
{
/// Section D
AnotherSyncOperationWithVoidResult();
/// Section E
return Task.CompletedTask;
}
The method LongOperationWithAnInt32ResultAsync(string) will perform synchronously even though this is not the intended effect.
This is because when the caller enters the callee at Section B, the code from Section D and Section E are executed immediately and are not awaited. This behavior is changed if, Section D is removed and, Section E was "return Task.Run(() => AnotherSyncOperationWithVoidResult())" instead. In this new Section E, the awaitable being tracked becomes the thread from Task.Run (wrapped with the returned Task).
If you replace Section B with "await Task.Delay(10000);" or "await FunctionalWebRequestAsync();" it works as intended. However, to my knowledge, neither of these internally generate a thread to be followed - so what exactly is being awaited?
I've accepted the main answer because it really helped me understand my misconception on Task functionality, but please also refer to my answer as well. It may be what you're looking for.
CodePudding user response:
so what exactly is being awaited?
Nothing is being awaited. Await means asynchronous wait. For a wait to be asynchronous, the awaitable (the Task
) should not be completed at the await
point. In your case the awaitable is already completed (the IsCompleted
property of the TaskAwaiter
returns true
), so the async state machine grabs immediately its result and proceeds with the next line as usual. There is no reason to pack the current state of the machine, invoke the OnCompleted
method of the awaiter, and hand back an incomplete Task
to the caller.
If you want to offload specific parts of an asynchronous method to the ThreadPool
, the recommended way is to wrap these parts in Task.Run
. Example:
public async Task<int> LongOperationWithAnInt32ResultAsync(string input)
{
/// Section A
_ = int.TryParse(input, out var parsedInt)
parsedInt = await Task.Run(() => SyncOperationWithAnInt32Result(parsedInt));
/// Section B
await Task.Run(async () => await MyCustomTaskThatIWantAwaited());
/// Section C
return parsedInt;
}
If you like the idea of controlling imperatively the thread where the code is running, there is a SwitchTo
extension method available in the Microsoft.VisualStudio.Threading package. Usage example:
await TaskScheduler.Default.SwitchTo(); // Switch to the ThreadPool
The opinion of the experts is to avoid this approach, and stick with the Task.Run
.
CodePudding user response:
Huge thanks to @TheodorZoulias for his explanation and answer as it was critical in me reaching this point.
My misconception was simple, Task (or Task{T}) cannot be used as a delegate. Task is a class through and through, meaning that if you define this:
public Task DoSomeReallyLongWork()
{
SyncTaskThatIsReallyLong();
AnotherSyncTaskThatIsReallyLong();
/* perform as much work as needed here */
return Task.CompletedTask;
}
This will run synchronously and ONLY synchronously. The thing actually being awaited is the Task.CompletedTask object that you returned, nothing else. And since the Task is already completed, the internal awaiter is also marked as completed.
This means that though the intention may have been to wrap multiple methods within a Task and then execute/await it, what's actually happening is that the methods are executing synchronously like any other call and then a completed Task is being returned.
If you want multiple methods to be awaited, this is done by making a new Task Object. Using our previous example:
public async Task DoSomeReallyLongWorkAsync()
{
/// Task.Run does not necessarily run on a separate thread
/// this is up to the scheduler (usually the .NET scheduler)
await Task.Run(LongSyncTasksWrapped);
}
public void LongSyncTasksWrapped()
{
SyncTaskThatIsReallyLong();
AnotherSyncTaskThatIsReallyLong();
/* perform as much work as needed here */
return;
}
There may be instances where you want cold Tasks (task that haven't been started yet) and then run them when needed. Using the previous example, this would be done by:
public async Task DoSomeReallyLongWorkAsync()
{
var coldTask = new Task(LongSyncTasksWrapped);
/// Must call .Start() whenever you want the Task to
/// actually start. Await will not start the Task, its
/// just an asynchronous form of .Wait()
coldTask.Start();
/// coldTask was considered "hot" from .Start()
/// await is waiting a hot task.
await coldTask;
}
public void LongSyncTasksWrapped()
{
SyncTaskThatIsReallyLong();
AnotherSyncTaskThatIsReallyLong();
/* perform as much work as needed here */
return;
}
This answers the question of what's being awaited, its the Task's awaiter that is internally generated by the class.