T []不比IEnumerable <t>更好作为参数类型吗? (考虑线程挑战)</t>

时间:2013-02-20 12:10:18

标签: c# multithreading collections thread-safety ienumerable

一些程序员认为最好将IEnumerable<T>参数传递给像List<T>这样的传递实现,并且这样做的一个常见原因是API可以在更多场景中立即使用,因为更多集合将与IEnumerable<T>兼容,而不是任何其他特定实现,例如List<T>

我越是深入研究多线程开发场景,我就越开始认为IEnumerable<T>也不是正确的类型,我将尝试解释下面的原因。

枚举集合时是否收到过以下异常?

收藏被修改;枚举操作可能无法执行。(InvalidOperationException)

Collection was modified exception

基本上是导致这种情况的原因是,你在一个线程上获得了集合,而其他人在另一个线程上修改了集合。

为了解决这个问题,我开始习惯在枚举之前拍摄集合的快照,方法是将集合转换为方法内的数组,如下所示:

static void LookAtCollection(IEnumerable<int> collection)
{   
    foreach (int Value in collection.ToArray() /* take a snapshot by converting to an array */)
    {
        Thread.Sleep(ITEM_DELAY_MS);
    }
}

我的问题是这个。 将数组编码而不是枚举作为一般规则会更好吗,即使这意味着调用者现在被迫在使用API​​时将其集合转换为数组?

这不是更清洁,更防弹吗? (参数类型是一个数组)

static void LookAtCollection(int[] collection)
{   
    foreach (int Value in collection)
    {
        Thread.Sleep(ITEM_DELAY_MS);
    }
}

该数组满足可枚举和固定长度的所有要求,并且调用者知道您将在该时间点对集合的快照进行操作,这可以节省更多错误。 / p>

我现在能找到的唯一更好的选择是IReadOnlyCollection,它将更加防弹,因为该集合在项目内容方面也是只读的。

修改

@DanielPryden提供了一篇非常好的文章"Arrays considered somewhat harmful"的链接。作者的评论&#34;我很少需要一个具有完全可变性的相当矛盾的属性的集合,同时,在大小上固定&#34; &#34;几乎在所有情况下,都有一个比阵列更好的工具。&#34; 让我相信阵列并不像我希望的那样接近银弹,而我同意风险和漏洞。我认为现在IReadOnlyCollection<T>接口是一个比数组和IEnumerable<T>更好的选择,但它现在给我留下了一个问题:被调用者是否通过IReadOnlyCollection<T>强制执行它方法声明中的参数类型?或者是否应该允许调用者决定将IEnumerable<T>的实现传递给查看集合的方法?

感谢所有答案。我从这些回复中学到了很多东西。

5 个答案:

答案 0 :(得分:4)

除非另有明确说明,否则大多数方法都不是线程安全的。

在这种情况下,我会说调用者需要处理线程安全问题。如果调用者将IEnumerable<T>参数传递给方法,那么如果方法枚举它,他就不会感到惊讶!如果枚举不是线程安全的,则调用者需要以线程安全的方式创建快照,并传递快照。

被调用者无法通过调用ToArray()ToList()来执行此操作 - 这些方法也将枚举集合,并且被调用者无法知道需要什么(例如锁定)来制作快照以线程安全的方式。

答案 1 :(得分:4)

这里真正的“问题”是虽然T[]可能是比理想情况更具体的参数类型,并且允许收件人自由访问以写入任何元素(可能是也可能不需要),{ {1}}太笼统了。从类型安全的角度来看,调用者可以提供对任何实现IEnumerable<T>的对象的引用,但实际上只有某些这样的对象才能真正起作用,并且调用者没有干净的方法知道那些对象。< / p>

如果IEnumerable<T>是引用类型,T在读取,写入和枚举方面具有固有的线程安全性,受某些条件限制(例如,访问不同元素的线程根本不会干扰;如果一个线程在另一个线程读取或枚举它的时候写一个项目,后一个线程将看到旧数据或新数据.Microsoft的集合接口都没有提供任何这样的保证,它们也没有提供任何集合可以指示什么的方法保证它可以或不可以。

我倾向于使用T[],或者定义T[],其中包含IThreadSafeList<T> where T:class之类的成员,允许CompareExchangeItem项目,但不会包括Interlocked.CompareExchangeInsert等无法以线程安全方式完成的内容。

答案 2 :(得分:2)

此问题并非特定于线程。尝试在显式或隐式调用GetEnumerator的循环中修改集合。结果是一样的。这是非法行动。

答案 3 :(得分:1)

永远不允许线程访问与其他线程相同的对象。 你应该总是复制变量/ objects / arrays并在线程休闲处解析它们。

当它完成时,它应该回调主线程,以便线程可以更新当前值/合并值。

如果两个物体可以在同一时间到达同一物体,或者是一场战争,或是异常或奇怪的行为。

想象一下这样的线程: 你有老板和两名员工。老板会复制他手中的信,并将其交给员工。 每个员工都有自己的任务,一个用于收集列表中的所有内容,另一个用于检查列表中的项目是否需要从供应商处订购。 他们每个人都进入他们自己的私人房间,老板只是将纸张放在桌子上,他有时会检查它,告诉别人此刻有多少。

员工1返回订购的对象并将其交给将其传递给物流部门的主管。 一小时后,员工2返回一个包含库存数量的更新列表。导演抛出旧列表并将其替换为新列表。

现在有一个列表的场景。

老板给出了同样的指示。员工一拿到列表并开始呼叫第一个供应商。员工2获取列表并开始挑选项目。导演希望查看列表,但列表已经消失,整个销售漏斗都停止了,因为没有准确的信息,因为列表已被占用。 第一个员工已完成呼叫并想要更新他的列表。他不能因为名单被占用而他不能继续下一个项目,因为名单已经消失,所以他被困在他的拇指或投掷合适。 当员工2准备好他的列表时,员工拿到列表并尝试完成呼叫订单状态,但是导演闯入并获取列表并将信息传递给销售团队。 导演和员工都开始争夺名单,导致没有工作,公司就死了。

这就是为什么你总是需要在线程中使用独立副本,除非你只知道该线程只需要访问该资源,因为你永远不知道该特定线程什么时候会对该资源做什么,因为你没有控制CPU时间。

我希望这有点帮助。

答案 4 :(得分:0)

IEnumerable背后的一个想法是它是LINQ查询的结果。由于查询不是声明而是执行代码,因此仅在IEnumerable中迭代或将项目复制到foreach或其他结构时调用Array的结果。现在,如果我们想将结果存储在像Array这样的静态结构中,我们必须在迭代之前更新它以及IEnumerable,否则我们将收到过时的数据。 IEnumerable.ToArray表示:更新集合并制作静态副本。所以我的答案是,如果我们必须在对集合进行操作之前进行更新,IEnumerable更简洁。在其他情况下,我们可以使用Array来简化代码。