Skip to content

Commit 074d247

Browse files
committed
Add TaskCompletionSource.SetFromTask
1 parent ca1b161 commit 074d247

File tree

7 files changed

+366
-15
lines changed

7 files changed

+366
-15
lines changed

src/libraries/System.Net.Requests/src/System/Net/TaskExtensions.cs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,7 @@ public static TaskCompletionSource<TResult> ToApm<TResult>(
1818

1919
task.ContinueWith(completedTask =>
2020
{
21-
bool shouldInvokeCallback = false;
22-
23-
if (completedTask.IsFaulted)
24-
{
25-
shouldInvokeCallback = tcs.TrySetException(completedTask.Exception!.InnerExceptions);
26-
}
27-
else if (completedTask.IsCanceled)
28-
{
29-
shouldInvokeCallback = tcs.TrySetCanceled();
30-
}
31-
else
32-
{
33-
shouldInvokeCallback = tcs.TrySetResult(completedTask.Result);
34-
}
21+
bool shouldInvokeCallback = tcs.TrySetFromTask(completedTask);
3522

3623
// Only invoke the callback if it exists AND we were able to transition the TCS
3724
// to the terminal state. If we couldn't transition the task it is because it was

src/libraries/System.Private.CoreLib/src/Resources/Strings.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3527,6 +3527,9 @@
35273527
<data name="Task_WaitMulti_NullTask" xml:space="preserve">
35283528
<value>The tasks array included at least one null element.</value>
35293529
</data>
3530+
<data name="Task_MustBeCompleted" xml:space="preserve">
3531+
<value>The provided task must have already completed.</value>
3532+
</data>
35303533
<data name="TaskT_ConfigureAwait_InvalidOptions" xml:space="preserve">
35313534
<value>Task&lt;TResult&gt;.ConfigureAwait does not support ConfigureAwaitOptions.SuppressThrowing. To suppress throwing, instead cast the Task&lt;TResult&gt; to its base class Task and await that with SuppressThrowing.</value>
35323535
</data>
@@ -4286,4 +4289,4 @@
42864289
<data name="Reflection_Disabled" xml:space="preserve">
42874290
<value>This operation is not available because the reflection support was disabled at compile time.</value>
42884291
</data>
4289-
</root>
4292+
</root>

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskCompletionSource.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,5 +285,75 @@ public bool TrySetCanceled(CancellationToken cancellationToken)
285285

286286
return rval;
287287
}
288+
289+
/// <summary>
290+
/// Transition the underlying <see cref="Task{TResult}"/> into the same completion state as the specified <paramref name="completedTask"/>.
291+
/// </summary>
292+
/// <param name="completedTask">The completed task whose completion status (including exception or cancellation information) should be copied to the underlying task.</param>
293+
/// <exception cref="ArgumentNullException"><paramref name="completedTask"/> is <see langword="null"/>.</exception>
294+
/// <exception cref="ArgumentException"><paramref name="completedTask"/> is not completed.</exception>
295+
/// <exception cref="InvalidOperationException">
296+
/// The underlying <see cref="Task{TResult}"/> is already in one of the three final states:
297+
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
298+
/// </exception>
299+
/// <remarks>
300+
/// This operation will return false if the <see cref="Task{TResult}"/> is already in one of the three final states:
301+
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
302+
/// </remarks>
303+
public void SetFromTask(Task completedTask)
304+
{
305+
if (!TrySetFromTask(completedTask))
306+
{
307+
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted);
308+
}
309+
}
310+
311+
/// <summary>
312+
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the same completion state as the specified <paramref name="completedTask"/>.
313+
/// </summary>
314+
/// <param name="completedTask">The completed task whose completion status (including exception or cancellation information) should be copied to the underlying task.</param>
315+
/// <returns><see langword="true"/> if the operation was successful; otherwise, <see langword="false"/>.</returns>
316+
/// <exception cref="ArgumentNullException"><paramref name="completedTask"/> is <see langword="null"/>.</exception>
317+
/// <exception cref="ArgumentException"><paramref name="completedTask"/> is not completed.</exception>
318+
/// <remarks>
319+
/// This operation will return false if the <see cref="Task{TResult}"/> is already in one of the three final states:
320+
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
321+
/// </remarks>
322+
public bool TrySetFromTask(Task completedTask)
323+
{
324+
ArgumentNullException.ThrowIfNull(completedTask);
325+
if (!completedTask.IsCompleted)
326+
{
327+
throw new ArgumentException(SR.Task_MustBeCompleted, nameof(completedTask));
328+
}
329+
330+
// Try to transition to the appropriate final state based on the state of completedTask.
331+
bool result = false;
332+
switch (completedTask.Status)
333+
{
334+
case TaskStatus.RanToCompletion:
335+
result = _task.TrySetResult();
336+
break;
337+
338+
case TaskStatus.Canceled:
339+
result = _task.TrySetCanceled(completedTask.CancellationToken, completedTask.GetCancellationExceptionDispatchInfo());
340+
break;
341+
342+
case TaskStatus.Faulted:
343+
result = _task.TrySetException(completedTask.GetExceptionDispatchInfos());
344+
break;
345+
}
346+
347+
// If we successfully transitioned to a final state, we're done. If we didn't, it's possible a concurrent operation
348+
// is still in the process of completing the task, and callers of this method expect the task to already be fully
349+
// completed when this method returns. As such, we spin until the task is completed, and then return whether this
350+
// call successfully did the transition.
351+
if (!result && !_task.IsCompleted)
352+
{
353+
_task.SpinUntilCompleted();
354+
}
355+
356+
return result;
357+
}
288358
}
289359
}

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskCompletionSource_T.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5+
using System.Runtime.ExceptionServices;
56

67
namespace System.Threading.Tasks
78
{
@@ -286,5 +287,75 @@ public bool TrySetCanceled(CancellationToken cancellationToken)
286287

287288
return rval;
288289
}
290+
291+
/// <summary>
292+
/// Transition the underlying <see cref="Task{TResult}"/> into the same completion state as the specified <paramref name="completedTask"/>.
293+
/// </summary>
294+
/// <param name="completedTask">The completed task whose completion status (including result, exception, or cancellation information) should be copied to the underlying task.</param>
295+
/// <exception cref="ArgumentNullException"><paramref name="completedTask"/> is <see langword="null"/>.</exception>
296+
/// <exception cref="ArgumentException"><paramref name="completedTask"/> is not completed.</exception>
297+
/// <exception cref="InvalidOperationException">
298+
/// The underlying <see cref="Task{TResult}"/> is already in one of the three final states:
299+
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
300+
/// </exception>
301+
/// <remarks>
302+
/// This operation will return false if the <see cref="Task{TResult}"/> is already in one of the three final states:
303+
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
304+
/// </remarks>
305+
public void SetFromTask(Task<TResult> completedTask)
306+
{
307+
if (!TrySetFromTask(completedTask))
308+
{
309+
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted);
310+
}
311+
}
312+
313+
/// <summary>
314+
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the same completion state as the specified <paramref name="completedTask"/>.
315+
/// </summary>
316+
/// <param name="completedTask">The completed task whose completion status (including result, exception, or cancellation information) should be copied to the underlying task.</param>
317+
/// <returns><see langword="true"/> if the operation was successful; otherwise, <see langword="false"/>.</returns>
318+
/// <exception cref="ArgumentNullException"><paramref name="completedTask"/> is <see langword="null"/>.</exception>
319+
/// <exception cref="ArgumentException"><paramref name="completedTask"/> is not completed.</exception>
320+
/// <remarks>
321+
/// This operation will return false if the <see cref="Task{TResult}"/> is already in one of the three final states:
322+
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
323+
/// </remarks>
324+
public bool TrySetFromTask(Task<TResult> completedTask)
325+
{
326+
ArgumentNullException.ThrowIfNull(completedTask);
327+
if (!completedTask.IsCompleted)
328+
{
329+
throw new ArgumentException(SR.Task_MustBeCompleted, nameof(completedTask));
330+
}
331+
332+
// Try to transition to the appropriate final state based on the state of completedTask.
333+
bool result = false;
334+
switch (completedTask.Status)
335+
{
336+
case TaskStatus.RanToCompletion:
337+
result = _task.TrySetResult(completedTask.Result);
338+
break;
339+
340+
case TaskStatus.Canceled:
341+
result = _task.TrySetCanceled(completedTask.CancellationToken, completedTask.GetCancellationExceptionDispatchInfo());
342+
break;
343+
344+
case TaskStatus.Faulted:
345+
result = _task.TrySetException(completedTask.GetExceptionDispatchInfos());
346+
break;
347+
}
348+
349+
// If we successfully transitioned to a final state, we're done. If we didn't, it's possible a concurrent operation
350+
// is still in the process of completing the task, and callers of this method expect the task to already be fully
351+
// completed when this method returns. As such, we spin until the task is completed, and then return whether this
352+
// call successfully did the transition.
353+
if (!result && !_task.IsCompleted)
354+
{
355+
_task.SpinUntilCompleted();
356+
}
357+
358+
return result;
359+
}
289360
}
290361
}

src/libraries/System.Runtime/ref/System.Runtime.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15352,13 +15352,15 @@ public TaskCompletionSource(System.Threading.Tasks.TaskCreationOptions creationO
1535215352
public System.Threading.Tasks.Task Task { get { throw null; } }
1535315353
public void SetCanceled() { }
1535415354
public void SetCanceled(System.Threading.CancellationToken cancellationToken) { }
15355+
public void SetFromTask(System.Threading.Tasks.Task completedTask) { throw null; }
1535515356
public void SetException(System.Collections.Generic.IEnumerable<System.Exception> exceptions) { }
1535615357
public void SetException(System.Exception exception) { }
1535715358
public void SetResult() { }
1535815359
public bool TrySetCanceled() { throw null; }
1535915360
public bool TrySetCanceled(System.Threading.CancellationToken cancellationToken) { throw null; }
1536015361
public bool TrySetException(System.Collections.Generic.IEnumerable<System.Exception> exceptions) { throw null; }
1536115362
public bool TrySetException(System.Exception exception) { throw null; }
15363+
public bool TrySetFromTask(System.Threading.Tasks.Task completedTask) { throw null; }
1536215364
public bool TrySetResult() { throw null; }
1536315365
}
1536415366
public partial class TaskCompletionSource<TResult>
@@ -15370,11 +15372,13 @@ public TaskCompletionSource(System.Threading.Tasks.TaskCreationOptions creationO
1537015372
public System.Threading.Tasks.Task<TResult> Task { get { throw null; } }
1537115373
public void SetCanceled() { }
1537215374
public void SetCanceled(System.Threading.CancellationToken cancellationToken) { }
15375+
public void SetFromTask(System.Threading.Tasks.Task<TResult> completedTask) { throw null; }
1537315376
public void SetException(System.Collections.Generic.IEnumerable<System.Exception> exceptions) { }
1537415377
public void SetException(System.Exception exception) { }
1537515378
public void SetResult(TResult result) { }
1537615379
public bool TrySetCanceled() { throw null; }
1537715380
public bool TrySetCanceled(System.Threading.CancellationToken cancellationToken) { throw null; }
15381+
public bool TrySetFromTask(System.Threading.Tasks.Task<TResult> completedTask) { throw null; }
1537815382
public bool TrySetException(System.Collections.Generic.IEnumerable<System.Exception> exceptions) { throw null; }
1537915383
public bool TrySetException(System.Exception exception) { throw null; }
1538015384
public bool TrySetResult(TResult result) { throw null; }

src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/TaskCompletionSourceTResultTests.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,5 +202,114 @@ private static void AssertCompletedTcsFailsToCompleteAgain<T>(TaskCompletionSour
202202
Assert.False(tcs.TrySetCanceled());
203203
Assert.False(tcs.TrySetCanceled(default));
204204
}
205+
206+
[Fact]
207+
public void SetFromTask_InvalidArgument_Throws()
208+
{
209+
TaskCompletionSource<object> tcs = new();
210+
AssertExtensions.Throws<ArgumentNullException>("completedTask", () => tcs.SetFromTask(null));
211+
AssertExtensions.Throws<ArgumentException>("completedTask", () => tcs.SetFromTask(new TaskCompletionSource<object>().Task));
212+
Assert.False(tcs.Task.IsCompleted);
213+
214+
tcs.SetResult(null);
215+
Assert.True(tcs.Task.IsCompletedSuccessfully);
216+
217+
AssertExtensions.Throws<ArgumentNullException>("completedTask", () => tcs.SetFromTask(null));
218+
AssertExtensions.Throws<ArgumentException>("completedTask", () => tcs.SetFromTask(new TaskCompletionSource<object>().Task));
219+
Assert.True(tcs.Task.IsCompletedSuccessfully);
220+
}
221+
222+
[Fact]
223+
public void SetFromTask_AlreadyCompleted_ReturnsFalseOrThrows()
224+
{
225+
object result = new();
226+
TaskCompletionSource<object> tcs = new();
227+
tcs.SetResult(result);
228+
229+
Assert.False(tcs.TrySetFromTask(Task.FromResult(new object())));
230+
Assert.False(tcs.TrySetFromTask(Task.FromException<object>(new Exception())));
231+
Assert.False(tcs.TrySetFromTask(Task.FromCanceled<object>(new CancellationToken(canceled: true))));
232+
233+
Assert.Throws<InvalidOperationException>(() => tcs.SetFromTask(Task.FromResult(new object())));
234+
Assert.Throws<InvalidOperationException>(() => tcs.SetFromTask(Task.FromException<object>(new Exception())));
235+
Assert.Throws<InvalidOperationException>(() => tcs.SetFromTask(Task.FromCanceled<object>(new CancellationToken(canceled: true))));
236+
237+
Assert.True(tcs.Task.IsCompletedSuccessfully);
238+
Assert.Same(result, tcs.Task.Result);
239+
}
240+
241+
[Theory]
242+
[InlineData(false)]
243+
[InlineData(true)]
244+
public void SetFromTask_CompletedSuccessfully(bool tryMethod)
245+
{
246+
TaskCompletionSource<object> tcs = new();
247+
Task<object> source = Task.FromResult(new object());
248+
249+
if (tryMethod)
250+
{
251+
Assert.True(tcs.TrySetFromTask(source));
252+
}
253+
else
254+
{
255+
tcs.SetFromTask(source);
256+
}
257+
258+
Assert.Same(source.Result, tcs.Task.Result);
259+
}
260+
261+
[Theory]
262+
[InlineData(false)]
263+
[InlineData(true)]
264+
public void SetFromTask_Faulted(bool tryMethod)
265+
{
266+
TaskCompletionSource<object> tcs = new();
267+
268+
var source = new TaskCompletionSource<object>();
269+
source.SetException([new FormatException(), new DivideByZeroException()]);
270+
271+
if (tryMethod)
272+
{
273+
Assert.True(tcs.TrySetFromTask(source.Task));
274+
}
275+
else
276+
{
277+
tcs.SetFromTask(source.Task);
278+
}
279+
280+
Assert.True(tcs.Task.IsFaulted);
281+
Assert.True(tcs.Task.Exception.InnerExceptions.Count == 2);
282+
}
283+
284+
[Theory]
285+
[InlineData(false)]
286+
[InlineData(true)]
287+
public void SetFromTask_Canceled(bool tryMethod)
288+
{
289+
TaskCompletionSource<object> tcs = new();
290+
291+
var cts = new CancellationTokenSource();
292+
cts.Cancel();
293+
Task<object> source = Task.FromCanceled<object>(cts.Token);
294+
295+
if (tryMethod)
296+
{
297+
Assert.True(tcs.TrySetFromTask(source));
298+
}
299+
else
300+
{
301+
tcs.SetFromTask(source);
302+
}
303+
304+
Assert.True(tcs.Task.IsCanceled);
305+
try
306+
{
307+
tcs.Task.GetAwaiter().GetResult();
308+
}
309+
catch (OperationCanceledException oce)
310+
{
311+
Assert.Equal(cts.Token, oce.CancellationToken);
312+
}
313+
}
205314
}
206315
}

0 commit comments

Comments
 (0)