关于c#:同时迭代并对泛型集合执行操作

Iterate and do operations on a generic collection concurrently

我用C异步袜子创建了一个游戏模拟程序。我需要同时删除/添加集合(保存客户端的列表)上的&do迭代。我目前使用的是"锁",不过,这是一个巨大的性能下降。我也不想使用"本地列表/副本"来保持列表的最新。我听说过"ConcurrentBags",但是,我不确定它们对于迭代有多安全(例如,如果一个线程从列表中删除了一个元素,而另一个线程正在对其进行迭代!)?)

你有什么建议?

编辑:这里有一种情况当一个包被发送到一个房间中的所有用户时

1
2
3
4
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
}

当新客户机连接时

1
2
3
4
5
 Client connectedClient = new Client(socket, this);
 lock (gameClientList)
 {
     gameClientList.Add(connectedClient);
 }

客户机断开连接时的情况相同。

我要求一个更好的选择(性能方面),因为锁会减慢一切。


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


听起来问题在于,您在foreach循环中完成了所有工作,并且锁定添加/删除方法的时间太长。解决这个问题的方法是在集合被锁定时快速复制它,然后您可以关闭锁并在副本上迭代。

1
2
3
4
5
Thing[] copy;
lock(myLock) {
  copy = _collection.ToArray();
}
foreach(var thing in copy) {...}

缺点是,当您开始操作该副本的某个对象时,它可能已从原始集合中删除,因此您可能不想再对其进行操作。这是另一件事,你只需要找出需求。如果这是一个问题,一个简单的选项是锁定循环的每个迭代,当然这会减慢速度,但至少在循环运行的整个过程中不会锁定:

1
2
3
4
5
6
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迭代它以获得相同的效果。这个迭代将是线程安全的。如果在迭代过程中添加或删除了一个项,它将不会变为bizerk。

不过,ConcurrentBag还有另一个问题。它实际上在内部将内容复制到一个新的List以使枚举器正常工作。因此,即使您只想通过枚举器选择一个项目,由于枚举器的工作方式,它仍然是一个O(N)操作。你可以通过拆卸来验证这一点。

但是,根据您更新的上下文线索,我认为这个集合将很小。似乎每个"游戏客户端"只有一个条目,这意味着它可能会存储少量的项目,对吗?如果这是正确的,那么GetEnumerator方法的性能将主要是微不足道的。

你也应该考虑ConcurrentDictionary。我注意到你正在尝试匹配基于zoneId的集合中的项目。如果您将这些项存储在由zoneId键控的ConcurrentDictionary中,那么您根本不需要迭代集合。当然,这假设每个zoneId只有一个条目,但情况可能并非如此。

你提到你不想使用"本地列表/副本",但你从来没有说原因。我认为你应该重新考虑一下原因。

  • 迭代可以是无锁的。
  • 添加和删除项目似乎是代码中不常见的基于上下文的线索。

有两种模式可以让列表复制策略工作得非常好。我在这里和这里的答案中都谈到了它们。