22// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33
44using System . Diagnostics ;
5- using System . Diagnostics . Contracts ;
65
76namespace System . Threading . Tasks
87{
@@ -28,56 +27,13 @@ public static class TaskExtensions
2827 /// <returns>A Task that represents the asynchronous operation of the provided Task{Task}.</returns>
2928 public static Task Unwrap ( this Task < Task > task )
3029 {
31- if ( task == null ) throw new ArgumentNullException ( "task" ) ;
30+ if ( task == null )
31+ throw new ArgumentNullException ( "task" ) ;
3232
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 =>
74- {
75- // Clean up if ContinueWith() operation fails due to TSE
76- tcs . TrySetException ( antecedent . Exception ) ;
77- } , CancellationToken . None , TaskContinuationOptions . OnlyOnFaulted | TaskContinuationOptions . DenyChildAttach , TaskScheduler . Default ) ;
78-
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.
33+ // Create a new Task to serve as a proxy for the actual inner task. Attach it
34+ // to the parent if the original was attached to the parent.
35+ var tcs = new TaskCompletionSource < VoidResult > ( task . CreationOptions & TaskCreationOptions . AttachedToParent ) ;
36+ TransferAsynchronously ( tcs , task ) ;
8137 return tcs . Task ;
8238 }
8339
@@ -97,86 +53,141 @@ public static Task Unwrap(this Task<Task> task)
9753 /// <returns>A Task{TResult} that represents the asynchronous operation of the provided Task{Task{TResult}}.</returns>
9854 public static Task < TResult > Unwrap < TResult > ( this Task < Task < TResult > > task )
9955 {
100- if ( task == null ) throw new ArgumentNullException ( "task" ) ;
101-
102- bool result ;
56+ if ( task == null )
57+ throw new ArgumentNullException ( "task" ) ;
10358
104- // tcs. Task serves as a proxy for task.Result.
105- // AttachedToParent is the only legal option for TCS-style task .
59+ // Create a new Task to serve as a proxy for the actual inner task. Attach it
60+ // to the parent if the original was attached to the parent .
10661 var tcs = new TaskCompletionSource < TResult > ( task . CreationOptions & TaskCreationOptions . AttachedToParent ) ;
62+ TransferAsynchronously ( tcs , task ) ;
63+ return tcs . Task ;
64+ }
10765
108- // Set up some actions to take when task has completed.
109- task . ContinueWith ( delegate
66+ /// <summary>
67+ /// Transfer the results of the <paramref name="outer"/> task's inner task to the <paramref name="completionSource"/>.
68+ /// </summary>
69+ /// <param name="completionSource">The completion source to which results should be transfered.</param>
70+ /// <param name="outer">
71+ /// The outer task that when completed will yield an inner task whose results we want marshaled to the <paramref name="completionSource"/>.
72+ /// </param>
73+ private static void TransferAsynchronously < TResult , TInner > ( TaskCompletionSource < TResult > completionSource , Task < TInner > outer ) where TInner : Task
74+ {
75+ Action callback = null ;
76+
77+ // Create a continuation delegate. For performance reasons, we reuse the same delegate/closure across multiple
78+ // continuations; by using .ConfigureAwait(false).GetAwaiter().UnsafeOnComplete(action), in most cases
79+ // this delegate can be stored directly into the Task's continuation field, eliminating the need for additional
80+ // allocations. Thus, this whole Unwrap operation generally results in four allocations: one for the TaskCompletionSource,
81+ // one for the returned task, one for the delegate, and one for the closure. Since the delegate is used
82+ // across multiple continuations, we use the callback variable as well to indicate which continuation we're in:
83+ // if the callback is non-null, then we're processing the continuation for the outer task and use the callback
84+ // object as the continuation off of the inner task; if the callback is null, then we're processing the
85+ // inner task.
86+ callback = delegate
11087 {
111- switch ( task . Status )
88+ Debug . Assert ( outer . IsCompleted ) ;
89+ if ( callback != null )
11290 {
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 ( _ =>
91+ // Process the outer task's completion
92+
93+ // Clear out the callback field to indicate that any future invocations should
94+ // be for processing the inner task, but store away a local copy in case we need
95+ // to use it as the continuation off of the outer task.
96+ Action innerCallback = callback ;
97+ callback = null ;
98+
99+ bool result = true ;
100+ switch ( outer . Status )
101+ {
102+ case TaskStatus . Canceled :
103+ case TaskStatus . Faulted :
104+ // The outer task has completed as canceled or faulted; transfer that
105+ // status to the completion source, and we're done.
106+ result = completionSource . TrySetFromTask ( outer ) ;
107+ break ;
108+ case TaskStatus . RanToCompletion :
109+ Task inner = outer . Result ;
110+ if ( inner == null )
129111 {
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 ;
112+ // The outer task completed successfully, but with a null inner task;
113+ // cancel the completionSource, and we're done.
114+ result = completionSource . TrySetCanceled ( ) ;
115+ }
116+ else if ( inner . IsCompleted )
117+ {
118+ // The inner task also completed! Transfer the results, and we're done.
119+ result = completionSource . TrySetFromTask ( inner ) ;
120+ }
121+ else
122+ {
123+ // Run this delegate again once the inner task has completed.
124+ inner . ConfigureAwait ( false ) . GetAwaiter ( ) . UnsafeOnCompleted ( innerCallback ) ;
125+ }
126+ break ;
127+ }
128+ Debug . Assert ( result ) ;
143129 }
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 ) ; ;
130+ else
131+ {
132+ // Process the inner task's completion. All we need to do is transfer its results
133+ // to the completion source.
134+ Debug . Assert ( outer . Status == TaskStatus . RanToCompletion ) ;
135+ Debug . Assert ( outer . Result . IsCompleted ) ;
136+ completionSource . TrySetFromTask ( outer . Result ) ;
137+ }
138+ } ;
149139
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 ;
140+ // Kick things off by hooking up the callback as the task's continuation
141+ outer . ConfigureAwait ( false ) . GetAwaiter ( ) . UnsafeOnCompleted ( callback ) ;
153142 }
154143
155- // Transfer the completion status from "source" to "me".
156- private static bool TrySetFromTask < TResult > ( this TaskCompletionSource < TResult > me , Task source )
144+ /// <summary>Copies that ending state information from <paramref name="task"/> to <paramref name="completionSource"/>.</summary>
145+ private static bool TrySetFromTask < TResult > ( this TaskCompletionSource < TResult > completionSource , Task task )
157146 {
158- Debug . Assert ( source . IsCompleted , "TrySetFromTask: Expected source to have completed." ) ;
159- bool rval = false ;
147+ Debug . Assert ( task . IsCompleted ) ;
160148
161- switch ( source . Status )
149+ bool result = false ;
150+ switch ( task . Status )
162151 {
163152 case TaskStatus . Canceled :
164- rval = me . TrySetCanceled ( ) ;
153+ result = completionSource . TrySetCanceled ( ExtractCancellationToken ( task ) ) ;
165154 break ;
166155
167156 case TaskStatus . Faulted :
168- rval = me . TrySetException ( source . Exception . InnerExceptions ) ;
157+ result = completionSource . TrySetException ( task . Exception . InnerExceptions ) ;
169158 break ;
170159
171160 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 ) ) ;
161+ Task < TResult > resultTask = task as Task < TResult > ;
162+ result = resultTask != null ?
163+ completionSource . TrySetResult ( resultTask . Result ) :
164+ completionSource . TrySetResult ( default ( TResult ) ) ;
176165 break ;
177166 }
167+ return result ;
168+ }
178169
179- return rval ;
170+ /// <summary>Gets the CancellationToken associated with a canceled task.</summary>
171+ private static CancellationToken ExtractCancellationToken ( Task task )
172+ {
173+ // With the public Task APIs as of .NET 4.6, the only way to extract a CancellationToken
174+ // that was associated with a Task is by await'ing it, catching the resulting
175+ // OperationCanceledException, and getting the token from the OCE.
176+ Debug . Assert ( task . IsCanceled ) ;
177+ try
178+ {
179+ task . GetAwaiter ( ) . GetResult ( ) ;
180+ }
181+ catch ( OperationCanceledException oce )
182+ {
183+ CancellationToken ct = oce . CancellationToken ;
184+ if ( ct . IsCancellationRequested )
185+ return ct ;
186+ }
187+ return new CancellationToken ( true ) ;
180188 }
189+
190+ /// <summary>Dummy type to use as a void TResult.</summary>
191+ private struct VoidResult { }
181192 }
182- }
193+ }
0 commit comments