C#的Partitioner的InvalidOperationException是什么?

小老鼠
发布: 2025-08-19 08:02:01
原创
757人浏览过

partitioner抛出invalidoperationexception的根本原因是其依赖的数据源在并行划分过程中被外部修改,导致内部状态不一致。1. 当使用partitioner.create处理非线程安全集合(如list)时,若另一线程在parallel.foreach执行期间添加、删除或修改集合元素,partitioner原先计算的分区索引将失效,从而触发异常;2. 解决方案是确保数据源稳定,最有效方法是在传递给partitioner前调用toarray()或tolist()创建副本,使并行操作基于不可变快照进行;3. 若数据源需动态更新,应改用concurrentbag、concurrentqueue等线程安全集合,它们能安全支持并发读写并兼容partitioner;4. 避免在并行处理中直接修改原始数据源,而应将结果写入线程安全的收集器(如concurrentbag);5. 不同partitioner.create重载中,数组和ilist版本因支持索引访问而性能更优,但同样要求结构稳定,而带enumerablepartitioneroptions的重载可控制缓冲行为,自定义orderablepartitioner则需自行保证线程安全。总之,该异常是系统对数据不一致的保护机制,通过使用数据副本或线程安全集合即可可靠避免。

C#的Partitioner的InvalidOperationException是什么?

C#中

Partitioner
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
抛出的
InvalidOperationException
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
,通常发生在当你尝试在并行处理过程中,修改了作为数据源的集合时。简单来说,就是
Partitioner
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
在划分任务时,它所依赖的底层数据源被“动了手脚”,导致其内部状态不一致,无法继续安全地执行划分操作。这通常不是一个bug,而是系统在告诉你,你正在做的事情可能会导致数据混乱或不完整。

解决方案

遇到

Partitioner
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
引发
InvalidOperationException
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
,核心思路是确保在并行处理期间,作为数据源的集合是稳定的、不可变的,或者至少其结构不会发生改变。

一种最直接、也最常用的方法,就是在将集合传递给

Partitioner
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
之前,先创建一个它的“快照”或副本。例如,如果你有一个
List<T>
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
,并且你担心在
Parallel.ForEach
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
执行过程中它会被修改,那么可以这样做:

List<int> originalList = new List<int> { 1, 2, 3, 4, 5 };
// ... 某个地方可能会修改originalList

// 在传递给Partitioner之前,创建一个副本
var stableSource = originalList.ToArray(); // 或者 .ToList()
// 现在,使用这个稳定的副本进行并行处理
Parallel.ForEach(Partitioner.Create(stableSource), item =>
{
    // 对item进行操作
    Console.WriteLine($"Processing {item}");
    // 在这里不要修改originalList或stableSource
});
登录后复制

这样做的好处是,

Partitioner
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
操作的是一个独立的、不会被外部修改的数组或列表,从而避免了
InvalidOperationException
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
。当然,这会引入一份内存开销,但对于大多数场景来说,这是可接受的权衡。

如果你的数据源必须是动态的,并且在并行处理期间会有并发修改,那么你需要考虑使用专门为并发设计的集合类型,比如

ConcurrentBag<T>
登录后复制
ConcurrentQueue<T>
登录后复制
登录后复制
。这些集合在设计上就考虑了多线程访问的安全性,它们通常能更好地与
Partitioner
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
协同工作,尽管它们在某些特定场景下可能不会提供最佳的性能分区策略。

// 假设这是一个可能被并发修改的集合
ConcurrentBag<string> concurrentData = new ConcurrentBag<string>();
concurrentData.Add("Alpha");
concurrentData.Add("Beta");
// ... 可以在其他线程继续添加或移除

// Partitioner.Create可以直接处理ConcurrentBag
Parallel.ForEach(Partitioner.Create(concurrentData), item =>
{
    Console.WriteLine($"Processing {item}");
    // 在这里对concurrentData进行修改通常是安全的,但要理解其语义
});
登录后复制

总而言之,问题的根源在于并行处理对数据源一致性的要求,解决方案则围绕着如何提供一个满足这一要求的数据源展开。

为什么Partitioner会抛出InvalidOperationException?

嗯,这事儿挺常见的,说实话,我也踩过这个坑。

Partitioner
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
,特别是当它试图对一个非线程安全的集合(比如
List<T>
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
Array
登录后复制
)进行划分时,它需要一个稳定的“视图”来确定每个并行任务应该处理哪些数据。你可以想象一下,它就像一个工头,正在给一群工人分配任务:第一个工人处理1-10号零件,第二个工人处理11-20号零件。如果在他分配任务的过程中,或者工人已经开始拿零件的时候,有人突然往生产线上加了几个零件,或者移走了几个,那工头之前算好的分配方案就全乱套了。

InvalidOperationException
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
就是系统在告诉你:“嘿,你的集合在被我划分的时候变了!我没法保证我分出去的任务是正确的,也没法保证不会出现重复处理或者遗漏数据的情况。”这是一种“快失败”(fail-fast)机制,它不是一个bug,而是一种保护,防止你的程序在不一致的状态下继续运行,从而产生更难以调试的错误结果。

具体来说,当你使用

Partitioner.Create(myList)
登录后复制
这样的方式时,
Partitioner
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
可能会根据
myList
登录后复制
登录后复制
的当前大小和结构来计算出各个并行任务的起始和结束索引。如果
myList
登录后复制
登录后复制
在此时被另一个线程添加、删除元素,或者甚至只是改变了元素顺序,那么之前计算好的索引可能就会指向错误的位置,或者一部分数据被遗漏,另一部分被重复处理。这种不确定性是并行编程的大忌,所以系统选择直接抛出异常,强制你处理这种并发修改。

它主要发生在以下几种情况:

  • 你在
    Parallel.ForEach
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    或PLINQ操作一个
    List<T>
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    Dictionary<TKey, TValue>
    登录后复制
    等非线程安全集合时,同时有另一个线程在向这个集合添加、删除元素。
  • 你自定义了一个
    OrderablePartitioner
    登录后复制
    登录后复制
    ,但你的
    GetPartitions
    登录后复制
    GetDynamicPartitions
    登录后复制
    方法没有正确处理底层数据源的并发修改,或者它本身就依赖于一个非线程安全的快照。

如何安全地在并行处理中修改数据源?

这确实是一个常见的需求,但“安全地在并行处理中修改数据源”这个说法本身就有点陷阱。更准确的说法应该是:如何在并行处理中收集结果,或者在并行处理中处理动态变化的数据。直接修改作为

Partitioner
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
数据源的集合,通常不是推荐的做法,因为这正是导致
InvalidOperationException
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
的原因。

如果你需要在并行处理中产生新的数据,并把这些数据收集起来,你应该使用线程安全的集合来存储结果,而不是去修改原始的数据源。例如:

List<int> numbers = Enumerable.Range(1, 100).ToList();
// 使用ConcurrentBag来收集并行处理的结果
ConcurrentBag<double> results = new ConcurrentBag<double>();

Parallel.ForEach(Partitioner.Create(numbers), number =>
{
    // 假设这是一个耗时的计算
    double result = Math.Sqrt(number) * 10;
    results.Add(result); // 安全地将结果添加到线程安全集合中
});

// 所有并行任务完成后,可以安全地访问results
foreach (var res in results)
{
    Console.WriteLine(res);
}
登录后复制

这里,

numbers
登录后复制
集合在整个
Parallel.ForEach
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
过程中是保持不变的,而
results
登录后复制
是一个专门为并发写入设计的集合。

如果你的“数据源”本身就是动态的,比如一个队列,你希望在并行处理的同时,有新的数据不断地被添加到队列中,并且能够被处理,那么你需要从一开始就选择一个线程安全的集合作为数据源,并且

Partitioner
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
能够很好地支持它。
ConcurrentQueue<T>
登录后复制
登录后复制
就是一个很好的例子:

ConcurrentQueue<string> tasksQueue = new ConcurrentQueue<string>();
tasksQueue.Enqueue("Task A");
tasksQueue.Enqueue("Task B");

// 模拟另一个线程不断添加任务
Task.Run(() =>
{
    for (int i = 0; i < 5; i++)
    {
        Thread.Sleep(500); // 模拟延迟
        tasksQueue.Enqueue($"Dynamic Task {i}");
        Console.WriteLine($"Added Dynamic Task {i}");
    }
});

// Partitioner.Create可以直接处理ConcurrentQueue
// 注意:对于无限流或持续添加的队列,你可能需要一个停止条件
Parallel.ForEach(Partitioner.Create(tasksQueue), task =>
{
    Console.WriteLine($"Processing: {task}");
    Thread.Sleep(200); // 模拟处理时间
});

Console.WriteLine("All tasks processed."); // 这行可能在队列完全清空前出现
登录后复制

在这种情况下,

Partitioner.Create(tasksQueue)
登录后复制
能够适应
ConcurrentQueue
登录后复制
的动态特性。然而,你需要注意的是,这种模式下,
Parallel.ForEach
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
会在队列为空时停止,如果你的生产者线程还在持续生产,你可能需要更复杂的协调机制(比如使用
BlockingCollection
登录后复制
)。

总的来说,避免在并行处理中直接修改作为

Partitioner
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
数据源的非线程安全集合,而是将修改操作转移到结果收集阶段,或者从一开始就使用线程安全的集合作为数据源。

Partitioner.Create的不同重载有什么区别

Partitioner.Create
登录后复制
方法提供了多个重载,它们的设计是为了适应不同类型的数据源和不同的分区需求。理解这些区别对于优化并行性能和避免潜在问题至关重要。

  1. Partitioner.Create<TSource>(IEnumerable<TSource> source)
    登录后复制

    • 这是最通用的重载,可以接受任何实现了
      IEnumerable<TSource>
      登录后复制
      登录后复制
      的集合。
    • 它会根据传入的
      source
      登录后复制
      登录后复制
      登录后复制
      的具体类型,在内部选择一个合适的分区策略。例如,如果
      source
      登录后复制
      登录后复制
      登录后复制
      List<T>
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      T[]
      登录后复制
      ,它可能会使用基于索引的范围分区;如果是非索引集合,它可能会使用迭代器分区。
    • 风险点: 如果
      source
      登录后复制
      登录后复制
      登录后复制
      是一个非线程安全的集合,并且在
      Parallel.ForEach
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      执行期间被修改,就非常容易抛出
      InvalidOperationException
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      。这是最常见的触发点。
  2. Partitioner.Create<TSource>(IList<TSource> list)
    登录后复制

    • 这个重载专门针对实现了
      IList<TSource>
      登录后复制
      的集合。
    • 由于
      IList
      登录后复制
      提供了索引访问能力,
      Partitioner
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      可以利用这一点进行更高效的范围分区。它通常会将列表划分为连续的块,然后将这些块分配给不同的并行任务。
    • 风险点: 尽管它能更高效地利用索引,但如果
      list
      登录后复制
      在并行处理期间被修改(添加、删除元素),同样会引发
      InvalidOperationException
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
  3. Partitioner.Create<TSource>(TSource[] array)
    登录后复制

    • 这是针对数组
      TSource[]
      登录后复制
      的重载。
    • 数组是固定大小的,提供了最直接的索引访问。
      Partitioner
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      可以非常高效地将数组划分为精确的、连续的子范围,这通常能提供最佳的性能。
    • 安全性: 相对于
      List<T>
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      ,数组的结构(大小)在创建后是不可变的,这使得它在作为
      Partitioner
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      的数据源时更安全,不会因为元素数量的变化而导致
      InvalidOperationException
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      。当然,如果你在并行处理中修改数组内部的元素值,那又是另一个层面的并发问题了,但至少不会因为结构变化而抛出
      InvalidOperationException
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
  4. Partitioner.Create(int fromInclusive, int toExclusive)
    登录后复制

    • 这个重载不是针对集合的,而是用来划分一个整数范围。
    • 它非常适合当你需要并行执行一个基于索引的循环时,例如处理一个大型数组或数据库记录的某个范围。
    • 示例:
      Parallel.For(0, 100, i => { /* process item at index i */ });
      登录后复制
      在内部就可能使用类似的分区策略。
  5. Partitioner.Create(IEnumerable<TSource> source, EnumerablePartitionerOptions options)
    登录后复制

    • 这个重载允许你为
      IEnumerable<TSource>
      登录后复制
      登录后复制
      源指定额外的选项,比如
      EnumerablePartitionerOptions.NoBuffering
      登录后复制
    • NoBuffering
      登录后复制
      选项会告诉
      Partitioner
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      不要在内部进行额外的缓冲。这在某些情况下可以减少内存使用,但可能会增加线程间的协调开销,从而影响性能。通常在处理非常大的数据集,或者数据生成速度很快时才会考虑。
  6. OrderablePartitioner<TSource>
    登录后复制

    • 这是最灵活但也最复杂的。它是一个抽象基类,允许你实现自定义的分区逻辑。
    • 当你需要对数据进行非常特殊的分区,或者你的数据源不是标准的集合类型,或者你需要实现更精细的负载均衡策略时,可以继承这个类。
    • 责任: 实现
      OrderablePartitioner
      登录后复制
      登录后复制
      意味着你需要自己处理所有并发和一致性问题。如果你的自定义分区逻辑没有正确处理底层数据源的并发修改,你同样会遇到
      InvalidOperationException
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      登录后复制
      或其他并发问题。

总的来说,选择哪个重载取决于你的数据源类型、你对性能的需求以及你是否需要处理动态或并发修改的数据。对于大多数情况,如果数据源是静态的,

ToArray()
登录后复制
后使用
Partitioner.Create(TSource[])
登录后复制
是最稳妥且高效的选择。如果数据源本身就是为并发设计的(如
ConcurrentBag
登录后复制
),那么直接使用
Partitioner.Create(IEnumerable<TSource>)
登录后复制
通常也能很好地工作。

以上就是C#的Partitioner的InvalidOperationException是什么?的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号