2
2
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
4
4
using System . Diagnostics ;
5
- using System . Diagnostics . Contracts ;
6
5
7
6
namespace System . Threading . Tasks
8
7
{
@@ -28,56 +27,20 @@ public static class TaskExtensions
28
27
/// <returns>A Task that represents the asynchronous operation of the provided Task{Task}.</returns>
29
28
public static Task Unwrap ( this Task < Task > task )
30
29
{
31
- if ( task == null ) throw new ArgumentNullException ( "task" ) ;
32
-
33
- bool result ;
34
-
35
- // tcs.Task serves as a proxy for task.Result.
36
- // AttachedToParent is the only legal option for TCS-style task.
37
- var tcs = new TaskCompletionSource < Task > ( task . CreationOptions & TaskCreationOptions . AttachedToParent ) ;
38
-
39
- // Set up some actions to take when task has completed.
40
- task . ContinueWith ( delegate
41
- {
42
- switch ( task . Status )
43
- {
44
- // If task did not run to completion, then record the cancellation/fault information
45
- // to tcs.Task.
46
- case TaskStatus . Canceled :
47
- case TaskStatus . Faulted :
48
- result = tcs . TrySetFromTask ( task ) ;
49
- Debug . Assert ( result , "Unwrap(Task<Task>): Expected TrySetFromTask #1 to succeed" ) ;
50
- break ;
51
-
52
- case TaskStatus . RanToCompletion :
53
- // task.Result == null ==> proxy should be canceled.
54
- if ( task . Result == null ) tcs . TrySetCanceled ( ) ;
55
-
56
- // When task.Result completes, take some action to set the completion state of tcs.Task.
57
- else
58
- {
59
- task . Result . ContinueWith ( _ =>
60
- {
61
- // Copy completion/cancellation/exception info from task.Result to tcs.Task.
62
- result = tcs . TrySetFromTask ( task . Result ) ;
63
- Debug . Assert ( result , "Unwrap(Task<Task>): Expected TrySetFromTask #2 to succeed" ) ;
64
- } , CancellationToken . None , TaskContinuationOptions . ExecuteSynchronously | TaskContinuationOptions . DenyChildAttach , TaskScheduler . Default )
65
- . ContinueWith ( antecedent =>
66
- {
67
- // Clean up if ContinueWith() operation fails due to TSE
68
- tcs . TrySetException ( antecedent . Exception ) ;
69
- } , CancellationToken . None , TaskContinuationOptions . OnlyOnFaulted | TaskContinuationOptions . DenyChildAttach , TaskScheduler . Default ) ;
70
- }
71
- break ;
72
- }
73
- } , TaskContinuationOptions . ExecuteSynchronously | TaskContinuationOptions . DenyChildAttach , TaskScheduler . Default ) . ContinueWith ( antecedent =>
30
+ if ( task == null )
31
+ throw new ArgumentNullException ( "task" ) ;
32
+
33
+ // Fast path for an already successfully completed outer task: just return the inner one.
34
+ // As in the subsequent slower path, a null inner task is special-cased to mean cancellation.
35
+ if ( task . Status == TaskStatus . RanToCompletion && ( task . CreationOptions & TaskCreationOptions . AttachedToParent ) == 0 )
74
36
{
75
- // Clean up if ContinueWith() operation fails due to TSE
76
- tcs . TrySetException ( antecedent . Exception ) ;
77
- } , CancellationToken . None , TaskContinuationOptions . OnlyOnFaulted | TaskContinuationOptions . DenyChildAttach , TaskScheduler . Default ) ;
37
+ return task . Result ?? Task . FromCanceled ( new CancellationToken ( true ) ) ;
38
+ }
78
39
79
- // Return this immediately as a proxy. When task.Result completes, or task is faulted/canceled,
80
- // the completion information will be transfered to tcs.Task.
40
+ // Create a new Task to serve as a proxy for the actual inner task. Attach it
41
+ // to the parent if the original was attached to the parent.
42
+ var tcs = new TaskCompletionSource < VoidResult > ( task . CreationOptions & TaskCreationOptions . AttachedToParent ) ;
43
+ TransferAsynchronously ( tcs , task ) ;
81
44
return tcs . Task ;
82
45
}
83
46
@@ -97,86 +60,148 @@ public static Task Unwrap(this Task<Task> task)
97
60
/// <returns>A Task{TResult} that represents the asynchronous operation of the provided Task{Task{TResult}}.</returns>
98
61
public static Task < TResult > Unwrap < TResult > ( this Task < Task < TResult > > task )
99
62
{
100
- if ( task == null ) throw new ArgumentNullException ( "task" ) ;
63
+ if ( task == null )
64
+ throw new ArgumentNullException ( "task" ) ;
101
65
102
- bool result ;
66
+ // Fast path for an already successfully completed outer task: just return the inner one.
67
+ // As in the subsequent slower path, a null inner task is special-cased to mean cancellation.
68
+ if ( task . Status == TaskStatus . RanToCompletion && ( task . CreationOptions & TaskCreationOptions . AttachedToParent ) == 0 )
69
+ {
70
+ return task . Result ?? Task . FromCanceled < TResult > ( new CancellationToken ( true ) ) ;
71
+ }
103
72
104
- // tcs. Task serves as a proxy for task.Result.
105
- // AttachedToParent is the only legal option for TCS-style task .
73
+ // Create a new Task to serve as a proxy for the actual inner task. Attach it
74
+ // to the parent if the original was attached to the parent .
106
75
var tcs = new TaskCompletionSource < TResult > ( task . CreationOptions & TaskCreationOptions . AttachedToParent ) ;
76
+ TransferAsynchronously ( tcs , task ) ;
77
+ return tcs . Task ;
78
+ }
107
79
108
- // Set up some actions to take when task has completed.
109
- task . ContinueWith ( delegate
80
+ /// <summary>
81
+ /// Transfer the results of the <paramref name="outer"/> task's inner task to the <paramref name="completionSource"/>.
82
+ /// </summary>
83
+ /// <param name="completionSource">The completion source to which results should be transfered.</param>
84
+ /// <param name="outer">
85
+ /// The outer task that when completed will yield an inner task whose results we want marshaled to the <paramref name="completionSource"/>.
86
+ /// </param>
87
+ private static void TransferAsynchronously < TResult , TInner > ( TaskCompletionSource < TResult > completionSource , Task < TInner > outer ) where TInner : Task
88
+ {
89
+ Action callback = null ;
90
+
91
+ // Create a continuation delegate. For performance reasons, we reuse the same delegate/closure across multiple
92
+ // continuations; by using .ConfigureAwait(false).GetAwaiter().UnsafeOnComplete(action), in most cases
93
+ // this delegate can be stored directly into the Task's continuation field, eliminating the need for additional
94
+ // allocations. Thus, this whole Unwrap operation generally results in four allocations: one for the TaskCompletionSource,
95
+ // one for the returned task, one for the delegate, and one for the closure. Since the delegate is used
96
+ // across multiple continuations, we use the callback variable as well to indicate which continuation we're in:
97
+ // if the callback is non-null, then we're processing the continuation for the outer task and use the callback
98
+ // object as the continuation off of the inner task; if the callback is null, then we're processing the
99
+ // inner task.
100
+ callback = delegate
110
101
{
111
- switch ( task . Status )
102
+ Debug . Assert ( outer . IsCompleted ) ;
103
+ if ( callback != null )
112
104
{
113
- // If task did not run to completion, then record the cancellation/fault information
114
- // to tcs.Task.
115
- case TaskStatus . Canceled :
116
- case TaskStatus . Faulted :
117
- result = tcs . TrySetFromTask ( task ) ;
118
- Debug . Assert ( result , "Unwrap(Task<Task<T>>): Expected TrySetFromTask #1 to succeed" ) ;
119
- break ;
120
-
121
- case TaskStatus . RanToCompletion :
122
- // task.Result == null ==> proxy should be canceled.
123
- if ( task . Result == null ) tcs . TrySetCanceled ( ) ;
124
-
125
- // When task.Result completes, take some action to set the completion state of tcs.Task.
126
- else
127
- {
128
- task . Result . ContinueWith ( _ =>
105
+ // Process the outer task's completion
106
+
107
+ // Clear out the callback field to indicate that any future invocations should
108
+ // be for processing the inner task, but store away a local copy in case we need
109
+ // to use it as the continuation off of the outer task.
110
+ Action innerCallback = callback ;
111
+ callback = null ;
112
+
113
+ bool result = true ;
114
+ switch ( outer . Status )
115
+ {
116
+ case TaskStatus . Canceled :
117
+ case TaskStatus . Faulted :
118
+ // The outer task has completed as canceled or faulted; transfer that
119
+ // status to the completion source, and we're done.
120
+ result = completionSource . TrySetFromTask ( outer ) ;
121
+ break ;
122
+ case TaskStatus . RanToCompletion :
123
+ Task inner = outer . Result ;
124
+ if ( inner == null )
125
+ {
126
+ // The outer task completed successfully, but with a null inner task;
127
+ // cancel the completionSource, and we're done.
128
+ result = completionSource . TrySetCanceled ( ) ;
129
+ }
130
+ else if ( inner . IsCompleted )
129
131
{
130
- // Copy completion/cancellation/exception info from task.Result to tcs.Task.
131
- result = tcs . TrySetFromTask ( task . Result ) ;
132
- Debug . Assert ( result , "Unwrap(Task<Task<T>>): Expected TrySetFromTask #2 to succeed" ) ;
133
- } ,
134
- CancellationToken . None , TaskContinuationOptions . ExecuteSynchronously | TaskContinuationOptions . DenyChildAttach , TaskScheduler . Default )
135
- . ContinueWith ( antecedent =>
136
- {
137
- // Clean up if ContinueWith() operation fails due to TSE
138
- tcs . TrySetException ( antecedent . Exception ) ;
139
- } , CancellationToken . None , TaskContinuationOptions . OnlyOnFaulted | TaskContinuationOptions . DenyChildAttach , TaskScheduler . Default ) ;
140
- }
141
-
142
- break ;
132
+ // The inner task also completed! Transfer the results, and we're done.
133
+ result = completionSource . TrySetFromTask ( inner ) ;
134
+ }
135
+ else
136
+ {
137
+ // Run this delegate again once the inner task has completed.
138
+ inner . ConfigureAwait ( false ) . GetAwaiter ( ) . UnsafeOnCompleted ( innerCallback ) ;
139
+ }
140
+ break ;
141
+ }
142
+ Debug . Assert ( result ) ;
143
143
}
144
- } , TaskContinuationOptions . ExecuteSynchronously | TaskContinuationOptions . DenyChildAttach , TaskScheduler . Current ) . ContinueWith ( antecedent =>
145
- {
146
- // Clean up if ContinueWith() operation fails due to TSE
147
- tcs . TrySetException ( antecedent . Exception ) ;
148
- } , CancellationToken . None , TaskContinuationOptions . OnlyOnFaulted | TaskContinuationOptions . DenyChildAttach , TaskScheduler . Default ) ; ;
144
+ else
145
+ {
146
+ // Process the inner task's completion. All we need to do is transfer its results
147
+ // to the completion source.
148
+ Debug . Assert ( outer . Status == TaskStatus . RanToCompletion ) ;
149
+ Debug . Assert ( outer . Result . IsCompleted ) ;
150
+ completionSource . TrySetFromTask ( outer . Result ) ;
151
+ }
152
+ } ;
149
153
150
- // Return this immediately as a proxy. When task.Result completes, or task is faulted/canceled,
151
- // the completion information will be transfered to tcs.Task.
152
- return tcs . Task ;
154
+ // Kick things off by hooking up the callback as the task's continuation
155
+ outer . ConfigureAwait ( false ) . GetAwaiter ( ) . UnsafeOnCompleted ( callback ) ;
153
156
}
154
157
155
- // Transfer the completion status from "source" to "me".
156
- private static bool TrySetFromTask < TResult > ( this TaskCompletionSource < TResult > me , Task source )
158
+ /// <summary>Copies that ending state information from <paramref name="task"/> to <paramref name="completionSource"/>.</summary>
159
+ private static bool TrySetFromTask < TResult > ( this TaskCompletionSource < TResult > completionSource , Task task )
157
160
{
158
- Debug . Assert ( source . IsCompleted , "TrySetFromTask: Expected source to have completed." ) ;
159
- bool rval = false ;
161
+ Debug . Assert ( task . IsCompleted ) ;
160
162
161
- switch ( source . Status )
163
+ bool result = false ;
164
+ switch ( task . Status )
162
165
{
163
166
case TaskStatus . Canceled :
164
- rval = me . TrySetCanceled ( ) ;
167
+ result = completionSource . TrySetCanceled ( ExtractCancellationToken ( task ) ) ;
165
168
break ;
166
169
167
170
case TaskStatus . Faulted :
168
- rval = me . TrySetException ( source . Exception . InnerExceptions ) ;
171
+ result = completionSource . TrySetException ( task . Exception . InnerExceptions ) ;
169
172
break ;
170
173
171
174
case TaskStatus . RanToCompletion :
172
- if ( source is Task < TResult > )
173
- rval = me . TrySetResult ( ( ( Task < TResult > ) source ) . Result ) ;
174
- else
175
- rval = me . TrySetResult ( default ( TResult ) ) ;
175
+ Task < TResult > resultTask = task as Task < TResult > ;
176
+ result = resultTask != null ?
177
+ completionSource . TrySetResult ( resultTask . Result ) :
178
+ completionSource . TrySetResult ( default ( TResult ) ) ;
176
179
break ;
177
180
}
181
+ return result ;
182
+ }
178
183
179
- return rval ;
184
+ /// <summary>Gets the CancellationToken associated with a canceled task.</summary>
185
+ private static CancellationToken ExtractCancellationToken ( Task task )
186
+ {
187
+ // With the public Task APIs as of .NET 4.6, the only way to extract a CancellationToken
188
+ // that was associated with a Task is by await'ing it, catching the resulting
189
+ // OperationCanceledException, and getting the token from the OCE.
190
+ Debug . Assert ( task . IsCanceled ) ;
191
+ try
192
+ {
193
+ task . GetAwaiter ( ) . GetResult ( ) ;
194
+ }
195
+ catch ( OperationCanceledException oce )
196
+ {
197
+ CancellationToken ct = oce . CancellationToken ;
198
+ if ( ct . IsCancellationRequested )
199
+ return ct ;
200
+ }
201
+ return new CancellationToken ( true ) ;
180
202
}
203
+
204
+ /// <summary>Dummy type to use as a void TResult.</summary>
205
+ private struct VoidResult { }
181
206
}
182
- }
207
+ }
0 commit comments