【问题标题】:Is ConcurrentDictionary safe to use?ConcurrentDictionary 可以安全使用吗?
【发布时间】:2021-05-25 10:16:14
【问题描述】:

我有三个独立的代码在不同的线程中运行。

线程任务 1:从设备读取数据并将其写入ConcurrentDictionary

线程任务2:将ConcurrentDictionary中的数据作为单独的文件写入计算机。

我在论坛上读过很多帖子说并发字典对于单独的线程是安全的。我还读到有锁定情况。事实上,我的脑海中出现了不止一个问号。

并发字典需要加锁吗?在哪些情况下需要锁定?如果需要加锁怎么办?以下方式使用会导致什么问题?

线程代码 1:每秒都有数据进来。

public void FillModuleBuffer(byte[] buffer, string IpPort)
{
     if (!CommunicationDictionary.dataLogList.ContainsKey(IpPort))
     {
         CommunicationDictionary.dataLogList.TryAdd(IpPort, buffer);
     }
}

线程代码 2: 下面的代码在计时器中工作。定时器持续时间为 200 毫秒。

if (CommunicationDictionary.dataLogList.ContainsKey(IpPort))
        {
          using (stream = File.Open(LogFilename, FileMode.Append, FileAccess.Write))
          {
              using (BinaryWriter writer = new BinaryWriter(stream))
              {
                 writer.Write(CommunicationDictionary.dataLogList[IpPort]);
                 writer.Flush();
                 writer.Close();
                 CommunicationDictionary.dataLogList.TryRemove(IpPort,out _);
               }
          }

}

注意:为了清楚起见,代码已被简化。

注2:在那之前我用过Dictionary。我遇到了一个非常不同的问题。在活动状态下,2-3 小时后,即使Dictionary 中没有数据,我也会收到数组超出索引范围的错误。

【问题讨论】:

  • Thread code 1: 删除ContainsKey 检查。它没有任何用处。
  • Thread code 2:ContainsKey 检查替换为 TryGetValue。或者,更好的是,考虑删除 both 并使用TryRemove only。请注意,如果您这样做,您将需要考虑如果您从字典中删除该项目并且流写入失败会发生什么。
  • @SomeBody 这可能是真的 - 但 OP 的代码不是 100% 安全的(例如,如果第三个线程从并发字典中删除项目,那么线程代码 2 最终将失败)。
  • 如果有一种方法绝对安全,而 可能 是安全的,我通常会鼓励“绝对安全”的方法。因为代码会随着时间而改变。
  • Saklanmaz 您可能有兴趣阅读我对When should I use ConcurrentDictionary and Dictionary? 的看法。简而言之,ConcurrentDictionary 可以看作是对受lock 保护的普通Dictionary 的性能优化。但是你在性能上获得了什么,你就失去了灵活性。 ConcurrentDictionary 可以用线程安全原子地做的事情是有限的。

标签: c# dictionary locking concurrentdictionary


【解决方案1】:

示例代码应该是线程安全的,但它显示了对如何使用并发字典的误解。例如:

 if (!CommunicationDictionary.dataLogList.ContainsKey(IpPort))
 {
     CommunicationDictionary.dataLogList.TryAdd(IpPort, buffer);
 }

这恰好起作用,因为只有一个线程添加到字典中,但由于存在单独的语句,字典可能会在它们之间发生变化。如果您查看 TryAdd 的文档,您会发现如果密钥已经存在,它将返回 false。所以不需要ContainsKey。有很多不同的方法,目的是同时做多个事情,以确保整个操作是原子的。

与阅读线程相同。所有对 concurrentDictionary 的访问都应替换为对 TryRemove 的一次调用

if (CommunicationDictionary.dataLogList.TryRemove(IpPort,out var data))
    {
      using (stream = File.Open(LogFilename, FileMode.Append, FileAccess.Write))
      {
          using (BinaryWriter writer = new BinaryWriter(stream))
          {
             writer.Write(data);
             writer.Flush();
             writer.Close();
           }
      }
}

请注意,这将保存一些数据块,并丢弃其他数据块,没有任何硬性保证会保存哪些块。这可能是预期的行为,但对于确保保存所有数据的队列来说,它会更常见。一个典型的实现会将concurrentQueue 包装在blockingCollection 中,其中包含一个或多个生产线程和一个消费线程。这避免了对单独计时器的需要。

【讨论】:

  • 首先感谢。据我了解,您建议使用 concurrentQueue 而不是将数据写入和删除缓冲区 [] 数组。如果我错了,请纠正。
  • 我在使用 TryRemove 时,如果文件忙无法写入,它仍然会删除 dataLogList 中的数据。否则,如果文件忙而无法写入,它将抛出异常并根据计时器的持续时间尝试重写。不幸的是,根据我的研究,无法检查文件是否繁忙。我在 TryRemove 上得到了 using 块。我认为问题已经通过这种方式消除了。
  • @Saklanmaz 这取决于你想要什么。如果您正在编写日志系统,我假设您希望记录所有数据,然后您当前的方法将不起作用,这里存在比线​​程安全更多的基本问题。如果可能的话,使用一个已经可以工作的现有日志系统。此外,从多个线程写入同一个文件可能不是一个好主意,您也许可以尝试将数据重新添加到字典中,但我建议您重新考虑您的设计。
  • 我想我弄错了。我不想从多个线程写入文件。一个线程检查连接并将传入数据写入 ConcurrentDictionary。另一个线程正在尝试将写入 ConcurrentDictionary 的数据写入文件。
  • @saklanmaz 如果因为文件繁忙而无法写入文件,那么可能有其他线程或进程对该文件具有写锁定。
猜你喜欢
  • 1970-01-01
  • 2018-03-20
  • 2010-10-11
  • 2013-01-06
  • 2016-04-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多