我遇到了与我正在研究的多玩家同步游戏同时使用共享集合的问题。我做了一些挖掘,在Alexey Drobyshevsky的代码项目帖子上发现了一个整洁的线程安全的IEnumerator / IList实现:
http://www.codeproject.com/KB/cs/safe_enumerable.aspx
在采用他的实现之后,我甚至用for / foreach循环替换了共享集合上的所有Linq查询,因为Linq查询仍在使用下面的不安全的IEnumerable。
这是我对SafeList的实现,列表本身作为消费类的ReadOnlyCollection公开。
http://theburningmonk.com/2010/03/thread-safe-enumeration-in-csharp/
切换到这个SafeList后,我看到的问题要少得多,但是在负载很重的情况下(80多个线程都在不同的点读取/写入列表)我仍然看到抛出InvalidOperationException:
元素列表已更改。枚举操作无法继续
我甚至尝试使用ReadWriterLockSlim代替我在SafeList实现中的锁对象,但事实证明这也没有结果。到目前为止,我唯一的另一个建议是每次线程需要循环时克隆列表。我希望每次都能避免克隆列表,因为列表的使用方式太多,可能会影响性能并可能引入其他难以发现的错误。
考虑到时间限制,我必须务实,并且如果克隆是解决这个问题的最安全和最快捷的方式,那么我很好,但是在采取这最后的尝试之前,我我只是想知道是否有人在那里遇到类似的东西能够提供一些建议。
非常感谢提前!
[编辑]以下是我所要求的问题的更多信息:
对于一个“游戏”,最多可以连接100个左右的同步客户端,并且游戏需要每隔几秒向每个连接的客户端发送更新消息,因此每隔几秒钟这个游戏需要遍历共享的玩家列表。 当玩家加入或离开时,需要相应地更新列表以反映更改。 除此之外,玩家可以与游戏交互并与其他玩家聊天,并且每次从玩家接收消息时,游戏将再次需要迭代通过相同的列表并进行广播。 当游戏试图在许多玩家同时离开/加入(写入操作)的同时向玩家广播消息(读取操作)时,通常会抛出异常。
答案 0 :(得分:6)
鉴于您对游戏结构的描述,考虑让单个线程是唯一可以直接访问玩家列表的线程。使列表有效地专用于该一个线程。
任何其他线程访问列表的方式是通过向列表管理器线程发送消息。因此该线程有一个等待的消息队列。当队列非空时,它会按照指示咀嚼消息。他们可能会说“添加新玩家”,或“删除玩家”,或“将此玩家的状态更新为'不快乐'”。
此线程还可以定期扫描列表以将更新发送给客户端,或者它可以(甚至更好)使用它具有列表中发生的已知更改流的事实,并将这些更改转发到客户。
基本原则:将数据专用于一个线程,并让线程通过消息队列进行通信。
您的基本数据结构是一个线程安全的队列类。 SO已有数十个例子。 (并避开任何声称“无锁”但仍然安全的线程。这不值得冒风险。)
答案 1 :(得分:1)
尽管有关重构的其他建议是要走的路,但'thread-safe'类中的一个错误会溢出(可能还有更多):
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
// instead of returning an usafe enumerator,
// we wrap it into our thread-safe class
return new SafeEnumerator<T>(_inner.GetEnumerator(), _lock);
}
您可以在运行构造函数之前从_inner.GetEnumerator
创建枚举器,因此任何线程都可以自由修改集合,直到您锁定构造函数为止。这是一个很小的时间段,但有80个线程就会发生。您需要锁定return ..
语句以保护枚举器。
编辑:以及在其他使用相同模式的地方。