【问题标题】:Is it thread safe to access ConcurrentDictionary values after getting these values with Linq使用 Linq 获取这些值后访问 ConcurrentDictionary 值是否线程安全
【发布时间】:2018-01-08 11:10:00
【问题描述】:

我有一个这样的 ConcurrentDictionary:

ConcurrentDictionary<int, Dto> concurrentDictionary = new ConcurrentDictionary<int, Dto>();

这是一个可读写的字典,可以被许多线程使用。我可以以线程安全的方式管理可写的一面。当我需要访问字典中的 Dto 时,我总是使用 Linq select 方法从中获取 Dto。

IEnumerable<Dto> dtos = concurrentDictionary.Select(p => p.Value);

我知道,并发字典上的 Linq 方法对于可读写的并发字典来说是无锁和线程安全的。在我使用 Linq 访问 Dto 之后,在这些 Dto 上使用一些读取函数是否是线程安全的?我的意思是在 foreach 循环中访问这些 Dto 或从中创建新列表并对这些 Dto 的属性进行一些过滤或排序?

ConcurrentDictionary<int, Dto> concurrentDictionary = new ConcurrentDictionary<int, Dto>();
for (int i = 0; i < 10000; i++)
{
    concurrentDictionary.TryAdd(i, new Dto()
    { Id = i, Name = RandomString(35), Type = "new", IsActive = true} );
}

IEnumerable<Dto> dtos = concurrentDictionary.Select(p => p.Value);

ArrayList arrayList = new ArrayList();
foreach (object obj in dtos)
{
    //is it thread safe accessing the Dto like this and adding them to new arraylist?
    arrayList.Add(obj);
}

//Is it thread safe accessing the Dto's properties like this? 
//Of course only for reading purposes.
string name = ((Dto) arrayList[0]).Name;

【问题讨论】:

  • DTO 是不可变的吗?如果是这样,那么没有问题。如果没有,那么您将无法知道某些代码是否可以改变它们。线程可以将 DTO 添加到字典中并保留对 DTO 的引用,然后它稍后会使用它来修改 DTO 的属性。
  • 与实际问题无关,但可以使用List时不要使用ArrayList。
  • Dtos 不是不可变的。 Dtos 可以添加到并发字典中,也可以从任何线程更新。在这种情况下,我可以为每个线程创建一个新列表并将每个线程中的这个列表用于阅读目的吗? IList dtos2= concurrentDictionary.Select(p => p.Value).ToList();这将创建一个新列表,但 Dto 不会被复制到新创建的列表中,它们只会通过引用传递。这个新创建的列表是否是线程安全的(当然是为了像过滤一样阅读)?
  • 术语“线程安全”只是意味着您的代码将function correctly in a multi-threaded environment,但什么是正确只能由您定义。 “您可以为每个线程创建一个新列表并将其用于阅读目的吗?”是的。此列表会受到其他线程上所做更改的影响吗?是的。如果您想要一个不受其他线程更改影响的列表,请使您的 Dtos 不可变。如果你不能这样做,那么你需要克隆你从并发字典中选择的Dtos。
  • @bornfromanegg 我明白你的意思。在这里,当我说“线程安全”时,我的意思是“代码将在多线程环境中正常运行”。所以我以这种方式问我的问题。是的。此列表可能会受到对其他线程所做的更改的影响。这不是问题。我只是对代码是否可以正常工作而不给出任何错误感到困惑。我还找到了一个关于并发字典和线程安全的很好的测试。这是link

标签: c# multithreading linq thread-safety concurrentdictionary


【解决方案1】:

根据GetEnumeratorConcurrentDictionary 的文档(突出显示是我的):

从字典返回的枚举器可以安全使用 与字典的读取和写入同时进行,但是 确实如此 不代表字典的即时快照。这 通过枚举器暴露的内容可能包含所做的修改 调用 GetEnumerator 后到字典中。

您是否认为这种线程安全取决于您需要什么语义。请注意,如果您确实想要快照,可以通过调用ToArray() 方法来获取:

IEnumerable<Dto> dtos = concurrentDictionary.ToArray().Select(p => p.Value);

根据documentationToArray()返回:

一个新数组,其中包含从复制的键和值对的快照 System.Collections.Concurrent.ConcurrentDictionary。

(请注意,这是属于ConcurrentDictionaryToArray 方法,而不是Linq ToArray() 方法,它将使用枚举器。)

最后,请注意,正如已经指出的那样,如果您的 DTO 是可变的,那么所有的赌注都将被取消。您可能会获得列表的快照,但没有什么可以阻止该列表的内容在另一个线程上被更改。

编辑:

通过阅读您在 cmets 中链接到的 page,要点如下:

ConcurrentDictionary 的所有公共和受保护成员 是线程安全的,可以从多个线程同时使用。 但是,通过其中一个接口访问的成员 ConcurrentDictionary 实现,包括扩展 方法,不保证是线程安全的,可能需要 由调用者同步。

这告诉我们,当与ConcurrentDictionary 一起使用时,不能保证 Linq 是线程安全的。 for each 应该没问题,因为这将调用 GetEnumerator,它具有本文开头提到的保证。我的直觉是Select 也可以,但不能保证。

所以要回答代码中的问题:

IEnumerable<Dto> dtos = concurrentDictionary.Select(p => p.Value);

ArrayList arrayList = new ArrayList();
foreach (object obj in dtos)
{
    //is it thread safe accessing the Dto like this and adding them to new arraylist?
    arrayList.Add(obj);
}

可能,但不能保证,无论如何,这对您没有任何好处,而不是 ToArray(),所以我将这里的所有内容替换为:

KeyValuePair<int, Dto>[] dtoArray = concurrentDictionary.ToArray();

其次:

//Is it thread safe accessing the Dto's properties like this? 
//Of course only for reading purposes.
string name = ((Dto) arrayList[0]).Name;

这是否安全与ConcurrentDictionary 无关,与您在Dto 中实现Name 方法有关。是的,您可以安全地访问Dto,但是您访问的任何属性或方法都需要是线程安全的。如果您按照上面的建议使用了ToArray(),那么

Dto dto = dtoArray[0].Value

是线程安全的,但是

string name = dtoArray[0].Value.Name

取决于Name 的实现。

【讨论】:

  • 谢谢@bornfromanegg。 ConcurrentDictionary 的 ToArray() 也是其他人推荐的方式。这也在这里讨论:stackoverflow.com/questions/11692389/…。它通过在内部锁定字典导致性能开销,但提供了无错误的功能。一个好的和有用的测试在这里:jeremyrsellars.github.io/no-new-legacy/posts/…
  • 我已经编辑了答案。另请注意,如果您担心性能,则不应使用ArrayList。事实上,永远不要使用ArrayList - 使用generic collection types 之一,或者使用数组。
  • 谢谢。我总是使用泛型集合而不是 ArrayList。使用 ArrayList 的示例来自其他一些代码。据我了解,在制作快照并创建新数组的 ToArray() 方法之后,我可以安全地使用 Linq。因此, concurrentDictionary.ToArray().Select(p => p.Value) 或 concurrentDictionary.ToArray().Select(p => p.Value).ToList() 对于无错误代码功能也是安全的。对吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-05-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-22
相关资源
最近更新 更多