Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TaskCompletionSource.SetFromTask #97077

Merged
merged 1 commit into from
Jan 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,7 @@ public static TaskCompletionSource<TResult> ToApm<TResult>(

task.ContinueWith(completedTask =>
{
bool shouldInvokeCallback = false;
if (completedTask.IsFaulted)
{
shouldInvokeCallback = tcs.TrySetException(completedTask.Exception!.InnerExceptions);
}
else if (completedTask.IsCanceled)
{
shouldInvokeCallback = tcs.TrySetCanceled();
}
else
{
shouldInvokeCallback = tcs.TrySetResult(completedTask.Result);
}
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
bool shouldInvokeCallback = tcs.TrySetFromTask(completedTask);
// Only invoke the callback if it exists AND we were able to transition the TCS
// to the terminal state. If we couldn't transition the task it is because it was
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3527,6 +3527,9 @@
<data name="Task_WaitMulti_NullTask" xml:space="preserve">
<value>The tasks array included at least one null element.</value>
</data>
<data name="Task_MustBeCompleted" xml:space="preserve">
<value>The provided task must have already completed.</value>
</data>
<data name="TaskT_ConfigureAwait_InvalidOptions" xml:space="preserve">
<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>
</data>
Expand Down Expand Up @@ -4286,4 +4289,4 @@
<data name="Reflection_Disabled" xml:space="preserve">
<value>This operation is not available because the reflection support was disabled at compile time.</value>
</data>
</root>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -285,5 +285,75 @@ public bool TrySetCanceled(CancellationToken cancellationToken)

return rval;
}

/// <summary>
/// Transition the underlying <see cref="Task{TResult}"/> into the same completion state as the specified <paramref name="completedTask"/>.
/// </summary>
/// <param name="completedTask">The completed task whose completion status (including exception or cancellation information) should be copied to the underlying task.</param>
/// <exception cref="ArgumentNullException"><paramref name="completedTask"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="completedTask"/> is not completed.</exception>
/// <exception cref="InvalidOperationException">
/// The underlying <see cref="Task{TResult}"/> is already in one of the three final states:
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
/// </exception>
/// <remarks>
/// This operation will return false if the <see cref="Task{TResult}"/> is already in one of the three final states:
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
/// </remarks>
public void SetFromTask(Task completedTask)
{
if (!TrySetFromTask(completedTask))
{
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted);
}
}

/// <summary>
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the same completion state as the specified <paramref name="completedTask"/>.
/// </summary>
/// <param name="completedTask">The completed task whose completion status (including exception or cancellation information) should be copied to the underlying task.</param>
/// <returns><see langword="true"/> if the operation was successful; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="completedTask"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="completedTask"/> is not completed.</exception>
/// <remarks>
/// This operation will return false if the <see cref="Task{TResult}"/> is already in one of the three final states:
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
/// </remarks>
public bool TrySetFromTask(Task completedTask)
{
ArgumentNullException.ThrowIfNull(completedTask);
if (!completedTask.IsCompleted)
{
throw new ArgumentException(SR.Task_MustBeCompleted, nameof(completedTask));
}

// Try to transition to the appropriate final state based on the state of completedTask.
bool result = false;
switch (completedTask.Status)
{
case TaskStatus.RanToCompletion:
result = _task.TrySetResult();
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
break;

case TaskStatus.Canceled:
result = _task.TrySetCanceled(completedTask.CancellationToken, completedTask.GetCancellationExceptionDispatchInfo());
break;

case TaskStatus.Faulted:
result = _task.TrySetException(completedTask.GetExceptionDispatchInfos());
break;
}

// If we successfully transitioned to a final state, we're done. If we didn't, it's possible a concurrent operation
// is still in the process of completing the task, and callers of this method expect the task to already be fully
// completed when this method returns. As such, we spin until the task is completed, and then return whether this
// call successfully did the transition.
if (!result && !_task.IsCompleted)
{
_task.SpinUntilCompleted();
}

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Runtime.ExceptionServices;

namespace System.Threading.Tasks
{
Expand Down Expand Up @@ -286,5 +287,75 @@ public bool TrySetCanceled(CancellationToken cancellationToken)

return rval;
}

/// <summary>
/// Transition the underlying <see cref="Task{TResult}"/> into the same completion state as the specified <paramref name="completedTask"/>.
/// </summary>
/// <param name="completedTask">The completed task whose completion status (including result, exception, or cancellation information) should be copied to the underlying task.</param>
/// <exception cref="ArgumentNullException"><paramref name="completedTask"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="completedTask"/> is not completed.</exception>
/// <exception cref="InvalidOperationException">
/// The underlying <see cref="Task{TResult}"/> is already in one of the three final states:
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
/// </exception>
/// <remarks>
/// This operation will return false if the <see cref="Task{TResult}"/> is already in one of the three final states:
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
/// </remarks>
public void SetFromTask(Task<TResult> completedTask)
{
if (!TrySetFromTask(completedTask))
{
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted);
}
}

/// <summary>
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the same completion state as the specified <paramref name="completedTask"/>.
/// </summary>
/// <param name="completedTask">The completed task whose completion status (including result, exception, or cancellation information) should be copied to the underlying task.</param>
/// <returns><see langword="true"/> if the operation was successful; otherwise, <see langword="false"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="completedTask"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="completedTask"/> is not completed.</exception>
/// <remarks>
/// This operation will return false if the <see cref="Task{TResult}"/> is already in one of the three final states:
/// <see cref="TaskStatus.RanToCompletion"/>, <see cref="TaskStatus.Faulted"/>, or <see cref="TaskStatus.Canceled"/>.
/// </remarks>
public bool TrySetFromTask(Task<TResult> completedTask)
{
ArgumentNullException.ThrowIfNull(completedTask);
if (!completedTask.IsCompleted)
{
throw new ArgumentException(SR.Task_MustBeCompleted, nameof(completedTask));
}

// Try to transition to the appropriate final state based on the state of completedTask.
bool result = false;
switch (completedTask.Status)
{
case TaskStatus.RanToCompletion:
result = _task.TrySetResult(completedTask.Result);
break;

case TaskStatus.Canceled:
result = _task.TrySetCanceled(completedTask.CancellationToken, completedTask.GetCancellationExceptionDispatchInfo());
break;

case TaskStatus.Faulted:
result = _task.TrySetException(completedTask.GetExceptionDispatchInfos());
break;
}

// If we successfully transitioned to a final state, we're done. If we didn't, it's possible a concurrent operation
// is still in the process of completing the task, and callers of this method expect the task to already be fully
// completed when this method returns. As such, we spin until the task is completed, and then return whether this
// call successfully did the transition.
if (!result && !_task.IsCompleted)
{
_task.SpinUntilCompleted();
}

return result;
}
}
}
4 changes: 4 additions & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15352,13 +15352,15 @@ public TaskCompletionSource(System.Threading.Tasks.TaskCreationOptions creationO
public System.Threading.Tasks.Task Task { get { throw null; } }
public void SetCanceled() { }
public void SetCanceled(System.Threading.CancellationToken cancellationToken) { }
public void SetFromTask(System.Threading.Tasks.Task completedTask) { throw null; }
public void SetException(System.Collections.Generic.IEnumerable<System.Exception> exceptions) { }
public void SetException(System.Exception exception) { }
public void SetResult() { }
public bool TrySetCanceled() { throw null; }
public bool TrySetCanceled(System.Threading.CancellationToken cancellationToken) { throw null; }
public bool TrySetException(System.Collections.Generic.IEnumerable<System.Exception> exceptions) { throw null; }
public bool TrySetException(System.Exception exception) { throw null; }
public bool TrySetFromTask(System.Threading.Tasks.Task completedTask) { throw null; }
public bool TrySetResult() { throw null; }
}
public partial class TaskCompletionSource<TResult>
Expand All @@ -15370,11 +15372,13 @@ public TaskCompletionSource(System.Threading.Tasks.TaskCreationOptions creationO
public System.Threading.Tasks.Task<TResult> Task { get { throw null; } }
public void SetCanceled() { }
public void SetCanceled(System.Threading.CancellationToken cancellationToken) { }
public void SetFromTask(System.Threading.Tasks.Task<TResult> completedTask) { throw null; }
public void SetException(System.Collections.Generic.IEnumerable<System.Exception> exceptions) { }
public void SetException(System.Exception exception) { }
public void SetResult(TResult result) { }
public bool TrySetCanceled() { throw null; }
public bool TrySetCanceled(System.Threading.CancellationToken cancellationToken) { throw null; }
public bool TrySetFromTask(System.Threading.Tasks.Task<TResult> completedTask) { throw null; }
public bool TrySetException(System.Collections.Generic.IEnumerable<System.Exception> exceptions) { throw null; }
public bool TrySetException(System.Exception exception) { throw null; }
public bool TrySetResult(TResult result) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,114 @@ private static void AssertCompletedTcsFailsToCompleteAgain<T>(TaskCompletionSour
Assert.False(tcs.TrySetCanceled());
Assert.False(tcs.TrySetCanceled(default));
}

[Fact]
public void SetFromTask_InvalidArgument_Throws()
{
TaskCompletionSource<object> tcs = new();
AssertExtensions.Throws<ArgumentNullException>("completedTask", () => tcs.SetFromTask(null));
AssertExtensions.Throws<ArgumentException>("completedTask", () => tcs.SetFromTask(new TaskCompletionSource<object>().Task));
Assert.False(tcs.Task.IsCompleted);

tcs.SetResult(null);
Assert.True(tcs.Task.IsCompletedSuccessfully);

AssertExtensions.Throws<ArgumentNullException>("completedTask", () => tcs.SetFromTask(null));
AssertExtensions.Throws<ArgumentException>("completedTask", () => tcs.SetFromTask(new TaskCompletionSource<object>().Task));
Assert.True(tcs.Task.IsCompletedSuccessfully);
}

[Fact]
public void SetFromTask_AlreadyCompleted_ReturnsFalseOrThrows()
{
object result = new();
TaskCompletionSource<object> tcs = new();
tcs.SetResult(result);

Assert.False(tcs.TrySetFromTask(Task.FromResult(new object())));
Assert.False(tcs.TrySetFromTask(Task.FromException<object>(new Exception())));
Assert.False(tcs.TrySetFromTask(Task.FromCanceled<object>(new CancellationToken(canceled: true))));

Assert.Throws<InvalidOperationException>(() => tcs.SetFromTask(Task.FromResult(new object())));
Assert.Throws<InvalidOperationException>(() => tcs.SetFromTask(Task.FromException<object>(new Exception())));
Assert.Throws<InvalidOperationException>(() => tcs.SetFromTask(Task.FromCanceled<object>(new CancellationToken(canceled: true))));

Assert.True(tcs.Task.IsCompletedSuccessfully);
Assert.Same(result, tcs.Task.Result);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void SetFromTask_CompletedSuccessfully(bool tryMethod)
{
TaskCompletionSource<object> tcs = new();
Task<object> source = Task.FromResult(new object());

if (tryMethod)
{
Assert.True(tcs.TrySetFromTask(source));
}
else
{
tcs.SetFromTask(source);
}

Assert.Same(source.Result, tcs.Task.Result);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void SetFromTask_Faulted(bool tryMethod)
{
TaskCompletionSource<object> tcs = new();

var source = new TaskCompletionSource<object>();
source.SetException([new FormatException(), new DivideByZeroException()]);

if (tryMethod)
{
Assert.True(tcs.TrySetFromTask(source.Task));
}
else
{
tcs.SetFromTask(source.Task);
}

Assert.True(tcs.Task.IsFaulted);
Assert.True(tcs.Task.Exception.InnerExceptions.Count == 2);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void SetFromTask_Canceled(bool tryMethod)
{
TaskCompletionSource<object> tcs = new();

var cts = new CancellationTokenSource();
cts.Cancel();
Task<object> source = Task.FromCanceled<object>(cts.Token);

if (tryMethod)
{
Assert.True(tcs.TrySetFromTask(source));
}
else
{
tcs.SetFromTask(source);
}

Assert.True(tcs.Task.IsCanceled);
try
{
tcs.Task.GetAwaiter().GetResult();
}
catch (OperationCanceledException oce)
{
Assert.Equal(cts.Token, oce.CancellationToken);
}
}
}
}
Loading
Loading