Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit f6ae29e

Browse files
committed
Merge pull request #1589 from stephentoub/taskextensions_unwrap
Reimplement TaskExtensions.Unwrap
2 parents df34a44 + 470e79f commit f6ae29e

File tree

2 files changed

+138
-114
lines changed

2 files changed

+138
-114
lines changed

src/System.Threading.Tasks/src/System/Threading/Tasks/TaskExtensions.CoreCLR.cs

Lines changed: 131 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.Diagnostics;
5-
using System.Diagnostics.Contracts;
65

76
namespace System.Threading.Tasks
87
{
@@ -28,56 +27,20 @@ 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");
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)
7436
{
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+
}
7839

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);
8144
return tcs.Task;
8245
}
8346

@@ -97,86 +60,148 @@ public static Task Unwrap(this Task<Task> task)
9760
/// <returns>A Task{TResult} that represents the asynchronous operation of the provided Task{Task{TResult}}.</returns>
9861
public static Task<TResult> Unwrap<TResult>(this Task<Task<TResult>> task)
9962
{
100-
if (task == null) throw new ArgumentNullException("task");
63+
if (task == null)
64+
throw new ArgumentNullException("task");
10165

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+
}
10372

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.
10675
var tcs = new TaskCompletionSource<TResult>(task.CreationOptions & TaskCreationOptions.AttachedToParent);
76+
TransferAsynchronously(tcs, task);
77+
return tcs.Task;
78+
}
10779

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
110101
{
111-
switch (task.Status)
102+
Debug.Assert(outer.IsCompleted);
103+
if (callback != null)
112104
{
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)
129131
{
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);
143143
}
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+
};
149153

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);
153156
}
154157

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)
157160
{
158-
Debug.Assert(source.IsCompleted, "TrySetFromTask: Expected source to have completed.");
159-
bool rval = false;
161+
Debug.Assert(task.IsCompleted);
160162

161-
switch(source.Status)
163+
bool result = false;
164+
switch(task.Status)
162165
{
163166
case TaskStatus.Canceled:
164-
rval = me.TrySetCanceled();
167+
result = completionSource.TrySetCanceled(ExtractCancellationToken(task));
165168
break;
166169

167170
case TaskStatus.Faulted:
168-
rval = me.TrySetException(source.Exception.InnerExceptions);
171+
result = completionSource.TrySetException(task.Exception.InnerExceptions);
169172
break;
170173

171174
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));
176179
break;
177180
}
181+
return result;
182+
}
178183

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);
180202
}
203+
204+
/// <summary>Dummy type to use as a void TResult.</summary>
205+
private struct VoidResult { }
181206
}
182-
}
207+
}

src/System.Threading.Tasks/tests/UnwrapTests.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public void NonGeneric_Completed_Completed(Task inner)
2828
{
2929
Task<Task> outer = Task.FromResult(inner);
3030
Task unwrappedInner = outer.Unwrap();
31-
// Assert.True(unwrappedInner.IsCompleted); // TODO: uncomment once we have implementation with this optimization
32-
// Assert.Same(inner, unwrappedInner); // TODO: uncomment once we have implementation with this optimization
31+
Assert.True(unwrappedInner.IsCompleted);
32+
Assert.Same(inner, unwrappedInner);
3333
AssertTasksAreEqual(inner, unwrappedInner);
3434
}
3535

@@ -43,8 +43,8 @@ public void Generic_Completed_Completed(Task<string> inner)
4343
{
4444
Task<Task<string>> outer = Task.FromResult(inner);
4545
Task<string> unwrappedInner = outer.Unwrap();
46-
// Assert.True(unwrappedInner.IsCompleted); // TODO: uncomment once we have implementation with this optimization
47-
// Assert.Same(inner, unwrappedInner); // TODO: uncomment once we have implementation with this optimization
46+
Assert.True(unwrappedInner.IsCompleted);
47+
Assert.Same(inner, unwrappedInner);
4848
AssertTasksAreEqual(inner, unwrappedInner);
4949
}
5050

@@ -407,7 +407,7 @@ public void Generic_AttachedToParent()
407407
/// <summary>
408408
/// Test that Unwrap with a non-generic task doesn't use TaskScheduler.Current.
409409
/// </summary>
410-
// [Fact] // TODO: Uncomment once new implementation matches mscorlib behavior and guarantees TaskScheduler.Default is used
410+
[Fact]
411411
public void NonGeneric_DefaultSchedulerUsed()
412412
{
413413
var scheduler = new CountingScheduler();
@@ -427,7 +427,7 @@ public void NonGeneric_DefaultSchedulerUsed()
427427
/// <summary>
428428
/// Test that Unwrap with a generic task doesn't use TaskScheduler.Current.
429429
/// </summary>
430-
// [Fact] // TODO: Uncomment once new implementation matches mscorlib behavior and guarantees TaskScheduler.Default is used
430+
[Fact]
431431
public void Generic_DefaultSchedulerUsed()
432432
{
433433
var scheduler = new CountingScheduler();
@@ -479,8 +479,7 @@ private static void AssertTasksAreEqual(Task expected, Task actual)
479479
Assert.Equal((IEnumerable<Exception>)expected.Exception.InnerExceptions, actual.Exception.InnerExceptions);
480480
break;
481481
case TaskStatus.Canceled:
482-
// TODO: Uncomment once we have new implementation that guarantees this
483-
//Assert.Equal(GetCanceledTaskToken(expected), GetCanceledTaskToken(actual));
482+
Assert.Equal(GetCanceledTaskToken(expected), GetCanceledTaskToken(actual));
484483
break;
485484
}
486485
}

0 commit comments

Comments
 (0)