【问题标题】:Is ConcurrentDictionary.GetOrAdd truly thread-safe?ConcurrentDictionary.GetOrAdd 真的是线程安全的吗?
【发布时间】:2018-12-17 20:38:13
【问题描述】:

如果该任务是为相同的输入创建的,我想在其中等待正在进行的任务的这段代码。这是我正在做的事情的最小复制。

private static ConcurrentDictionary<int, Task<int>> _tasks = new ConcurrentDictionary<int, Task<int>>();

private readonly ExternalService _service;


public async Task SampleTask(){
  var result = await _service.DoSomething();
  await Task.Delay(1000) //this task takes some time do finish
  return result;
}

public async Task<int> DoTask(int key) {
   var task = _tasks.GetOrAdd(key, _ => SampleTask());
   var taskResult = await task;
   _tasks.TryRemove(key, out task);
   return taskResult;
}

我正在编写一个测试,以确保当多个请求想要在(大致)同时执行任务时等待相同的任务。我通过模拟_service 并计算_service.DoSomething() 被调用的次数来做到这一点。如果对DoTask(int key) 的调用大致在同一时间进行,则应该只有一次。

但是,结果显示,如果我多次调用 DoTask(int key) 且调用之间的延迟小于 1~2 毫秒,则两个任务都将创建并执行 SampleTask() 的实例,而第二个任务将替换字典中的第一个。

考虑到这一点,我们可以说这个方法是真正的线程安全的吗?或者我的问题本身不是线程安全的问题吗?

【问题讨论】:

  • 当您调用GetOrAdd() 时,您传递给它的委托在某些情况下可能会被多次调用。这是你看到的吗?
  • 你也不在等待SampleTask()
  • @MatthewWatson,那会是什么情况?
  • @Tseng,我需要在工厂代表处等待吗?在它被分配给task 变量后,我正在等待它
  • @LuísGabrieldeAndrade 你一开始想做什么? ConcurrentDictionary 是线程安全的,但这并不意味着它包含的对象是线程安全的。它也不是任务队列。如果您想处理多条消息,您可能应该使用ActionBlock&lt;T&gt; 或 TPL DataflowBlock 库中的其他类。另一个不错的选择是使用System.Threading.Channels 来实现发布/订阅工作者。

标签: c# task


【解决方案1】:

引用the documentation(强调我的):

对于字典的修改和写入操作,ConcurrentDictionary&lt;TKey,TValue&gt; 使用细粒度锁定来确保线程安全。 (对字典的读取操作以无锁方式执行。)但是,valueFactory 委托在锁外调用以避免在锁下执行未知代码可能出现的问题。因此,GetOrAdd 对于ConcurrentDictionary&lt;TKey,TValue&gt; 类上的所有其他操作而言不是原子的。

由于另一个线程可以在valueFactory 生成一个值时插入一个键/值,你不能相信仅仅因为valueFactory 执行,它产生的值将被插入到字典中并返回。 如果在不同线程上同时调用GetOrAdd,valueFactory可能会被多次调用,但字典中只会添加一个键/值对。

因此,虽然字典是正确的线程安全的,但在您的情况下,对 valueFactory 或 _ =&gt; SampleTask() 的调用不能保证是唯一的。所以你的工厂函数应该能够接受这个事实。

你可以确认这个from the source:

public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
    if (key == null) throw new ArgumentNullException("key");
    if (valueFactory == null) throw new ArgumentNullException("valueFactory");

    TValue resultingValue;
    if (TryGetValue(key, out resultingValue))
    {
        return resultingValue;
    }
    TryAddInternal(key, valueFactory(key), false, true, out resultingValue);
    return resultingValue;
}

如您所见,valueFactory 在负责正确锁定字典的TryAddInternal 之外被调用。

但是,由于valueFactory 是一个 lambda 函数,它在您的情况下返回一个任务 (_ =&gt; SampleTask()),并且字典本身不会等待该任务,因此该函数将快速完成在遇到第一个await(设置异步状态机时)后返回不完整的Task。因此,除非调用非常快,否则任务应该很快添加到字典中,并且后续调用将重用相同的任务。

如果您要求在所有情况下只发生一次,您应该考虑自己锁定任务创建。由于它会很快完成(不管您的任务实际需要多长时间才能解决),锁定不会造成太大伤害。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-12
    相关资源
    最近更新 更多