如何使用谓词实现Task.WhenAny()

时间:2016-07-10 06:11:57

标签: c# task-parallel-library

我想异步执行多个任务,每个任务都会运行http请求,可以抛出异常或安全结束。我需要在第一个任务成功完成或所有任务都失败时完成。请指教。

6 个答案:

答案 0 :(得分:5)

等待任何任务并在满足条件时返回任务。否则,请等待其他任务,直到没有其他任务等待。

public static async Task<Task> WhenAny( IEnumerable<Task> tasks, Predicate<Task> condition )
{
    var tasklist = tasks.ToList();
    while ( tasklist.Count > 0 )
    {
        var task = await Task.WhenAny( tasklist );
        if ( condition( task ) )
            return task;
        tasklist.Remove( task );
    }
    return null;
}

简单检查

var tasks = new List<Task> {
    Task.FromException( new Exception() ),
    Task.FromException( new Exception() ),
    Task.FromException( new Exception() ),
    Task.CompletedTask, };

var completedTask = WhenAny( tasks, t => t.Status == TaskStatus.RanToCompletion ).Result;

if ( tasks.IndexOf( completedTask ) != 3 )
    throw new Exception( "not expected" );

答案 1 :(得分:2)

public static Task<Task<T>> WhenFirst<T>(IEnumerable<Task<T>> tasks, Func<Task<T>, bool> predicate)
{
    if (tasks == null) throw new ArgumentNullException(nameof(tasks));
    if (predicate == null) throw new ArgumentNullException(nameof(predicate));

    var tasksArray = (tasks as IReadOnlyList<Task<T>>) ?? tasks.ToArray();
    if (tasksArray.Count == 0) throw new ArgumentException("Empty task list", nameof(tasks));
    if (tasksArray.Any(t => t == null)) throw new ArgumentException("Tasks contains a null reference", nameof(tasks));

    var tcs = new TaskCompletionSource<Task<T>>();
    var count = tasksArray.Count;

    Action<Task<T>> continuation = t =>
        {
            if (predicate(t))
            {
                tcs.TrySetResult(t);
            }
            if (Interlocked.Decrement(ref count) == 0)
            {
                tcs.TrySetResult(null);
            }
        };

    foreach (var task in tasksArray)
    {
        task.ContinueWith(continuation);
    }

    return tcs.Task;
}

样本用法:

var task = await WhenFirst(tasks, t => t.Status == TaskStatus.RanToCompletion);

if (task != null)
    var value = await task;

请注意,这并不会传播失败任务的异常(正如WhenAny没有)。

您还可以为非通用Task创建此版本。

答案 2 :(得分:1)

public static Task<T> GetFirstResult<T>(
    ICollection<Func<CancellationToken, Task<T>>> taskFactories, 
    Predicate<T> predicate) where T : class
{
    var tcs = new TaskCompletionSource<T>();
    var cts = new CancellationTokenSource();

    int completedCount = 0;
    // in case you have a lot of tasks you might need to throttle them 
    //(e.g. so you don't try to send 99999999 requests at the same time)
    // see: http://stackoverflow.com/a/25877042/67824
    foreach (var taskFactory in taskFactories)
    {
        taskFactory(cts.Token).ContinueWith(t => 
        {
            if (t.Exception != null)
            {
                Console.WriteLine($"Task completed with exception: {t.Exception}");
            }
            else if (predicate(t.Result))
            {
                cts.Cancel();
                tcs.TrySetResult(t.Result);
            }

            if (Interlocked.Increment(ref completedCount) == taskFactories.Count)
            {
                tcs.SetException(new InvalidOperationException("All tasks failed"));
            }

        }, cts.Token);
    }

    return tcs.Task;
}

样本用法:

using System.Net.Http;
var client = new HttpClient();
var response = await GetFirstResult(
    new Func<CancellationToken, Task<HttpResponseMessage>>[] 
    {
        ct => client.GetAsync("http://microsoft123456.com", ct),
        ct => client.GetAsync("http://microsoft123456.com", ct),
        ct => client.GetAsync("http://microsoft123456.com", ct),
        ct => client.GetAsync("http://microsoft123456.com", ct),
        ct => client.GetAsync("http://microsoft123456.com", ct),
        ct => client.GetAsync("http://microsoft123456.com", ct),
        ct => client.GetAsync("http://microsoft123456.com", ct),
        ct => client.GetAsync("http://microsoft.com", ct),
        ct => client.GetAsync("http://microsoft123456.com", ct),
        ct => client.GetAsync("http://microsoft123456.com", ct),
    }, 
    rm => rm.IsSuccessStatusCode);
Console.WriteLine($"Successful response: {response}");

答案 3 :(得分:0)

这是优秀Eli Arbel的answer的尝试改进。这些是改进点:

  1. predicate中的异常被传播为所返回任务的错误。
  2. 在接受任务作为结果之后,不会调用predicate
  3. predicate是在原始SynchronizationContext中执行的。这样就可以访问UI元素(如果从UI线程调用了WhenFirst方法)
  4. 直接枚举源IEnumerable<Task<T>>,而无需先转换为数组。
public static Task<Task<T>> WhenFirst<T>(IEnumerable<Task<T>> tasks,
    Func<Task<T>, bool> predicate)
{
    if (tasks == null) throw new ArgumentNullException(nameof(tasks));
    if (predicate == null) throw new ArgumentNullException(nameof(predicate));

    var tcs = new TaskCompletionSource<Task<T>>(
        TaskCreationOptions.RunContinuationsAsynchronously);
    var pendingCount = 1; // The initial 1 represents the enumeration itself
    foreach (var task in tasks)
    {
        if (task == null) throw new ArgumentException($"The {nameof(tasks)}" +
            " argument included a null value.", nameof(tasks));
        Interlocked.Increment(ref pendingCount);
        HandleTaskCompletion(task);
    }
    if (Interlocked.Decrement(ref pendingCount) == 0) tcs.TrySetResult(null);
    return tcs.Task;

    async void HandleTaskCompletion(Task<T> task)
    {
        try
        {
            await task; // Continue on the captured context
        }
        catch { } // Ignore exception

        if (tcs.Task.IsCompleted) return;

        try
        {
            if (predicate(task))
                tcs.TrySetResult(task);
            else
                if (Interlocked.Decrement(ref pendingCount) == 0)
                    tcs.TrySetResult(null);
        }
        catch (Exception ex)
        {
            tcs.TrySetException(ex);
        }
    }
}

答案 4 :(得分:0)

另一种实现方法,与Sir Rufo's answer非常相似,但使用的是AsyncEnumerableIx.NET

实施一些帮助程序方法,以在完成任务后立即流式传输所有任务:

static IAsyncEnumerable<Task<T>> WhenCompleted<T>(IEnumerable<Task<T>> source) =>
    AsyncEnumerable.Create(_ =>
    {
        var tasks = source.ToList();
        Task<T> current = null;
        return AsyncEnumerator.Create(
            async () => tasks.Any() && tasks.Remove(current = await Task.WhenAny(tasks)), 
            () => current,
            async () => { });
    });
}

然后可以按完成顺序处理任务,例如根据要求返回第一个匹配的对象:

await WhenCompleted(tasks).FirstOrDefault(t => t.Status == TaskStatus.RanToCompletion)

答案 5 :(得分:0)

只是想添加一些使用 List.Remove 的答案@Peebo 和 @SirRufo(因为我还不能评论)

我会考虑使用:

var tasks = source.ToHashSet();

代替:

var tasks = source.ToList();

所以删除会更有效