我的工作假设是LINQ在与 System.Collections.Concurrent 集合(包括 ConcurrentDictionary )一起使用时是线程安全的。
(其他溢出帖似乎同意:link)
但是,检查LINQ OrderBy 扩展方法的实现情况表明,对于实现 ICollection 的并发集合子集,它似乎不是线程安全的(例如 ConcurrentDictionary )。
OrderedEnumerable GetEnumerator (source here)构建一个缓冲区结构(source here)的实例将集合转换为 ICollection ( ConcurrentDictionary 实现),然后执行collection.CopyTo,并将数组初始化为集合的大小。
因此,如果 ConcurrentDictionary (在这种情况下具体为 ICollection )在OrderBy操作期间增大,在初始化数组并复制到其中之间,此操作会抛出。
以下测试代码显示此异常:
(注意:我很感激在线程安全的集合上执行 OrderBy ,这种集合在你下面发生变化并没有那么有意义,但我不相信它应该抛出)
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Program
{
class Program
{
static void Main(string[] args)
{
try
{
int loop = 0;
while (true) //Run many loops until exception thrown
{
Console.WriteLine($"Loop: {++loop}");
_DoConcurrentDictionaryWork().Wait();
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
private static async Task _DoConcurrentDictionaryWork()
{
var concurrentDictionary = new ConcurrentDictionary<int, object>();
var keyGenerator = new Random();
var tokenSource = new CancellationTokenSource();
var orderByTaskLoop = Task.Run(() =>
{
var token = tokenSource.Token;
while (token.IsCancellationRequested == false)
{
//Keep ordering concurrent dictionary on a loop
var orderedPairs = concurrentDictionary.OrderBy(x => x.Key).ToArray(); //THROWS EXCEPTION HERE
//...do some more work with ordered snapshot...
}
});
var updateDictTaskLoop = Task.Run(() =>
{
var token = tokenSource.Token;
while (token.IsCancellationRequested == false)
{
//keep mutating dictionary on a loop
var key = keyGenerator.Next(0, 1000);
concurrentDictionary[key] = new object();
}
});
//Wait for 1 second
await Task.Delay(TimeSpan.FromSeconds(1));
//Cancel and dispose token
tokenSource.Cancel();
tokenSource.Dispose();
//Wait for orderBy and update loops to finish (now token cancelled)
await Task.WhenAll(orderByTaskLoop, updateDictTaskLoop);
}
}
}
OrderBy 引发异常会导致一些可能的结论:
1)我对LINQ作为并发集合的线程安全的假设是不正确的,并且在LINQ查询期间不会发生变异的集合上执行LINQ(无论它们是并发还是非并发)是安全的
2)LINQ OrderBy 的实现存在一个错误,并且尝试将源集合强制转换为ICollection并尝试执行集合副本是不正确的(并且应该只需简单介绍其迭代IEnumerable的默认行为。
3)我误解了这里发生的事情......
非常感谢!
答案 0 :(得分:7)
在OrderBy
(或其他LINQ方法)应始终使用源GetEnumerator
的{{1}}或者它应该在并发集合上是线程安全的任何地方都没有说明。所有承诺的都是这种方法
按照a的升序对序列的元素进行排序 键。
IEnumerable
在某种全球意义上也不是线程安全的。它对其上执行的其他操作具有线程安全性。更重要的是,文档说明了
ConcurrentDictionary的所有公共成员和受保护成员 是线程安全的,可以从多个线程同时使用。 但是,成员通过其中一个接口访问了 ConcurrentDictionary实现,包括扩展 方法,不保证是线程安全的,可能需要 由来电者同步。
所以,你的理解是正确的(ConcurrentDictionary
会看到OrderBy
你传递给它真的是IEnumerable
,然后会得到那个集合的长度,分配那个大小的缓冲区,然后会调用ICollection
,这当然不是任何类型集合的线程安全),但它不是ICollection.CopyTo
中的错误,因为OrderBy
和OrderBy
都不是曾经承诺过你的假设。
如果要在ConcurrentDictionary
上以线程安全的方式执行OrderBy
,则需要依赖承诺线程安全的方法。例如:
ConcurrentDictionary
如果你不想分配额外的数组(使用// note: this is NOT IEnumerable.ToArray()
// but public ToArray() method of ConcurrentDictionary itself
// it is guaranteed to be thread safe with respect to other operations
// on this dictionary
var snapshot = concurrentDictionary.ToArray();
// we are working on snapshot so no one other thread can modify it
// of course at this point real contents of dictionary might not be
// the same as our snapshot
var sorted = snapshot.OrderBy(c => c.Key);
),你可以使用ToArray
,它可以在这种情况下使用,但是我们再次处于没有实际意义的领域并依赖对于在未承诺的情况下可以安全使用的东西(Select(c => c)
也不会总是枚举你的集合。如果集合是数组或列表 - 它将快捷方式并使用索引器代替)。所以你可以像这样创建扩展方法:
Select
如果您想要安全并且不想分配数组,请使用它:
public static class Extensions {
public static IEnumerable<T> ForceEnumerate<T>(this ICollection<T> collection) {
foreach (var item in collection)
yield return item;
}
}
在这种情况下,我们强制枚举concurrentDictionary.ForceEnumerate().OrderBy(c => c.Key).ToArray();
(我们知道文档是安全的),然后将其传递给ConcurrentDictionary
,知道它不会对纯OrderBy
造成任何伤害。请注意,正如mjwills在注释中正确指出的那样,这与IEnumerable
不完全相同,因为ToArray
生成快照(锁定集合,防止在构建数组时进行修改)和ToArray
\ {{ 1}}没有获取任何锁(因此在枚举正在进行时可能会添加\删除项)。虽然我怀疑在执行有问题的事情时很重要 - 在Select
完成后的两种情况下 - 你都不知道你的有序结果是否反映当前的收集状态。