【问题标题】:Task.WaitAny() — Checking for resultsTask.WaitAny() — 检查结果
【发布时间】:2018-08-30 19:17:46
【问题描述】:

我在一个数组中有一系列任务。如果一个任务是“好”,它会返回一个字符串。如果它是“坏的”:它返回一个空值。

我希望能够并行运行所有任务,一旦第一个返回“好”,然后取消其他任务并获得“好”结果。

我现在正在这样做,但问题是所有任务都需要运行,然后我循环遍历它们以寻找第一个好的结果。

List<Task<string>> tasks = new List<Task<string>>();
Task.WaitAll(tasks.ToArray());

【问题讨论】:

  • Doesn't Task.WhenAny 对你有用吗?
  • 问题是我需要第一个好的任务并获得数据,许多任务会返回但未能获得我需要的内容
  • this question 做你想做的事吗?

标签: c# async-await task task-parallel-library cancellationtokensource


【解决方案1】:

我希望能够并行运行所有任务,一旦第一个返回“Good”,然后取消其他任务并获得“Good”结果。

这是个误会,因为Cancellation in TPL is co-operative,所以Task一旦启动,就没有办法取消了。 CancellationToken 可以在 Task 启动之前工作或稍后抛出异常,如果请求取消,这意味着启动并采取必要的操作,例如从逻辑中抛出自定义异常

检查以下query,它列出了许多有趣的答案,但没有一个是取消。以下也是一种可能的选择:

public static class TaskExtension<T>
{
  public static async Task<T> FirstSuccess(IEnumerable<Task<T>> tasks, T goodResult)

    {
        // Create a List<Task<T>>
        var taskList = new List<Task<T>>(tasks);
        // Placeholder for the First Completed Task
        Task<T> firstCompleted = default(Task<T>);
        // Looping till the Tasks are available in the List
        while (taskList.Count > 0)
        {
            // Fetch first completed Task
            var currentCompleted = await Task.WhenAny(taskList);

            // Compare Condition
            if (currentCompleted.Status == TaskStatus.RanToCompletion
                && currentCompleted.Result.Equals(goodResult))
            {
                // Assign Task and Clear List
                firstCompleted = currentCompleted;
                break;
            }
            else
               // Remove the Current Task
               taskList.Remove(currentCompleted);
        }
        return (firstCompleted != default(Task<T>)) ? firstCompleted.Result : default(T);
    }
}

用法:

var t1 = new Task<string>(()=>"bad");

var t2 = new Task<string>(()=>"bad");

var t3 = new Task<string>(()=>"good");

var t4 = new Task<string>(()=>"good");

var taskArray = new []{t1,t2,t3,t4};

foreach(var tt in taskArray)
  tt.Start();

var finalTask = TaskExtension<string>.FirstSuccess(taskArray,"good");

Console.WriteLine(finalTask.Result);

您甚至可以返回Task&lt;Task&lt;T&gt;&gt;,而不是Task&lt;T&gt; 进行必要的逻辑处理

【讨论】:

  • taskList.Clear(); 是一个有趣的选择,为什么不简单的break 离开循环呢?或者更好的是,直接调用return firstCompleted.Result,这样就可以将最后一行替换为无条件的return default(T)
  • @KevinGosse 确实break 是一个更合乎逻辑的选择,出于某种奇怪的原因,我更热衷于清空收藏。已编辑代码,感谢您的评论。
  • 您是否应该在剩余任务上调用 .Result(在“Good”之后),以便被忽略任务中的异常不会破坏应用程序?
【解决方案2】:

您可以使用以下示例达到您想要的结果。

List<Task<string>> tasks = new List<Task<string>>();  

// ***Use ToList to execute the query and start the tasks.   
List<Task<string>> goodBadTasks = tasks.ToList();  

// ***Add a loop to process the tasks one at a time until none remain.  
while (goodBadTasks.Count > 0)  
{  
    // Identify the first task that completes.  
    Task<string> firstFinishedTask = await Task.WhenAny(goodBadTasks);  

    // ***Remove the selected task from the list so that you don't  
    // process it more than once.  
    goodBadTasks.Remove(firstFinishedTask);  

    // Await the completed task.  
    string firstFinishedTaskResult = await firstFinishedTask;  
    if(firstFinishedTaskResult.Equals("good")
         // do something

}  

EDIT :如果您想终止所有任务,您可以使用 CancellationToken。

更多详情请阅读docs

【讨论】:

  • 再增加一个完成的任务没有空结果的条件。首次执行 When any 应在 while 循环之外发生
  • 我认为任务结果检查用户可以对结果做任何他想做的事情。在他的情况下,结果是好是坏。
【解决方案3】:

我正在研究Task.WhenAny(),它将在第一个“已完成”任务时触发。不幸的是,从这个意义上说,一个已完成的任务基本上是任何事情......即使是一个异常也被认为是“完成”。据我所知,没有其他方法可以检查您所谓的“好”值。

虽然我认为您的问题没有令人满意的答案,但我认为您的问题可能有替代解决方案。考虑使用Parallel.ForEach

        Parallel.ForEach(tasks, (task, state) =>
        {
            if (task.Result != null)
                state.Stop();
        });

state.Stop() 将在发现非空结果时停止执行并行循环。

除了能够在找到“好”值时停止执行,它在许多(但不是所有)场景下都会表现得更好。

【讨论】:

  • Parallel Foreach 的问题在于它不能保证所有Enumerable 元素的并行执行,它具有内部调度机制,可以根据包括系统配置和感知持续时间在内的多个因素来诱导并行性的任务,因此提供的 IEnumerable 的各种元素也可以使用相同的线程池线程运行。您的回答将有助于找到具有良好返回值的第一个任务,但可能不是最快的。
  • 另外,如果Parallel ForEach 提供Task 类型的数组,通常情况并非如此,那么它们无论如何都会在单独的线程池线程上运行,而ParallelLoopState Stop 可能无法停止执行,除了我不确定中途停止线程的含义,如果这个调用真的这样做了
  • 我同意你的两个cmets。但是这个问题有特定的目标,我看不出他如何使用 Task.WaitAny() 或 Task.WhenAny() 来实现这些目标。因此,我提供了一个可能(或可能不会)实现他的目标的替代答案。
【解决方案4】:

使用Task.WhenAny它返回完成的任务。检查它是否为空。如果是,请将其从列表中删除并再次调用 Task.WhenAny。

如果没问题,取消列表中的所有任务(它们都应该有一个CancellationTokenSource.Token

编辑:

所有任务都应使用相同的CancellationTokenSource.Token。然后你只需要取消一次。 这里有一些代码需要澄清:

private async void button1_Click(object sender, EventArgs e)
{
    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

    List<Task<string>> tasks = new List<Task<string>>();
    tasks.Add(Task.Run<string>(() => // run your tasks
       {
           while (true)
           {
               if (cancellationTokenSource.Token.IsCancellationRequested)
               {
                   return null;
               }
               return "Result";  //string or null
           }
       }));
    while (tasks.Count > 0)
    {
        Task<string> resultTask = await Task.WhenAny(tasks);
        string result = await resultTask;
        if (result == null)
        {
            tasks.Remove(resultTask);
        }
        else
        {
            // success
            cancellationTokenSource.Cancel(); // will cancel all tasks
        }
    }
}

【讨论】:

  • 取消令牌是合作的,除非明确使用,否则不会有帮助
  • 取消没有什么神奇的(只是一个对象),至少当你远离链接的时候。
  • CancellationToken 不只是另一个对象,否则你不需要它,可以与任何随机对象一起工作,它是一个完整的机制来诱导任务的协同取消
  • 在你当前的代码中cancellationTokenSource没有关联任何Task来执行Cancellation。
  • 随心所欲地投反对票,但代码可以正常工作。您可以添加额外的检查以确保任务没有以异常结束。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-07-13
  • 1970-01-01
  • 1970-01-01
  • 2011-07-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多