C# 我应该选择simple Dictionary还是ConcurrentDictionary处理任务并行库

C# 我应该选择simple Dictionary还是ConcurrentDictionary处理任务并行库,c#,concurrency,task-parallel-library,tpl-dataflow,C#,Concurrency,Task Parallel Library,Tpl Dataflow,以下是一个简化的场景-用户希望下载并处理一些数据: private ConcurrentDictionary<int, (string path, string name)> _testDictionary; public async Task StartDownload(List<(int id, string path, string name)> properties) { foreach (var (id, path, name) in propertie

以下是一个简化的场景-用户希望下载并处理一些数据:

private ConcurrentDictionary<int, (string path, string name)> _testDictionary;
public async Task StartDownload(List<(int id, string path, string name)> properties)
{
    foreach (var (id, path, name) in properties)
    {
        _testDictionary.TryAdd(id, (path, name));
    }
    await CreatePipeline(properties);
    //after returning I would like to check if _testDictionary contains any elements,
    //and what is their status
}

在结果块的末尾,将从_testDictionary中删除该项或根据它的运行方式进行更新。我愚蠢的问题是——如果我为创建管道的所有块设置MaxDegreeOfParallelism=1,并确保不会有多个管道同时运行,那么我真的需要ConcurrentDictionary来完成这个任务吗?或者简单的字典就足够了?我担心管道可能会在不同的线程上执行,从那里访问简单字典可能会导致问题。

是的,如果代码的结构确保多个线程不能同时访问字典,那么普通字典就足够了。如果您担心字典内部状态的可见性,以及某个线程在某个点看到过时状态的可能性,那么这不是问题,因为:

TPL包括在任务排队时以及在任务执行的开始/结束时的适当屏障,以便适当地使值可见

数据流 正如我所看到的,从_testDictionary的角度来看,StartDownload试图扮演生产者的角色,而CreatePipeline则扮演消费者的角色。Add和Remove调用分为两个不同的函数,这就是为什么需要将变量类设置为level

如果CreatePipeline同时包含两个调用,并返回所有未处理的元素,该怎么办

public async Task<Dictionary<int, (string path, string name)>> CreatePipeline(List<(int id, string path, string name)> properties)
{
    var unprocessed = new ConcurrentDictionary<int, (string path, string name)>(
        properties.ToDictionary(
            prop => prop.id, 
            prop => (prop.path, prop.name)));

    // var downloadBlock = ...;

    var resultsBlock = new ActionBlock<int>(
        (data) => unprocessed.TryRemove(data, out _), options);

    //...

    downloadBlock.Complete();
    await resultsBlock.Completion;

    return unprocessed.ToDictionary(
        dict => dict.Key,
        dict => dict.Value);
}
不可变字典 如果您想确保返回的未处理项目不能被其他线程修改,那么您可以利用

所以,如果我们把所有的东西放在一起,它可能看起来像这样:

await Task.WhenAll(properties.Select(downloadBlock.SendAsync));
public async Task StartDownload(List<(int id, string path, string name)> properties)
{
    var unprocessedProperties = await CreatePipeline(properties);
    foreach (var property in unprocessedProperties)
    {
        //TODO
    }
}

public async Task<ImmutableDictionary<int, (string path, string name)>> CreatePipeline(List<(int id, string path, string name)> properties)
{
    var options = new ExecutionDataflowBlockOptions {MaxDegreeOfParallelism = 1};

    var unprocessed = new ConcurrentDictionary<int, (string path, string name)>(
        properties.ToDictionary(
            prop => prop.id, 
            prop => (prop.path, prop.name)));

    var downloadBlock = new TransformBlock<(int id, string path, string name), int>(
        (data) => data.id, options);

    var resultsBlock = new ActionBlock<int>(
        (data) => unprocessed.TryRemove(data, out _), options);

    downloadBlock.LinkTo(resultsBlock, new DataflowLinkOptions { PropagateCompletion = true });
    await Task.WhenAll(properties.Select(downloadBlock.SendAsync));

    downloadBlock.Complete();
    await resultsBlock.Completion;

    return unprocessed.ToImmutableDictionary(
        dict => dict.Key, 
        dict => dict.Value); 
}
该结构可以很容易地进行修改,以注入缓冲块来平滑负载:

private readonly ITargetBlock<(int id, string path, string name)> downloadBlock;

public MyAwesomeBufferedClass()
{
    var transform = new TransformBlock<(int id, string path, string name), int>(
        (data) => data.id,
        new ExecutionDataflowBlockOptions {MaxDegreeOfParallelism = 1});

    var buffer = new BufferBlock<(int id, string path, string name)>(
        new DataflowBlockOptions() { BoundedCapacity = 100});

    buffer.LinkTo(transform, new DataflowLinkOptions {PropagateCompletion = true});
    downloadBlock = buffer;
}

public void StartDownload(List<(int id, string path, string name)> properties)
{
    _ = properties.Select(downloadBlock.SendAsync).ToList(); 
}

public async Task StopDownload()
{
    downloadBlock.Complete();
    await downloadBlock.Completion;
}

你是在问字典是线程安全的吗?不,不是。静态方法是线程安全的。请参阅MSDN文档。如果您是从一个线程与之交互,字典就可以了。如果没有,您需要ConcurrentDictionary。如果您不确定,请使用ConcurrentDictionary。作为旁注,您可能打算编写downloadBlock.Complete;而不是resultsBlock.Complete;。如果您对递归块实现感兴趣,其中每个处理的元素可以生成更多要处理的元素,那么有一个。嗨,Theodor,谢谢您的回答。是的,我知道普通字典不是线程安全的。我担心管道本身。比方说,如果我从UI线程创建并调用TPL管道,在我设置MaxDegreeOfParallelism=1的情况下,它是否保证总是在同一UI线程上执行其所有块?@niks否,这是不保证的。默认情况下,数据流块内的代码将在线程池线程上运行。如果希望它在UI线程上运行,则必须使用特定的配置每个块:the.Oh,这意味着如果没有任何特定的配置,我无法确保多个线程不会访问_testDictionary。那我最好还是用字典吧。少担心。谢谢大家!@niks从多个线程访问字典没有问题,前提是访问模式不是并发的,并且在切换点插入了内存屏障。但是,是的,如果使用ConcurrentDictionary可以让你心平气和,换取一点小小的性能提升,我同意这是一个很好的权衡-谢谢你的帮助,西奥多。我决定结束这个问题,接受彼得的回答。你的帖子直接回答了这个问题,但我觉得Peter的答案更深入地解释了为什么我使用特殊列表注册项目的方法一开始就存在缺陷。再次感谢你们,请投我一票!彼得,谢谢你这么详细的回答。我之所以想使用_testDictionary类级别变量,是因为当管道运行时,可以向_testDictionary添加新项,当wait CreatePipeline返回时,它会检查是否添加了任何新元素,然后再次运行CreatePipeline,直到没有新元素出现在_testDictionary中从管道执行返回。我决定在示例中省略这部分代码,因为它似乎与我的问题无关。@niks这是改变游戏规则的信息:D.它需要不同的体系结构。您的StartDownload应该只针对ITargetBlock调用SendAsync/Post。它可能是您当前的TransformBlock或TB前面的BufferBlock,用于平滑处理请求。您根本就需要_testDictionary,直接使用管道即可。我应该据此编辑我的答案吗?对此我很抱歉。这就是StackOverflow对我的作用——我问了一个我认为很重要的问题,结果我的想法完全错了,我最终得到了完全不同的解决方案:我不得不说,这是一个更好的解决方案。嗯,那太好了
如果您可以编辑您的答案来说明您将如何实施此解决方案,我将不胜感激。谢谢您的更新!据我所知,此解决方案使管道保持正常运行。给它食物。然而我有一个问题-我通常把等待下载块;在try-catch中,但在本例中,如果管道需要处理的项目之一抛出错误,并且我尚未调用wait downloadBlock.Completion;那个错误根本不会出现在catch中?如果我打电话给Wait downloadBlock.完成;这是管道的终点,我不能向它传递任何新的项目,哪种项目违背了这个解决方案的全部目的?@niks问得好。幸运的是,我有好消息告诉您,在这种情况下,错误处理可以变得非常简单。在TransformBlock中,每当发生异常时,您都可以使用多种可能性。如果它是一个暂时的故障,例如由于不可靠的网络,那么您可以简单地将完全相同的项目再次发布到队列中,然后尝试稍后处理它。也可以使用缓冲块创建死信队列。并链接一个ActionBlock以创建关于它们的日志条目。
private readonly ITargetBlock<(int id, string path, string name)> downloadBlock;

public MyAwesomeClass()
{
    downloadBlock = new TransformBlock<(int id, string path, string name), int>(
        (data) => data.id, 
        new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1 });
}

public void StartDownload(List<(int id, string path, string name)> properties)
{
   //Starts to send props, but does not await them
    _ = properties.Select(downloadBlock.SendAsync).ToList();
   
   //You can await the send operation if you wish 
}

public async Task StopDownload()
{
    downloadBlock.Complete();
    await downloadBlock.Completion;
}  
private readonly ITargetBlock<(int id, string path, string name)> downloadBlock;

public MyAwesomeBufferedClass()
{
    var transform = new TransformBlock<(int id, string path, string name), int>(
        (data) => data.id,
        new ExecutionDataflowBlockOptions {MaxDegreeOfParallelism = 1});

    var buffer = new BufferBlock<(int id, string path, string name)>(
        new DataflowBlockOptions() { BoundedCapacity = 100});

    buffer.LinkTo(transform, new DataflowLinkOptions {PropagateCompletion = true});
    downloadBlock = buffer;
}

public void StartDownload(List<(int id, string path, string name)> properties)
{
    _ = properties.Select(downloadBlock.SendAsync).ToList(); 
}

public async Task StopDownload()
{
    downloadBlock.Complete();
    await downloadBlock.Completion;
}