问题
I am currently working on a web API that requires that I perform several different checks for rights and perform async operations to see if a user is allowed to make an API call. If the user can pass just one of the checks, they may continue, otherwise I need to throw an exception and boot them back out of the API with a 403 error.
I'd like to do something similar to this:
public async Task<object> APIMethod()
{
var tasks = new[] { CheckOne(), CheckTwo(), CheckThree() };
// On first success, ignore the result of the rest of the tasks and continue
// If none of them succeed, throw exception;
CoreBusinessLogic();
}
// Checks two and three are the same
public async Task CheckOne()
{
var result = await PerformSomeCheckAsync();
if (result == CustomStatusCode.Fail)
{
throw new Exception();
}
}
回答1:
Use Task.WhenAny
to keep track of tasks as they complete and perform the desired logic.
The following example demonstrates the explained logic
public class Program {
public static async Task Main() {
Console.WriteLine("Hello World");
await new Program().APIMethod();
}
public async Task APIMethod() {
var cts = new CancellationTokenSource();
var tasks = new[] { CheckOne(cts.Token), CheckTwo(cts.Token), CheckThree(cts.Token) };
var failCount = 0;
var runningTasks = tasks.ToList();
while (runningTasks.Count > 0) {
//As tasks complete
var finishedTask = await Task.WhenAny(runningTasks);
//remove completed task
runningTasks.Remove(finishedTask);
Console.WriteLine($"ID={finishedTask.Id}, Result={finishedTask.Result}");
//process task (in this case to check result)
var result = await finishedTask;
//perform desired logic
if (result == CustomStatusCode.Success) { //On first Success
cts.Cancel(); //ignore the result of the rest of the tasks
break; //and continue
}
failCount++;
}
// If none of them succeed, throw exception;
if (failCount == tasks.Length)
throw new InvalidOperationException();
//Core Business logic....
foreach (var t in runningTasks) {
Console.WriteLine($"ID={t.Id}, Result={t.Result}");
}
}
public async Task<CustomStatusCode> CheckOne(CancellationToken cancellationToken) {
await Task.Delay(1000); // mimic doing work
if (cancellationToken.IsCancellationRequested)
return CustomStatusCode.Canceled;
return CustomStatusCode.Success;
}
public async Task<CustomStatusCode> CheckTwo(CancellationToken cancellationToken) {
await Task.Delay(500); // mimic doing work
if (cancellationToken.IsCancellationRequested)
return CustomStatusCode.Canceled;
return CustomStatusCode.Fail;
}
public async Task<CustomStatusCode> CheckThree(CancellationToken cancellationToken) {
await Task.Delay(1500); // mimic doing work
if (cancellationToken.IsCancellationRequested)
return CustomStatusCode.Canceled;
return CustomStatusCode.Fail;
}
}
public enum CustomStatusCode {
Fail,
Success,
Canceled
}
The above example produces the following output
Hello World
ID=1, Result=Fail
ID=2, Result=Success
ID=3, Result=Canceled
observe in the example how a cancellation token was used to help cancel the remaining tasks that have not completed as yet when the first successful task completed. This can help improve performance if the called tasks are designed correctly.
If in your example PerformSomeCheckAsync
allows for cancellation, then it should be taken advantage of since, once a successful condition is found the remaining task are no longer needed, then leaving them running is not very efficient, depending on their load.
The provided example above can be aggregated into a reusable extension method
public static class WhenAnyExtension {
/// <summary>
/// Continues on first successful task, throw exception if all tasks fail
/// </summary>
/// <typeparam name="TResult">The type of task result</typeparam>
/// <param name="tasks">An IEnumerable<T> to return an element from.</param>
/// <param name="predicate">A function to test each element for a condition.</param>
/// <param name="cancellationToken"></param>
/// <returns>The first result in the sequence that passes the test in the specified predicate function.</returns>
public static async Task<TResult> WhenFirst<TResult>(this IEnumerable<Task<TResult>> tasks, Func<TResult, bool> predicate, CancellationToken cancellationToken = default(CancellationToken)) {
var running = tasks.ToList();
var taskCount = running.Count;
var failCount = 0;
var result = default(TResult);
while (running.Count > 0) {
if (cancellationToken.IsCancellationRequested) {
result = await Task.FromCanceled<TResult>(cancellationToken);
break;
}
var finished = await Task.WhenAny(running);
running.Remove(finished);
result = await finished;
if (predicate(result)) {
break;
}
failCount++;
}
// If none of them succeed, throw exception;
if (failCount == taskCount)
throw new InvalidOperationException("No task result satisfies the condition in predicate");
return result;
}
}
Simplifying the original example to
public static async Task Main()
{
Console.WriteLine("Hello World");
await new Program().APIMethod();
}
public async Task APIMethod()
{
var cts = new CancellationTokenSource();
var tasks = new[]{CheckThree(cts.Token), CheckTwo(cts.Token), CheckOne(cts.Token), CheckTwo(cts.Token), CheckThree(cts.Token)};
//continue on first successful task, throw exception if all tasks fail
await tasks.WhenFirst(result => result == CustomStatusCode.Success);
cts.Cancel(); //cancel remaining tasks if any
foreach (var t in tasks)
{
Console.WriteLine($"Id = {t.Id}, Result = {t.Result}");
}
}
which produces the following result
Hello World
Id = 1, Result = Canceled
Id = 2, Result = Fail
Id = 3, Result = Success
Id = 4, Result = Fail
Id = 5, Result = Canceled
based on the Check*
functions
来源:https://stackoverflow.com/questions/60347327/how-to-continue-on-first-successful-task-throw-exception-if-all-tasks-fail