C# 在泛型集合上同时迭代和执行操作

C# 在泛型集合上同时迭代和执行操作,c#,multithreading,list,collections,C#,Multithreading,List,Collections,我用c#async socks创建了一个游戏仿真程序。我需要同时删除/添加和执行集合(保存客户端的列表)上的迭代。我目前正在使用“锁”,然而,这是一个巨大的性能下降。我也不想使用“本地列表/副本”来更新列表。我听说过“ConcurrentBags”,但是,我不确定它们对迭代的线程安全性如何(例如,如果一个线程从列表中删除一个元素,而另一个线程正在对其进行迭代!?) 你有什么建议 编辑:以下是一种情况 这是指将数据包发送给房间中的所有用户 lock (parent.gameClientList)

我用c#async socks创建了一个游戏仿真程序。我需要同时删除/添加和执行集合(保存客户端的列表)上的迭代。我目前正在使用“锁”,然而,这是一个巨大的性能下降。我也不想使用“本地列表/副本”来更新列表。我听说过“ConcurrentBags”,但是,我不确定它们对迭代的线程安全性如何(例如,如果一个线程从列表中删除一个元素,而另一个线程正在对其进行迭代!?)

你有什么建议

编辑:以下是一种情况 这是指将数据包发送给房间中的所有用户

lock (parent.gameClientList)
{
    for (int i = 0; i <= parent.gameClientList.Count() - 1; i++) if (parent.gameClientList[i].zoneId == zoneId) parent.gameClientList[i].SendXt(packetElements); //if room matches - SendXt sends a packet
}
客户端断开连接时的情况也是如此


我要求提供一个更好的替代方案(从性能角度来看),因为锁会降低所有速度。

听起来问题在于您在foreach循环中完成所有工作,并且它将添加/删除方法锁定太久。解决这个问题的方法是在集合被锁定时快速创建一个副本,然后可以关闭锁并对副本进行迭代

Thing[] copy;
lock(myLock) {
  copy = _collection.ToArray();
}
foreach(var thing in copy) {...}
缺点是,当您开始对该副本的某些对象进行操作时,它可能已从原始集合中删除,因此您可能不想再对其进行操作。这是另一件事,您只需了解需求即可。如果这是一个问题,一个简单的选项是锁定循环的每个迭代,这当然会减慢速度,但至少不会在循环运行的整个过程中锁定:

foreac(var thing in copy) {
  lock(myLock) {
    if (_collection.Contains(thing)) //check that it's still in the original colleciton
      DoWork(thing); //If you can move this outside the lock it'd make your app snappier, but it could also cause problems if you're doing something "dangerous" in DoWork.
  }
}

如果这就是您所说的“本地副本”,那么您可以忽略此选项,但我想我会提供它,以防您指的是其他内容。

每次同时执行某项操作时,您都会因任务管理(即锁定)而蒙受损失。我建议您看看流程中的瓶颈是什么。您似乎有一个共享内存模型,而不是消息传递模型。如果您知道需要立即修改整个集合,则可能没有好的解决方案。但是,如果您正在按照特定的顺序进行更改,您可以利用该顺序来防止延迟。锁是悲观并发的一种实现。您可以切换到乐观并发模型。一方面成本在等待,另一方面成本在重试。同样,实际的解决方案取决于您的用例。

ConcurrentBag的问题在于它是无序的,所以您不能像当前那样按索引提取项目。但是,您可以通过
foreach
对其进行迭代以获得相同的效果。此迭代将是线程安全的。如果在迭代过程中添加或删除一个项目,它将不会变得更复杂

但是,
ConcurrentBag
还有另一个问题。它实际上是在内部将内容复制到一个新的
列表
,以使枚举器正常工作。因此,即使您只想通过枚举器拾取单个项,由于枚举器的工作方式,它仍然是一个O(n)操作。您可以通过分解它来验证这一点

然而,根据您的更新中的上下文线索,我假设此集合将很小。似乎每个“游戏客户端”只有一个条目,这意味着它可能会存储少量的项目,对吗?如果这是正确的,那么
GetEnumerator
方法的性能将非常低

你也应该考虑<代码>并发字典< /代码>。我注意到您正在尝试根据

zoneId
匹配集合中的项目。如果将项目存储在由
zoneId
键入的
ConcurrentDictionary
中,则根本不需要迭代集合。当然,这假设每个
zoneId
只有一个条目,情况可能并非如此

您提到您不想使用“本地列表/副本”,但您从未说过原因。我认为你应该重新考虑这个问题,原因如下

  • 迭代可以是无锁的
  • 从代码中添加和删除项似乎很少基于上下文线索

有几种模式可以让列表复制策略非常有效。我在回答和回答中谈到了这些问题。

您阅读了
ConcurrentBag
上的文档了吗@Gabe是的,它抛出了一个异常,因为它不知道应该做什么。你想让它做什么?当集合被修改时,它应该返回什么?@Gabe“正确完成”没有任何意义。我已经告诉过你好几次了,没有明显的“适当”结果。现在还不清楚应该发生什么,这就是抛出异常的原因。你需要准确地定义需要发生什么,才能得到答案。除了抛出异常,您对任何结果都满意吗?这很容易,但很快你就会回到这里,得到一些你想要“修复”的意外结果,因为你没有定义在异常修改的情况下要做什么。你真的需要显示代码,因为你在谈论和描述“任意性”这种情况下,您需要一个适用于未提供的问题的特定答案
ReaderWriter
锁似乎不像以前那样对性能造成了很大的影响。如果与修改列表的频率相比,您对列表进行了大量的迭代,那么您可能能够保持您的方法,但会得到性能提升。实际上,许多线程可以同时读取,但当一个线程想要写入时,它会获得独占访问权(类似于您现在使用的锁)。
foreac(var thing in copy) {
  lock(myLock) {
    if (_collection.Contains(thing)) //check that it's still in the original colleciton
      DoWork(thing); //If you can move this outside the lock it'd make your app snappier, but it could also cause problems if you're doing something "dangerous" in DoWork.
  }
}