【问题标题】:Task Parallel Library WaitAny with specified result具有指定结果的任务并行库 WaitAny
【发布时间】:2017-04-01 03:06:15
【问题描述】:

我正在尝试编写一些代码来并行调用多个不同服务器的 Web 服务,因此 TPL 似乎是显而易见的选择。

我的 Web 服务调用中只有一个会返回我想要的结果,而其他所有调用都不会。我正在尝试找出一种有效地拥有Task.WaitAny 的方法,但只有在第一个匹配条件的Task 返回时才解除阻塞。

我尝试使用WaitAny,但无法确定过滤器的放置位置。我已经走到这一步了:

public void SearchServers()
{
    var servers = new[] {"server1", "server2", "server3", "server4"};
    var tasks = servers
                 .Select(s => Task<bool>.Factory.StartNew(server => CallServer((string)server), s))
                 .ToArray();

    Task.WaitAny(tasks); //how do I say "WaitAny where the result is true"?

    //Omitted: cancel any outstanding tasks since the correct server has been found
}

private bool CallServer(string server)
{
    //... make the call to the server and return the result ...
}

编辑:快速澄清,以防上面有任何混淆。我正在尝试执行以下操作:

  1. 对于每个服务器,启动一个Task 来检查它
  2. 或者,等到服务器返回 true(最多只有 1 个服务器会返回 true)
  3. 或者,等到所有服务器都返回 false,即没有匹配项。

【问题讨论】:

    标签: c# task-parallel-library


    【解决方案1】:

    我能想到的最好的方法是为每个Task 指定一个ContinueWith,检查结果,如果true 取消其他任务。对于取消任务,您可能需要使用CancellationToken

    var tasks = servers
        .Select(s => Task.Run(...)
            .ContinueWith(t =>
                if (t.Result) {
                    // cancel other threads
                }
            )
        ).ToArray();
    

    更新:另一种解决方案是 WaitAny 直到正确的任务完成(但它有一些缺点,例如从列表中删除已完成的任务并从剩余的任务中创建一个新数组是相当繁重的操作) :

    List<Task<bool>> tasks = servers.Select(s => Task<bool>.Factory.StartNew(server => CallServer((string)server), s)).ToList();
    
    bool result;
    do {
        int idx = Task.WaitAny(tasks.ToArray());
        result = tasks[idx].Result;
        tasks.RemoveAt(idx);
    } while (!result && tasks.Count > 0);
    
    // cancel other tasks
    

    更新 2:现在我会用 Rx 来做:

    [Fact]
    public async Task AwaitFirst()
    {
        var servers = new[] { "server1", "server2", "server3", "server4" };
        var server = await servers
            .Select(s => Observable
                .FromAsync(ct => CallServer(s, ct))
                .Where(p => p)
                .Select(_ => s)
            )
            .Merge()
            .FirstAsync();
        output.WriteLine($"Got result from {server}");
    }
    
    private async Task<bool> CallServer(string server, CancellationToken ct)
    {
        try
        {
            if (server == "server1")
            {
                await Task.Delay(TimeSpan.FromSeconds(1), ct);
                output.WriteLine($"{server} finished");
                return false;
            }
            if (server == "server2")
            {
                await Task.Delay(TimeSpan.FromSeconds(2), ct);
                output.WriteLine($"{server} finished");
                return false;
            }
            if (server == "server3")
            {
                await Task.Delay(TimeSpan.FromSeconds(3), ct);
                output.WriteLine($"{server} finished");
                return true;
            }
            if (server == "server4")
            {
                await Task.Delay(TimeSpan.FromSeconds(4), ct);
                output.WriteLine($"{server} finished");
                return true;
            }
        }
        catch(OperationCanceledException)
        {
            output.WriteLine($"{server} Cancelled");
            throw;
        }
    
        throw new ArgumentOutOfRangeException(nameof(server));
    }
    

    在我的机器上测试需要 3.32 秒(这意味着它没有等待第 4 个服务器),我得到以下输出:

    server1 finished
    server2 finished
    server3 finished
    server4 Cancelled
    Got result from server3
    

    【讨论】:

    • 我已经尝试了您的代码示例,但它似乎并没有按照我想要的方式工作。我不能使用WaitAny,因为它只会在第一个Task 完成时返回,即使服务器不是正确的。我也不能使用WaitAll,否则即使找到正确的任务,我也必须等待所有任务完成。理想情况下,我想做“等到其中一个任务返回 true 或所有任务完成(即所有服务器都不匹配)”。有什么办法可以做到吗?
    • 更新了我的答案,但现在我更喜欢@svick 的答案。
    • 标记为答案,因为我无法使用来自@svick 的答案,即使它看起来很不错,因为我使用的是 .Net 4.0。
    • 再次更新了我的答案,Rx 摇滚 :-)
    【解决方案2】:

    您可以使用OrderByCompletion() from the AsyncEx library,它会在任务完成时返回它们。您的代码可能类似于:

    var tasks = servers
        .Select(s => Task.Factory.StartNew(server => CallServer((string)server), s))
        .OrderByCompletion();
    
    foreach (var task in tasks)
    {
        if (task.Result)
        {
            Console.WriteLine("found");
            break;
        }
        Console.WriteLine("not found yet");
    }
    
    // cancel any outstanding tasks since the correct server has been found
    

    【讨论】:

    • task.Result 不会阻塞当前线程直到下一个任务完成?如果当前线程是例如,也许这也应该在另一个线程上完成。 UI线程?
    • @Jasd 是的,确实如此。但问题要求改进的WaitAny(),它也会阻塞。所以我假设这不是 UI 应用程序,或者它已经在单独的线程上运行。
    • 这看起来很整洁,但不幸的是我使用的是 VS2010,所以我不能使用那个库。
    • @AdamRodger 在这种情况下,您可以使用Jon Skeet's code for that methodStephen Toub's Interleaved(),这有点不同(它返回IEnumerable&lt;Task&lt;Task&gt;&gt;)。
    • Jon Skeet 的代码已移动 here
    【解决方案3】:

    使用 Interlocked.CompareExchange 就可以做到这一点,只有一个 Task 能够在 serverReturedData 上写入

        public void SearchServers()
            {
                ResultClass serverReturnedData = null;
                var servers = new[] {"server1", "server2", "server3", "server4"};
                var tasks = servers.Select(s => Task<bool>.Factory.StartNew(server => 
                {
                   var result = CallServer((string)server), s);
                   Interlocked.CompareExchange(ref serverReturnedData, result, null);
    
                }).ToArray();
    
                Task.WaitAny(tasks); //how do I say "WaitAny where the result is true"?
            //
            // use serverReturnedData as you want.
            // 
            }
    

    编辑:正如 Jasd 所说,上面的代码可以在变量 serverReturnedData 具有有效值之前返回(如果服务器返回空值,这可能会发生),以确保您可以将结果包装在自定义对象中。

    【讨论】:

    • Task.WaitAny(tasks); serverReturnedData 之后仍然可以更改(因为其他任务将完成)。此外,不保证完成的第一个任务是返回 true 的任务。
    • 上面的代码保证了第一个非空值将被存储在局部变量上,如果第一个返回的任务返回一个空值,是的,它会在之后改变。但这可以通过将结果包装在自定义对象中轻松解决。
    • 好的。所以首先,bool是一个值类型,不能为null,所以你可能想把Interlocked.CompareExchange的初始状态和第三个参数改成false。此外,完成的第一个任务(您在Task.WaitAny(tasks); 中等待的任务)不能保证是返回true 的任务。但是 IMO OP 想要等待返回 true 的第一个任务。
    • 没错,改变了我的例子,没有注意到布尔声明 xD。第二个可能是我在阅读问题时的误解
    【解决方案4】:

    这是基于 svick 回答的通用解决方案:

    public static async Task<T> GetFirstResult<T>(
    this IEnumerable<Func<CancellationToken, Task<T>>> taskFactories, 
    Action<Exception> exceptionHandler,
    Predicate<T> predicate)
    {
        T ret = default(T);
        var cts = new CancellationTokenSource();
        var proxified = taskFactories.Select(tf => tf(cts.Token)).ProxifyByCompletion();
        int i;
        for (i = 0; i < proxified.Length; i++)
        {
            try
            {
                ret = await proxified[i].ConfigureAwait(false);
            }
            catch (Exception e)
            {
                exceptionHandler(e);
                continue;
            }
            if (predicate(ret))
            {
                break;
            }
        }
    
        if (i == proxified.Length)
        {
            throw new InvalidOperationException("No task returned the expected value");
        }
        cts.Cancel(); //we have our value, so we can cancel the rest of the tasks
        for (int j = i+1; j < proxified.Length; j++)
        {
            //observe remaining tasks to prevent process crash 
            proxified[j].ContinueWith(
             t => exceptionHandler(t.Exception), TaskContinuationOptions.OnlyOnFaulted)
                       .Forget();
        }
        return ret;
    }
    

    ProxifyByCompletion 的实现方式为:

    public static Task<T>[] ProxifyByCompletion<T>(this IEnumerable<Task<T>> tasks)
    {
        var inputTasks = tasks.ToArray();
        var buckets = new TaskCompletionSource<T>[inputTasks.Length];
        var results = new Task<T>[inputTasks.Length];
        for (int i = 0; i < buckets.Length; i++)
        {
            buckets[i] = new TaskCompletionSource<T>();
            results[i] = buckets[i].Task;
        }
        int nextTaskIndex = -1;
        foreach (var inputTask in inputTasks)
        {
            inputTask.ContinueWith(completed =>
            {
                var bucket = buckets[Interlocked.Increment(ref nextTaskIndex)];
                if (completed.IsFaulted)
                {
                    Trace.Assert(completed.Exception != null);
                    bucket.TrySetException(completed.Exception.InnerExceptions);
                }
                else if (completed.IsCanceled)
                {
                    bucket.TrySetCanceled();
                }
                else
                {
                    bucket.TrySetResult(completed.Result);
                }
            }, CancellationToken.None, 
               TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
        }
        return results;
    }
    

    Forget是抑制CS4014的空方法:

    public static void Forget(this Task task) //suppress CS4014
    {
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-02-23
      • 1970-01-01
      • 2014-06-17
      • 2023-03-14
      • 2015-10-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多