/
AsyncTaskMethodBuilderT.cs
572 lines (513 loc) · 30.9 KB
/
AsyncTaskMethodBuilderT.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Provides a builder for asynchronous methods that return <see cref="Task{TResult}"/>.
/// This type is intended for compiler use only.
/// </summary>
/// <remarks>
/// AsyncTaskMethodBuilder{TResult} is a value type, and thus it is copied by value.
/// Prior to being copied, one of its Task, SetResult, or SetException members must be accessed,
/// or else the copies may end up building distinct Task instances.
/// </remarks>
public struct AsyncTaskMethodBuilder<TResult>
{
/// <summary>The lazily-initialized built task.</summary>
private Task<TResult>? m_task; // Debugger depends on the exact name of this field.
/// <summary>Initializes a new <see cref="AsyncTaskMethodBuilder"/>.</summary>
/// <returns>The initialized <see cref="AsyncTaskMethodBuilder"/>.</returns>
public static AsyncTaskMethodBuilder<TResult> Create() => default;
/// <summary>Initiates the builder's execution with the associated state machine.</summary>
/// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
/// <param name="stateMachine">The state machine instance, passed by reference.</param>
[DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine =>
AsyncMethodBuilderCore.Start(ref stateMachine);
/// <summary>Associates the builder with the state machine it represents.</summary>
/// <param name="stateMachine">The heap-allocated state machine object.</param>
/// <exception cref="ArgumentNullException">The <paramref name="stateMachine"/> argument was null (<see langword="Nothing" /> in Visual Basic).</exception>
/// <exception cref="InvalidOperationException">The builder is incorrectly initialized.</exception>
public void SetStateMachine(IAsyncStateMachine stateMachine) =>
AsyncMethodBuilderCore.SetStateMachine(stateMachine, m_task);
/// <summary>
/// Schedules the specified state machine to be pushed forward when the specified awaiter completes.
/// </summary>
/// <typeparam name="TAwaiter">Specifies the type of the awaiter.</typeparam>
/// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
/// <param name="awaiter">The awaiter.</param>
/// <param name="stateMachine">The state machine.</param>
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine =>
AwaitOnCompleted(ref awaiter, ref stateMachine, ref m_task);
internal static void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine, ref Task<TResult>? taskField)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine
{
try
{
awaiter.OnCompleted(GetStateMachineBox(ref stateMachine, ref taskField).MoveNextAction);
}
catch (Exception e)
{
Threading.Tasks.Task.ThrowAsync(e, targetContext: null);
}
}
/// <summary>
/// Schedules the specified state machine to be pushed forward when the specified awaiter completes.
/// </summary>
/// <typeparam name="TAwaiter">Specifies the type of the awaiter.</typeparam>
/// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
/// <param name="awaiter">The awaiter.</param>
/// <param name="stateMachine">The state machine.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine =>
AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine, [NotNull] ref Task<TResult>? taskField)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
IAsyncStateMachineBox box = GetStateMachineBox(ref stateMachine, ref taskField);
AwaitUnsafeOnCompleted(ref awaiter, box);
}
// Tier0 codegen for this function may still allocate (while FullOpts won't).
// TODO: remove once https://github.com/dotnet/runtime/issues/90965 is implemented
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
internal static void AwaitUnsafeOnCompleted<TAwaiter>(
ref TAwaiter awaiter, IAsyncStateMachineBox box)
where TAwaiter : ICriticalNotifyCompletion
{
// The null tests here ensure that the jit can optimize away the interface
// tests when TAwaiter is a ref type.
if ((null != (object?)default(TAwaiter)) && (awaiter is ITaskAwaiter))
{
ref TaskAwaiter ta = ref Unsafe.As<TAwaiter, TaskAwaiter>(ref awaiter); // relies on TaskAwaiter/TaskAwaiter<T> having the same layout
TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, continueOnCapturedContext: true);
}
else if ((null != (object?)default(TAwaiter)) && (awaiter is IConfiguredTaskAwaiter))
{
ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ta = ref Unsafe.As<TAwaiter, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter>(ref awaiter);
TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, (ta.m_options & ConfigureAwaitOptions.ContinueOnCapturedContext) != 0);
}
else if ((null != (object?)default(TAwaiter)) && (awaiter is IStateMachineBoxAwareAwaiter))
{
try
{
((IStateMachineBoxAwareAwaiter)awaiter).AwaitUnsafeOnCompleted(box);
}
catch (Exception e)
{
// Whereas with Task the code that hooks up and invokes the continuation is all local to corelib,
// with ValueTaskAwaiter we may be calling out to an arbitrary implementation of IValueTaskSource
// wrapped in the ValueTask, and as such we protect against errant exceptions that may emerge.
// We don't want such exceptions propagating back into the async method, which can't handle
// exceptions well at that location in the state machine, especially if the exception may occur
// after the ValueTaskAwaiter already successfully hooked up the callback, in which case it's possible
// two different flows of execution could end up happening in the same async method call.
Threading.Tasks.Task.ThrowAsync(e, targetContext: null);
}
}
else
{
// The awaiter isn't specially known. Fall back to doing a normal await.
try
{
awaiter.UnsafeOnCompleted(box.MoveNextAction);
}
catch (Exception e)
{
Threading.Tasks.Task.ThrowAsync(e, targetContext: null);
}
}
}
/// <summary>Gets the "boxed" state machine object.</summary>
/// <typeparam name="TStateMachine">Specifies the type of the async state machine.</typeparam>
/// <param name="stateMachine">The state machine.</param>
/// <param name="taskField">The reference to the Task field storing the Task instance.</param>
/// <returns>The "boxed" state machine.</returns>
private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
ref TStateMachine stateMachine,
[NotNull] ref Task<TResult>? taskField)
where TStateMachine : IAsyncStateMachine
{
ExecutionContext? currentContext = ExecutionContext.Capture();
// Check first for the most common case: not the first yield in an async method.
// In this case, the first yield will have already "boxed" the state machine in
// a strongly-typed manner into an AsyncStateMachineBox. It will already contain
// the state machine as well as a MoveNextDelegate and a context. The only thing
// we might need to do is update the context if that's changed since it was stored.
if (taskField is AsyncStateMachineBox<TStateMachine> stronglyTypedBox)
{
if (stronglyTypedBox.Context != currentContext)
{
stronglyTypedBox.Context = currentContext;
}
return stronglyTypedBox;
}
// The least common case: we have a weakly-typed boxed. This results if the debugger
// or some other use of reflection accesses a property like ObjectIdForDebugger or a
// method like SetNotificationForWaitCompletion prior to the first await happening. In
// such situations, we need to get an object to represent the builder, but we don't yet
// know the type of the state machine, and thus can't use TStateMachine. Instead, we
// use the IAsyncStateMachine interface, which all TStateMachines implement. This will
// result in a boxing allocation when storing the TStateMachine if it's a struct, but
// this only happens in active debugging scenarios where such performance impact doesn't
// matter.
if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
{
// If this is the first await, we won't yet have a state machine, so store it.
if (weaklyTypedBox.StateMachine == null)
{
Debugger.NotifyOfCrossThreadDependency(); // same explanation as with usage below
weaklyTypedBox.StateMachine = stateMachine;
}
// Update the context. This only happens with a debugger, so no need to spend
// extra IL checking for equality before doing the assignment.
weaklyTypedBox.Context = currentContext;
return weaklyTypedBox;
}
// Alert a listening debugger that we can't make forward progress unless it slips threads.
// If we don't do this, and a method that uses "await foo;" is invoked through funceval,
// we could end up hooking up a callback to push forward the async method's state machine,
// the debugger would then abort the funceval after it takes too long, and then continuing
// execution could result in another callback being hooked up. At that point we have
// multiple callbacks registered to push the state machine, which could result in bad behavior.
Debugger.NotifyOfCrossThreadDependency();
// At this point, taskField should really be null, in which case we want to create the box.
// However, in a variety of debugger-related (erroneous) situations, it might be non-null,
// e.g. if the Task property is examined in a Watch window, forcing it to be lazily-initialized
// as a Task<TResult> rather than as an AsyncStateMachineBox. The worst that happens in such
// cases is we lose the ability to properly step in the debugger, as the debugger uses that
// object's identity to track this specific builder/state machine. As such, we proceed to
// overwrite whatever's there anyway, even if it's non-null.
#if NATIVEAOT
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes
// per each async method in NativeAOT binaries without adding much value. Avoid
// generating this extra code until a better solution is implemented.
var box = new AsyncStateMachineBox<TStateMachine>();
#else
AsyncStateMachineBox<TStateMachine> box = AsyncMethodBuilderCore.TrackAsyncMethodCompletion ?
CreateDebugFinalizableAsyncStateMachineBox<TStateMachine>() :
new AsyncStateMachineBox<TStateMachine>();
#endif
taskField = box; // important: this must be done before storing stateMachine into box.StateMachine!
box.StateMachine = stateMachine;
box.Context = currentContext;
// Log the creation of the state machine box object / task for this async method.
if (TplEventSource.Log.IsEnabled())
{
TplEventSource.Log.TraceOperationBegin(box.Id, "Async: " + stateMachine.GetType().Name, 0);
}
// And if async debugging is enabled, track the task.
if (Threading.Tasks.Task.s_asyncDebuggingEnabled)
{
Threading.Tasks.Task.AddToActiveTasks(box);
}
return box;
}
#if !NATIVEAOT
// Avoid forcing the JIT to build DebugFinalizableAsyncStateMachineBox<TStateMachine> unless it's actually needed.
[MethodImpl(MethodImplOptions.NoInlining)]
private static AsyncStateMachineBox<TStateMachine> CreateDebugFinalizableAsyncStateMachineBox<TStateMachine>()
where TStateMachine : IAsyncStateMachine =>
new DebugFinalizableAsyncStateMachineBox<TStateMachine>();
/// <summary>
/// Provides an async state machine box with a finalizer that will fire an EventSource
/// event about the state machine if it's being finalized without having been completed.
/// </summary>
/// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
private sealed class DebugFinalizableAsyncStateMachineBox<TStateMachine> : // SOS DumpAsync command depends on this name
AsyncStateMachineBox<TStateMachine>
where TStateMachine : IAsyncStateMachine
{
~DebugFinalizableAsyncStateMachineBox()
{
// If the state machine is being finalized, something went wrong during its processing,
// e.g. it awaited something that got collected without itself having been completed.
// Fire an event with details about the state machine to help with debugging.
if (!IsCompleted) // double-check it's not completed, just to help minimize false positives
{
TplEventSource.Log.IncompleteAsyncMethod(this);
}
}
}
#endif
/// <summary>A strongly-typed box for Task-based async state machines.</summary>
/// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
private class AsyncStateMachineBox<TStateMachine> : // SOS DumpAsync command depends on this name
Task<TResult>, IAsyncStateMachineBox
where TStateMachine : IAsyncStateMachine
{
/// <summary>Delegate used to invoke on an ExecutionContext when passed an instance of this box type.</summary>
private static readonly ContextCallback s_callback = ExecutionContextCallback;
// Used to initialize s_callback above. We don't use a lambda for this on purpose: a lambda would
// introduce a new generic type behind the scenes that comes with a hefty size penalty in AOT builds.
private static void ExecutionContextCallback(object? s)
{
Debug.Assert(s is AsyncStateMachineBox<TStateMachine>);
// Only used privately to pass directly to EC.Run
Unsafe.As<AsyncStateMachineBox<TStateMachine>>(s).StateMachine!.MoveNext();
}
/// <summary>The state machine itself.</summary>
public TStateMachine? StateMachine; // mutable struct; do not make this readonly. SOS DumpAsync command depends on this name.
public AsyncStateMachineBox()
{
// The async state machine uses the base Task's state object field to store the captured execution context.
// Ensure that state object isn't published out for others to see.
Debug.Assert((m_stateFlags & (int)InternalTaskOptions.PromiseTask) != 0, "Expected state flags to already be configured.");
Debug.Assert(m_stateObject is null, "Expected to be able to use the state object field for ExecutionContext.");
m_stateFlags |= (int)InternalTaskOptions.HiddenState;
}
/// <summary>Debugger-only display string for the async state machine.</summary>
private string DebuggerDisplay
{
get
{
// Ideally we just use the type of the TStateMachine as the "method" name. However, in certain use in the
// debugger, TStateMachine might actually be a weakly-typed IAsyncStateMachine, in which case we can ToString
// the state machine instance. But in debug builds the state machine type could also be a class, in which case
// the field could be null, so worst case we just fall back to using "IAsyncStateMachine".
string stateMachineName = typeof(TStateMachine) != typeof(IAsyncStateMachine) ?
typeof(TStateMachine).Name :
StateMachine?.ToString() ??
nameof(IAsyncStateMachine);
// Keep the shape of this message in sync with that of the base Task<TResult>.
return IsCompletedSuccessfully && typeof(TResult) != typeof(VoidTaskResult) ?
$"Id = {Id}, Status = {Status}, Method = {stateMachineName}, Result = {m_result}" :
$"Id = {Id}, Status = {Status}, Method = {stateMachineName}";
}
}
/// <summary>A delegate to the <see cref="MoveNext()"/> method.</summary>
public Action MoveNextAction => (Action)(m_action ??= new Action(MoveNext));
/// <summary>Captured ExecutionContext with which to invoke <see cref="MoveNextAction"/>; may be null.</summary>
/// <remarks>
/// This uses the base Task.m_stateObject field to store the context, as that field is otherwise unused for state machine boxes.
/// This *must* not be set to anything other than null or an ExecutionContext, or it will result in a type safety hole.
/// We also don't want this ExecutionContext exposed out to consumers of the Task via Task.AsyncState, so
/// the ctor sets the HiddenState option to prevent this from leaking out.
/// </remarks>
public ref ExecutionContext? Context
{
get
{
Debug.Assert(m_stateObject is null or ExecutionContext, $"Expected {nameof(m_stateObject)} to be null or an ExecutionContext but was {(m_stateObject is object o ? o.GetType().ToString() : "(null)")}.");
return ref Unsafe.As<object?, ExecutionContext?>(ref m_stateObject);
}
}
internal sealed override void ExecuteFromThreadPool(Thread threadPoolThread) => MoveNext(threadPoolThread);
/// <summary>Calls MoveNext on <see cref="StateMachine"/></summary>
public void MoveNext() => MoveNext(threadPoolThread: null);
private void MoveNext(Thread? threadPoolThread)
{
Debug.Assert(!IsCompleted);
bool loggingOn = TplEventSource.Log.IsEnabled();
if (loggingOn)
{
TplEventSource.Log.TraceSynchronousWorkBegin(this.Id, CausalitySynchronousWork.Execution);
}
ExecutionContext? context = Context;
if (context == null)
{
Debug.Assert(StateMachine != null);
StateMachine.MoveNext();
}
else
{
if (threadPoolThread is null)
{
ExecutionContext.RunInternal(context, s_callback, this);
}
else
{
ExecutionContext.RunFromThreadPoolDispatchLoop(threadPoolThread, context, s_callback, this);
}
}
if (IsCompleted)
{
ClearStateUponCompletion();
}
if (loggingOn)
{
TplEventSource.Log.TraceSynchronousWorkEnd(CausalitySynchronousWork.Execution);
}
}
/// <summary>Clears out all state associated with a completed box.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ClearStateUponCompletion()
{
Debug.Assert(IsCompleted);
// This logic may be invoked multiple times on the same instance and needs to be robust against that.
// If async debugging is enabled, remove the task from tracking.
if (s_asyncDebuggingEnabled)
{
RemoveFromActiveTasks(this);
}
// Clear out state now that the async method has completed.
// This avoids keeping arbitrary state referenced by lifted locals
// if this Task / state machine box is held onto.
StateMachine = default;
Context = default;
#if !NATIVEAOT
// In case this is a state machine box with a finalizer, suppress its finalization
// as it's now complete. We only need the finalizer to run if the box is collected
// without having been completed.
if (AsyncMethodBuilderCore.TrackAsyncMethodCompletion)
{
GC.SuppressFinalize(this);
}
#endif
}
/// <summary>Gets the state machine as a boxed object. This should only be used for debugging purposes.</summary>
IAsyncStateMachine IAsyncStateMachineBox.GetStateMachineObject() => StateMachine!; // likely boxes, only use for debugging
}
/// <summary>Gets the <see cref="Task{TResult}"/> for this builder.</summary>
/// <returns>The <see cref="Task{TResult}"/> representing the builder's asynchronous operation.</returns>
public Task<TResult> Task
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => m_task ?? InitializeTaskAsPromise();
}
/// <summary>
/// Initializes the task, which must not yet be initialized. Used only when the Task is being forced into
/// existence when no state machine is needed, e.g. when the builder is being synchronously completed with
/// an exception, when the builder is being used out of the context of an async method, etc.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private Task<TResult> InitializeTaskAsPromise()
{
Debug.Assert(m_task == null);
return m_task = new Task<TResult>();
}
internal static Task<TResult> CreateWeaklyTypedStateMachineBox()
{
#if NATIVEAOT
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes
// per each async method in NativeAOT binaries without adding much value. Avoid
// generating this extra code until a better solution is implemented.
return new AsyncStateMachineBox<IAsyncStateMachine>();
#else
return AsyncMethodBuilderCore.TrackAsyncMethodCompletion ?
CreateDebugFinalizableAsyncStateMachineBox<IAsyncStateMachine>() :
new AsyncStateMachineBox<IAsyncStateMachine>();
#endif
}
/// <summary>
/// Completes the <see cref="Task{TResult}"/> in the
/// <see cref="TaskStatus">RanToCompletion</see> state with the specified result.
/// </summary>
/// <param name="result">The result to use to complete the task.</param>
/// <exception cref="InvalidOperationException">The task has already completed.</exception>
public void SetResult(TResult result)
{
// Get the currently stored task, which will be non-null if get_Task has already been accessed.
// If there isn't one, get a task and store it.
if (m_task is null)
{
m_task = Threading.Tasks.Task.FromResult(result);
}
else
{
// Slow path: complete the existing task.
SetExistingTaskResult(m_task, result);
}
}
/// <summary>Completes the already initialized task with the specified result.</summary>
/// <param name="result">The result to use to complete the task.</param>
/// <param name="task">The task to complete.</param>
internal static void SetExistingTaskResult(Task<TResult> task, TResult? result)
{
Debug.Assert(task != null, "Expected non-null task");
if (TplEventSource.Log.IsEnabled())
{
TplEventSource.Log.TraceOperationEnd(task.Id, AsyncCausalityStatus.Completed);
}
if (!task.TrySetResult(result))
{
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted);
}
}
/// <summary>
/// Completes the <see cref="Task{TResult}"/> in the
/// <see cref="TaskStatus">Faulted</see> state with the specified exception.
/// </summary>
/// <param name="exception">The <see cref="Exception"/> to use to fault the task.</param>
/// <exception cref="ArgumentNullException">The <paramref name="exception"/> argument is null (<see langword="Nothing" /> in Visual Basic).</exception>
/// <exception cref="InvalidOperationException">The task has already completed.</exception>
public void SetException(Exception exception) => SetException(exception, ref m_task);
internal static void SetException(Exception exception, ref Task<TResult>? taskField)
{
if (exception == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.exception);
}
// Get the task, forcing initialization if it hasn't already been initialized.
Task<TResult> task = (taskField ??= new Task<TResult>());
// If the exception represents cancellation, cancel the task. Otherwise, fault the task.
bool successfullySet = exception is OperationCanceledException oce ?
task.TrySetCanceled(oce.CancellationToken, oce) :
task.TrySetException(exception);
// Unlike with TaskCompletionSource, we do not need to spin here until _taskAndStateMachine is completed,
// since AsyncTaskMethodBuilder.SetException should not be immediately followed by any code
// that depends on the task having completely completed. Moreover, with correct usage,
// SetResult or SetException should only be called once, so the Try* methods should always
// return true, so no spinning would be necessary anyway (the spinning in TCS is only relevant
// if another thread completes the task first).
if (!successfullySet)
{
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted);
}
}
/// <summary>
/// Called by the debugger to request notification when the first wait operation
/// (await, Wait, Result, etc.) on this builder's task completes.
/// </summary>
/// <param name="enabled">
/// true to enable notification; false to disable a previously set notification.
/// </param>
/// <remarks>
/// This should only be invoked from within an asynchronous method,
/// and only by the debugger.
/// </remarks>
internal void SetNotificationForWaitCompletion(bool enabled) =>
SetNotificationForWaitCompletion(enabled, ref m_task);
internal static void SetNotificationForWaitCompletion(bool enabled, [NotNull] ref Task<TResult>? taskField)
{
// Get the task (forcing initialization if not already initialized), and set debug notification
(taskField ??= CreateWeaklyTypedStateMachineBox()).SetNotificationForWaitCompletion(enabled);
// NOTE: It's important that the debugger use builder.SetNotificationForWaitCompletion
// rather than builder.Task.SetNotificationForWaitCompletion. Even though the latter will
// lazily-initialize the task as well, it'll initialize it to a Task<T> (which is important
// to minimize size for cases where an ATMB is used directly by user code to avoid the
// allocation overhead of a TaskCompletionSource). If that's done prior to the first await,
// the GetMoveNextDelegate code, which needs an AsyncStateMachineBox, will end up creating
// a new box and overwriting the previously created task. That'll change the object identity
// of the task being used for wait completion notification, and no notification will
// ever arrive, breaking step-out behavior when stepping out before the first yielding await.
}
/// <summary>
/// Gets an object that may be used to uniquely identify this builder to the debugger.
/// </summary>
/// <remarks>
/// This property lazily instantiates the ID in a non-thread-safe manner.
/// It must only be used by the debugger and tracing purposes, and only in a single-threaded manner
/// when no other threads are in the middle of accessing this or other members that lazily initialize the task.
/// </remarks>
internal object ObjectIdForDebugger => m_task ??= CreateWeaklyTypedStateMachineBox();
}
}