【问题标题】:Waiting for a single task to fail out of a List<Task<..>> more cleanly, possibly with LINQ?等待单个任务更干净地从 List<Task<..>> 中失败,可能使用 LINQ?
【发布时间】:2016-04-21 17:22:59
【问题描述】:

在我的应用程序中,我有一个List&lt;Task&lt;Boolean&gt;&gt;,我在Task.Wait[..] 上确定它们是否成功完成(Result = true)。虽然如果在我等待的过程中 Task 完成并返回一个虚假值,我想取消所有其他 Task 我正在等待并基于此做一些事情。

我已经创建了两个“丑陋”的方法来做到这一点

// Create a CancellationToken and List<Task<..>> to work with
CancellationToken myCToken = new CancellationToken();
List<Task<Boolean>> myTaskList = new List<Task<Boolean>>();

//-- Method 1 --
    // Wait for one of the Tasks to complete and get its result
Boolean finishedTaskResult = myTaskList[Task.WaitAny(myTaskList.ToArray(), myCToken)].Result;

    // Continue waiting for Tasks to complete until there are none left or one returns false
    while (myTaskList.Count > 0 && finishedTaskResult)
    {
        // Wait for the next Task to complete
        finishedTaskResult = myTaskList[Task.WaitAny(myTaskList.ToArray(), myCToken)].Result;
        if (!finishedTaskResult) break;
    }
    // Act on finishTaskResult here

// -- Method 2 -- 
    // Create a label to 
    WaitForOneCompletion:
    int completedTaskIndex = Task.WaitAny(myTaskList.ToArray(), myCToken);

    if (myTaskList[completedTaskIndex].Result)
    {
        myTaskList.RemoveAt(completedTaskIndex);
        goto WaitForOneCompletion;
    }
    else
        ;// One task has failed to completed, handle appropriately 

我想知道是否有更清洁的方法可以做到这一点,可能使用 LINQ?

【问题讨论】:

    标签: c# linq task-parallel-library


    【解决方案1】:

    Jon SkeetStephen Toubmyself 都对“按完成排序”方法有所不同。

    但是,我发现通常人们不需要这种复杂性,只要他们的注意力稍微不同。

    在这种情况下,您有一组任务,并希望在其中一个返回 false 时立即取消它们。与其从控制器的角度思考(“调用代码如何做到这一点”),不如从任务的角度思考(“每个任务如何做到这一点”) ")。

    如果你引入一个更高级别的异步操作“做这个工作,然后在必要时取消”,你会发现你的调用代码很好地清理了:

    public async Task DoWorkAndCancel(Func<CancellationToken, Task<bool>> work,
        CancellationTokenSource cts)
    {
      if (!await work(cts.Token))
        cts.Cancel();
    }
    
    List<Func<CancellationToken, Task<bool>>> allWork = ...;
    var cts = new CancellationTokenSource();
    var tasks = allWork.Select(x => DoWorkAndCancel(x, cts));
    await Task.WhenAll(tasks);
    

    【讨论】:

    • 我喜欢这个。我现在对Task 感到很舒服,一些高级别的可能性正在向我敞开。我发现我正在查看我几周/几个月前编写的 TPL 代码,并质疑我当时的想法/方式。我将把它作为一个 TODO 并尽快完成,因为这确实很有意义。
    【解决方案2】:

    您可以使用以下方法获取任务序列并创建一个新的任务序列,该序列表示初始任务,但按照它们全部完成的顺序返回:

    public static IEnumerable<Task<T>> Order<T>(this IEnumerable<Task<T>> tasks)
    {
        var taskList = tasks.ToList();
    
        var taskSources = new BlockingCollection<TaskCompletionSource<T>>();
    
        var taskSourceList = new List<TaskCompletionSource<T>>(taskList.Count);
        foreach (var task in taskList)
        {
            var newSource = new TaskCompletionSource<T>();
            taskSources.Add(newSource);
            taskSourceList.Add(newSource);
    
            task.ContinueWith(t =>
            {
                var source = taskSources.Take();
    
                if (t.IsCanceled)
                    source.TrySetCanceled();
                else if (t.IsFaulted)
                    source.TrySetException(t.Exception.InnerExceptions);
                else if (t.IsCompleted)
                    source.TrySetResult(t.Result);
            }, CancellationToken.None, TaskContinuationOptions.PreferFairness, TaskScheduler.Default);
        }
    
        return taskSourceList.Select(tcs => tcs.Task);
    }
    

    现在您可以根据完成情况对任务进行排序,您可以基本上完​​全按照您的要求编写代码:

    foreach(var task in myTaskList.Order())
        if(!await task)
            cancellationTokenSource.Cancel();
    

    【讨论】:

    • 我想我明白这一点,我仍在考虑它,但现在看起来每个Task 都会连续运行,尽管这对我来说似乎是错误的。
    • @KDecker 是什么让您认为它们会按顺序执行?您的代码的整个前提是您已经启动了所有任务,因此您已经知道所有任务都在并行运行在我们甚至到达您问题中的代码之前
    • 这就是为什么这个想法对我来说没有意义。我猜是ContinueWithTaskCompletionSource 的使用让我很困惑。我见过两者都使用过,但只是很少使用,而且从来没有被我使用过。我“看到”这是如何工作的,但我无法解释。我想我只需要继续看下去。 // 不过还是谢谢!
    • 啊,我想我现在明白了。每个Task 都有一个延续,它从BlockingCollection 中获取一个TaskCompSource(因此它们“按顺序”完成)。一旦任务获得 TCS,它将 TCS 的结果设置为其结果。该方法返回一个List&lt;Task&gt;,每个都可以等待,但是任务会按照它们完成的顺序添加到这个列表中。 // 不是一个漂亮的解释,但我想我明白了。再次感谢!
    • 我冒昧地返回IEnumerable&lt;Task&lt;Tuple&lt;T, int&gt;&gt;&gt;,其中int 代表Task 在原始参数列表中的索引。这样我们即使在订购后也可以识别任务!
    【解决方案3】:

    使用Task.WhenAny 实现,您可以像扩展重载一样创建接收过滤器。

    此方法返回一个Task,当任何提供的任务完成并且结果通过过滤器时,该Task 将完成。

    类似这样的:

    static class TasksExtensions
    {
        public static Task<Task<T>> WhenAny<T>(this IList<Task<T>> tasks, Func<T, bool> filter)
        {
            CompleteOnInvokePromiseFilter<T> action = new CompleteOnInvokePromiseFilter<T>(filter);
    
            bool flag = false;
            for (int i = 0; i < tasks.Count; i++)
            {
                Task<T> completingTask = tasks[i];
    
                if (!flag)
                {
                    if (action.IsCompleted) flag = true;
                    else if (completingTask.IsCompleted)
                    {
                        action.Invoke(completingTask);
                        flag = true;
                    }
                    else completingTask.ContinueWith(t =>
                    {
                        action.Invoke(t);
                    });
                }
            }
    
            return action.Task;
        }
    }
    
    class CompleteOnInvokePromiseFilter<T>
    {
        private int firstTaskAlreadyCompleted;
        private TaskCompletionSource<Task<T>> source;
        private Func<T, bool> filter;
    
        public CompleteOnInvokePromiseFilter(Func<T, bool> filter)
        {
            this.filter = filter;
            source = new TaskCompletionSource<Task<T>>();
        }
    
        public void Invoke(Task<T> completingTask)
        {
            if (completingTask.Status == TaskStatus.RanToCompletion && 
                filter(completingTask.Result) && 
                Interlocked.CompareExchange(ref firstTaskAlreadyCompleted, 1, 0) == 0)
            {
                source.TrySetResult(completingTask);
            }
        }
    
        public Task<Task<T>> Task { get { return source.Task; } }
    
        public bool IsCompleted { get { return source.Task.IsCompleted; } }
    }
    

    你可以像这样使用这个扩展方法:

    List<Task<int>> tasks = new List<Task<int>>();    
    ...Initialize Tasks...
    
    var task = await tasks.WhenAny(x => x % 2 == 0);
    
    //In your case would be something like tasks.WhenAny(b => b);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-03-13
      • 2016-04-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多