【问题标题】:Task.WhenAll doesn't wait for the tasks to be done [closed]Task.WhenAll 不等待任务完成[关闭]
【发布时间】:2021-07-17 20:34:04
【问题描述】:

我正在编写从服务器获取不同数据部分的代码,将它们存储在 dict 中,以便它们按正确的顺序排列,然后在获取所有数据后将它们组合起来。但由于某种原因,它不起作用。它在获取所有内容之前进入CombineDataParts()。我不明白我做错了什么,因为所有方法都在等待。请帮忙!

public async Task<Data> GetTableData(List<string> partNames)
{
    List<Task> TaskList = new List<Task>();
    SortedDictionary<string, Data> dataList = new SortedDictionary<string, Data>();
    
    foreach (var name in partNames)
    {
        TaskList.Add(SaveDatainDict(name, DataList));
    }
    
    await Task.WhenAll(TaskList);
    return CombineDataParts(tableDataList.Select(t => t.Value).ToList());
}


private async Task SaveDatainDict(string part, SortedDictionary<string, Data> dataList)
{
    dataList[part] = await GetDataPart(part);
}


private async Task<Data> GetDataPart(string key)
{   
    using (var response = await client.GetData(key))
    {
        return ProcessData(response.Stream);
    }   
}

【问题讨论】:

  • SortedDictionary&lt;K,V&gt; 类不是线程安全的。您能否尝试在SaveDatainDict 方法的开头添加lock (dataList),看看是否有什么不同?
  • @TheodorZoulias:这是个糟糕的主意。使用SortedDictionary 需要锁定dataList[part] = (something); 操作,但您建议在await GetDataPart(part) 期间也持有锁定
  • 老实说,我认为这不是字典线程问题。我猜所有的keys 都是独一无二的?我很好奇您是否在return CombineDataParts 行上设置了断点,如果您在TaskList 中看到正确数量的任务并且所有IsCompletedSuccessfully 的值为true。他们中的任何一个都有例外吗?
  • 如果它线程问题,您能否通过保留GetDataPart 中的List&lt;Task&lt;Data&gt;&gt; 来消除中间数据结构和方法?然后Task.WhenAll will return an array of Data 可以在其上运行Select 语句。
  • @BenVoigt 是的,你是对的。只是按照我建议的方式添加 lock 可能甚至无法编译。

标签: c# .net multithreading asynchronous async-await


【解决方案1】:

为了让未来的读者更清楚,将我的评论分解为完整的答案...

正如@TheodorZoulais 所暗示的,这可能是SortedDictionary 不是线程安全的问题。这意味着当任务本身完成时,SortedDictionary 不能保证立即在线程间一致地反映状态,这在这里引起了头疼。这可能可以通过锁定正确的位置来解决……但有一种更简单的方法。

由于您只是使用SortedDictionary 来组合Tasks 的结果,我们可以通过稍微不同的使用Task.WhenAll 来完全避开问题。 Task.WhenAll 做了两件非常方便的事情:

  1. 为我们返回一个任务到await
  2. 如果我们传递给Task.WhenAllTasks 有结果,它会将这些结果合并到一个可枚举的结果中(link to the relevant documentation)

第一点是你现在的使用方式,这很好。但是如果我们也使用第二点,我们可以完全取消 SortedDictionary。

因此,只需对您的代码进行一些调整即可让我们实现线程安全...

public async Task<Data> GetTableData(List<string> partNames)
{
    // This time around, we will keep a list of Task<Data> instead of just Task
    List<Task<Data>> taskList = new List<Task<Data>>();

    foreach (var name in partNames)
    {
        // Directly save the tasks from GetDataPart
        taskList.Add(GetDataPart(name));
    }

    // Save the results combined by Task.WhenAll
    IEnumerable<Data> dataParts = await Task.WhenAll(taskList);

    // Looks like CombineDataParts was expecting a list, so convert and we're done!
    return CombineDataParts(dataParts.ToList());
}

private async Task<Data> GetDataPart(string key)
{   
    using (var response = await client.GetData(key))
    {
        return ProcessData(response.Stream);
    }   
}

这里有一些额外的说明:

  • SortedDictionary 确保对键进行排序,而此代码没有。此实现将按照名称在partNames 中的顺序返回值(引用:Tasks.WhenAll&lt;TResult&gt;(Task&lt;TResult&gt;[]) documentation 上的注释)。如果您想在此方法中强制执行特定排序(正如 SortedDictionary 在原始代码中所做的那样),您应该考虑使用类似 List.Sort 的方式对 partNames 进行排序
  • 根据您的偏好,您可以在顶部使用另一个 LINQ 语句而不是 foreach 循环来节省几行:IEnumerable&lt;Task&lt;Data&gt;&gt; taskList = partNames.Select(name =&gt; GetDataPart(name));

【讨论】:

  • 等等。所以保存在dataParts列表中的数据不会是有序的吗?我以为是因为任务以正确的顺序添加到列表中
  • @an007 啊,我明白了。正确的是,这些值以相同的任务顺序返回(来源:关于 [Tasks.WhenAll(Task[]) 文档](docs.microsoft.com/en-us/dotnet/api/…) 的备注)。我的意思是,排序取决于在调用此代码之前排序的列表。之前使用 SortedDictionary 的实现将强制执行排序,无论传入什么排序。我将更新我的注释以更清楚。
猜你喜欢
  • 2016-01-30
  • 2012-11-06
  • 2015-11-16
  • 2023-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多