The following C# code:
class Program
{
static readonly List<TaskCompletionSource<bool>> buffer =
new List<TaskCompletionSource<bool>>();
static Timer timer;
public static void Main()
{
var outstanding = Enumerable.Range(1, 10)
.Select(Enqueue)
.ToArray();
timer = new Timer(x => Flush(), null,
TimeSpan.FromSeconds(1),
TimeSpan.FromMilliseconds(-1));
try
{
Task.WaitAll(outstanding);
}
catch {}
Console.ReadKey();
}
static Task Enqueue(int i)
{
var task = new TaskCompletionSource<bool>();
buffer.Add(task);
return task.Task;
}
static void Flush()
{
try
{
throw new ArgumentException("test");
}
catch (Exception e)
{
foreach (var each in buffer)
{
var lenBefore = e.StackTrace.Length;
each.TrySetException(e);
var lenAfter = e.StackTrace.Length;
Console.WriteLine($"Before - After: {lenBefore} - {lenAfter}");
Console.WriteLine(e.StackTrace);
}
}
}
}
Produces:
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
Before - After: 149 - 149
But when I change Enqueue
method to async:
static async Task Enqueue(int i)
{
var task = new TaskCompletionSource<bool>();
buffer.Add(task);
return await task.Task;
}
The result is:
Before - After: 149 - 643
Before - After: 643 - 1137
Before - After: 1137 - 1631
Before - After: 1631 - 2125
Before - After: 2125 - 2619
Before - After: 2619 - 3113
Before - After: 3113 - 3607
Before - After: 3607 - 4101
Before - After: 4101 - 4595
Before - After: 4595 - 5089
It looks like stack trace growth recursively for each buffered item. For the first item exception stack trace will be:
at Program.Flush() in C:\src\Program.cs:line 41
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
While second will look like below and so on:
at Program.Flush() in C:\src\Program.cs:line 41
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotificati...
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Program.<Enqueue>d__3.MoveNext() in C:\src\Program.cs:line 34
What is going on here and how to fix it?
The TaskCompletionSource class is a great way to convert such code into a Task you can simply await . It's a bit of additional work, but the result is much easier to read and use. Be sure to take full advantage of TaskCompletionSource in your asynchronous C# code!
The main benefits of asynchronous programming using async / await include the following: Increase the performance and responsiveness of your application, particularly when you have long-running operations that do not require to block the execution.
async, await, and TaskThe await keyword waits for the async method until it returns a value. So the main application thread stops there until it receives a return value. The Task class represents an asynchronous operation and Task<TResult> generic class represents an operation that can return a value.
The stack trace listing provides a way to follow the call stack to the line number in the method where the exception occurs. The StackTrace property returns the frames of the call stack that originate at the location where the exception was thrown.
Short answer: await
tries to unwrap result, while method withod await
does not try to access task result.
Longer answer:
TaskAwaiter
is being inlined, and the HandleNonSuccessAndDebuggerNotification causes a call to ThrowForNonSuccess which seems to be inlined too and, as single exception is used to set result for 10 TaskCompletionSource
s, the reason of growing stack of that exception can be seen here.Simple solution is to use new Exception("Some descriptive message", originalException)
on each TrySetException
call.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With