集合

选择集合

一般情况下,应使用泛型集合。 下表介绍了一些常用的集合方案和可用于这些方案的集合类。 如果你是使用泛型集合的新手,此表将帮助你选择最适合你的任务的泛型集合。

选择集合
我要……泛型集合选项非泛型集合选项线程安全或不可变集合选项
将项存储为键/值对以通过键进行快速查找 Dictionary<TKey,TValue> Hashtable

(根据键的哈希代码组织的键/值对的集合。)
ConcurrentDictionary<TKey,TValue>

ReadOnlyDictionary<TKey,TValue>

ImmutableDictionary<TKey,TValue>
按索引访问项 List<T> Array

ArrayList
ImmutableList<T>

ImmutableArray
使用项先进先出 (FIFO) Queue<T> Queue ConcurrentQueue<T>

ImmutableQueue<T>
使用数据后进先出 (LIFO) Stack<T> Stack ConcurrentStack<T>

ImmutableStack<T>
按顺序访问项 LinkedList<T> 无建议 无建议
删除集合中的项或向集合添加项时接收通知。 (实现 INotifyPropertyChanged 和 INotifyCollectionChanged ObservableCollection<T> 无建议 无建议
已排序的集合 SortedList<TKey,TValue> SortedList ImmutableSortedDictionary<TKey,TValue>

ImmutableSortedSet<T>
数学函数的一个集 HashSet<T>

SortedSet<T>
无建议 ImmutableHashSet<T>

ImmutableSortedSet<T>

HashSet<T> 类是用于包含唯一元素的无序集合。SortedSet<T> 类提供在执行插入、删除和搜索操作之后让数据一直按排序顺序排列的自平衡树。

抽象基类 KeyedCollection<TKey,TItem> 的行为类似列表和字典。

 

 ConcurrentStack、ConcurrentQueue和ConcurrentBag类型内部是使用链表实现的。因此,其内存利用不如非并发的Stack和Queue高效。但是它们适用于并发访问,因为链表更容易实现无锁算法或者少锁的算法。

 

 ConcurrentBag<T> 表示对象的线程安全的无序集合,与集不同,包支持重复项。ConcurrentBag<T> 可以接受 null 作为引用类型的有效值。

一个ConcurrentBag<T>对象上的每一个线程都有自己的私有链表。线程在调用Add方法时会将元素添加到自己的私有链表中,因此不会出现竞争。当我们枚举集合中的元素时,其枚举器会遍历每一个线程的私有链表,依次返回

在调用Take时,ConcurrentBag<T>首先会查询当前线程的私有列表,如果列表中至少有一个元素存在[插图],那么该操作就可以在不引入竞争的情况下完成。但是,如果私有列表是空的,则必须从其他线程的私有列表中“窃取”一个元素,而这种操作可能造成竞争。
因此,准确地说,Take方法将返回调用线程在集合中最近添加的元素。如果该线程上已经没有任何元素,它会返回其他线程(随机挑选)最近添加的元素。

 

集合的算法复杂性

不可变的集合类型通常性能较低,但却提供了不可变性,这通常是一种非常有效的优点。

集合的算法复杂性
可变分期最坏情况不可变复杂性
Stack<T>.Push O(1) O(n) ImmutableStack<T>.Push O(1)
Queue<T>.Enqueue O(1) O(n) ImmutableQueue<T>.Enqueue O(1)
List<T>.Add O(1) O(n) ImmutableList<T>.Add O(log n)
List<T>.Item[Int32] O(1) O(1) ImmutableList<T>.Item[Int32] O(log n)
List<T>.Enumerator O(n) O(n) ImmutableList<T>.Enumerator O(n)
HashSet<T>.Add, lookup O(1) O(n) ImmutableHashSet<T>.Add O(log n)
SortedSet<T>.Add O(log n) O(n) ImmutableSortedSet<T>.Add O(log n)
Dictionary<T>.Add O(1) O(n) ImmutableDictionary<T>.Add O(log n)
Dictionary<T> lookup O(1) O(1) - 或者从严格意义上说,O(n) ImmutableDictionary<T> lookup O(log n)
SortedDictionary<T>.Add O(log n) O(n log n) ImmutableSortedDictionary<T>.Add O(log n)

由于其索引器的 O(log n) 时间,ImmutableList<T> 在 for 循环内的效果较差。 使用 foreach 循环枚举 ImmutableList<T> 很有效,因为 ImmutableList<T> 使用二进制树来存储其数据,而不是像 List<T> 那样使用简单数组。 数组可以非常快速地编入索引,但必须向下遍历二进制树,直到找到具有所需索引的节点。

此外,SortedSet<T> 与 ImmutableSortedSet<T> 的复杂性相同。 这是因为它们都使用了二进制树。 当然,显著的差异在于 ImmutableSortedSet<T> 使用不可变的二进制树。 由于 ImmutableSortedSet<T> 还提供了一个允许变化的 System.Collections.Immutable.ImmutableSortedSet<T>.Builder 类,因此可以同时实现不可变性和保障性能。

 SortedDictionary<TKey,TValue> 是一个有 O(log n) 插入和检索操作的已排序字典,这使它有效代替了 SortedList<TKey,TValue>

 

检查是否相等

诸如 Contains、 IndexOf、 LastIndexOf和 Remove 的方法将相等比较器用于集合元素。 如果集合是泛型的,则按照以下原则比较项是否相等:

此外,字典集合的某些构造函数重载接受 IEqualityComparer<T> 实现,用于比较键是否相等。 

 

确定排序顺序

对于比较对象,有 default comparer 和 explicit comparer的概念。

默认比较器依赖至少一个正在被比较的对象来实现 IComparable 接口。 在用作列表集合的值或字典集合的键的所有类上实现 IComparable 不失为一个好办法。 对泛型集合而言,等同性比较是根据以下内容确定的:

为了提供显式比较,某些方法接受 IComparer 实现作为参数。 例如, List<T>.Sort 方法接受 System.Collections.Generic.IComparer<T> 实现。

 

 

SortedList<TKey,TValue> 类与 SortedDictionary<TKey,TValue> 类之间的一些区别。

表 1
 SortedList<TKey,TValue> 泛型类SortedDictionary<TKey,TValue> 泛型类
返回键和值的属性是有索引的,从而允许高效的索引检索。 无索引的检索。
检索属于 O(log n) 操作。 检索属于 O(log n) 操作。
插入和删除通常属于 O(n) 操作;不过,对于已按排序顺序排列的数据,插入属于 O(log n) 操作,这样每个元素都可以添加到列表的末尾。 (这假设不需要调整大小。) 插入和删除属于 O(log n) 操作。
比 SortedDictionary<TKey,TValue> 使用更少的内存。 比 SortedList 非泛型类和 SortedList<TKey,TValue> 泛型类使用更多内存。

对于必须可通过多个线程并发访问的已排序列表或字典,可以向派生自 ConcurrentDictionary<TKey,TValue> 的类添加排序逻辑

 

线程安全集合

并发集合类型使用轻量同步机制,如 SpinLockSpinWaitSemaphoreSlim 和 CountdownEvent,这些机制是 .NET Framework 4 中的新增功能。 这些同步类型通常在将线程真正置于等待状态之前,会在短时间内使用忙旋转。 预计等待时间非常短时,旋转比等待所消耗的计算资源少得多,因为后者涉及资源消耗量大的内核转换。 对于使用旋转的集合类,这种效率意味着多个线程能够以非常快的速率添加和删除项。

 

BlockingCollection<T> 是一个线程安全集合类,可提供实现制造者-使用者模式。多个线程或任务可同时向集合添加项,如果集合达到其指定最大容量,则制造线程将发生阻塞,直到移除集合中的某个项。 多个使用者可以同时移除项,如果集合变空,则使用线程将发生阻塞,直到制造者添加某个项。 制造线程可调用 CompleteAdding 来指示不再添加项。 使用者将监视 IsCompleted 属性以了解集合何时为空且不再添加项。

创建 BlockingCollection<T> 时,不仅可以指定上限容量,而且可以指定要使用的集合类型。 例如,可为先进先出 (FIFO) 行为指定 ConcurrentQueue<T>,也可为后进先出 (LIFO) 行为指定 ConcurrentStack<T>。 可使用实现 IProducerConsumerCollection<T> 接口的任何集合类。 BlockingCollection<T> 的默认集合类型为 ConcurrentQueue<T>

如果对 BlockingCollection<T>.GetConsumingEnumerable 进行了并发调用,这些调用会争用。 无法在一个枚举中观察到在另一个枚举中使用的项目。按照此方式枚举集合会在没有项可用或集合为空时阻止使用者线程,直到CompleteAdding被调用才返回

若要枚举集合而不对其进行修改,只需使用 foreach (For Each) 即可,无需使用 GetConsumingEnumerable 方法。 但是,务必要了解此类枚举表示的是某个精确时间点的集合快照。 如果其他线程在你执行循环的同时添加或删除项,则循环可能不会表示集合的实际状态。

 

在使用者需要同时取出多个集合中的项的情况下,可以创建 BlockingCollection<T> 的数组并使用静态方法,如 TakeFromAny 和 AddToAny 方法,这两个方法可以在该数组的任意集合中添加或取出项。 如果一个集合发生阻塞,此方法会立即尝试其他集合,直到找到能够执行该操作的集合。

//Generate some source data.
      BlockingCollection<int>[] sourceArrays = new BlockingCollection<int>[5];
      for(int i = 0; i < sourceArrays.Length; i++)
          sourceArrays[i] = new BlockingCollection<int>(500);
      Parallel.For(0, sourceArrays.Length * 500, (j) =>
                          {
                              int k = BlockingCollection<int>.TryAddToAny(sourceArrays, j);
                              if(k >=0)
                                  Console.WriteLine("added {0} to source data", j);
                          });

      foreach (var arr in sourceArrays)
          arr.CompleteAdding();

 

ConcurrentDictionary<TKey,TValue> 专为多线程方案而设计。 无需在代码中使用锁定即可在集合中添加或移除项。 但始终可能出现以下情况:一个线程检索一个值,而另一线程通过为同一键赋予新值来立即更新集合。

此外,尽管 ConcurrentDictionary<TKey,TValue> 的所有方法都是线程安全的,但并非所有方法都是原子的,尤其是 GetOrAdd 和 AddOrUpdate。 为避免未知代码阻止所有线程,传递给这些方法的用户委托将在词典的内部锁之外调用。 因此,可能发生以下事件序列:

  1. threadA 调用 GetOrAdd,未找到项,通过调用 valueFactory 委托创建要添加的新项。

  2. threadB 并发调用 GetOrAdd,其 valueFactory 委托受到调用,并且它在 threadA 之前到达内部锁,并将其新键值对添加到词典中 。

  3. threadA 的用户委托完成,此线程到达锁位置,但现在发现已有项存在。

  4. threadA 执行“Get”,返回之前由 threadB 添加的数据 。

因此,无法保证 GetOrAdd 返回的数据与线程的 valueFactory 创建的数据相同。 调用 AddOrUpdate 时可能发生相似的事件序列。

 

 异常Collection was modified; enumeration operation may not execute 原因:遍历时集合被修改

.NET Core 3.0 came with C# 8.0. Since then, we have been able to modify a Dictionary<TKey,TValue> during enumeration (foreach) via .Remove and .Clear only.

 

Microsoft.Extensions.ObjectPool 命名空间下已存在 Microsoft.Extensions.ObjectPool.ObjectPool<T> 类型。  在需要某个类的多个实例并且创建或销毁该类的成本很高的情况下,对象池可以改进应用程序性能。 

 

posted @ 2020-08-16 21:01  yetsen  阅读(131)  评论(0编辑  收藏  举报