解决集合的并发问题
我在与我正在开发的多人同步游戏中并发使用共享集合时遇到问题。我做了一些挖掘,并在 Alexey Drobyshevsky 在 codeproject 上的帖子中找到了一个简洁的线程安全 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/
切换到此安全列表后,我看到的问题少得多,但在重负载下(80多个线程,所有线程都在不同点从列表中读取/写入)我仍然看到 InvalidOperationException 被抛出:
元素列表已更改。枚举操作无法继续
我什至尝试在 SafeList 的实现中使用 ReadWriterLockSlim 代替锁定对象,但事实证明也无济于事。到目前为止,我唯一的其他建议是每次线程需要循环遍历列表时克隆该列表。我希望避免每次都克隆列表,因为列表在太多地方使用,这可能会影响性能,并且可能会引入其他难以发现的错误。
考虑到时间限制,我必须务实地对待它,如果克隆是解决这个问题最安全、最快捷的方法,那么我就同意了,但在进行最后一次尝试之前,我只是想知道如果有人遇到类似的事情可以提供一些建议。
非常感谢!
[编辑] 以下是有关我按照要求看到的问题的更多信息:
对于一个“游戏”,最多可以连接 100 个左右的同步客户端,并且游戏需要每隔几秒向每个连接的客户端发送更新消息因此,每隔几秒钟,这个游戏就需要遍历共享的玩家列表。 当玩家加入或离开时,列表需要相应更新以反映变化。 除此之外,玩家可以与游戏互动并与其他玩家聊天,每次从玩家收到消息时,游戏都需要再次遍历相同的列表并进行广播。 当游戏尝试向玩家广播消息(读取操作),同时许多玩家同时离开/加入(写入操作)时,通常会引发异常。
I'm having issues with concurrent usage of a shared collection with a multi-player synchronous game I'm working on. I did some digging around and found a neat thread-safe IEnumerator/IList implementation on Alexey Drobyshevsky's post on codeproject here:
http://www.codeproject.com/KB/cs/safe_enumerable.aspx
After adopting his implementation I have even replaced all Linq queries on the shared collection with for/foreach loops because the Linq queries were still using the unsafe IEnumerable underneath.
Here's my implementation of the SafeList, and the list itself is being exposed to as a ReadOnlyCollection to the consuming classes.
http://theburningmonk.com/2010/03/thread-safe-enumeration-in-csharp/
After switching to this SafeList I am seeing far less problems, but under heavy load (80+ threads all of which read/write from and to the list at different points) I'm still seeing InvalidOperationException being thrown:
The element list has changed. The enumeration operation failed to continue
I have even tried using ReadWriterLockSlim in place of the lock object in my implementation of the SafeList but that proved fruitless too. The only other suggestion I have had so far is cloning the list every time a threads needs to loop through it. I'm hoping to avoid cloning the list every single time as the list is being used in way too many places it might be a performance hit and might introduce other bugs that are difficult to spot.
Given the time constraints I'll have to be pragmatic about it and if cloning is the safest and quickest way to go about solving this problem then I'm fine with it, but before resorting to this last ditch-attempt I'm just wondering if anyone out there has come across something similar is able to offer some advice.
Many thanks in advance!
[EDIT] Here is a little more information about the problem I'm seeing as requested:
For one 'game', there can be up to 100 or so synchronous clients connected and the game needs to message each connected client with updates every few seconds and so every few seconds this game needs to iterate through the shared list of players.
When a player joins, or leaves, the list needs to be updated accordingly to reflect the changes.
To add to that, the players can interact with the game and chat to other players, and every time a message is received from the player the game would again need to iterate through the same list and do a broadcast.
The exceptions are typically thrown when the game is trying to broadcast messages to players (read operation) at the same time as many players leaving/joining (write operation) at the same time.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
鉴于您对游戏结构的描述,考虑有一个线程是唯一可以直接访问玩家列表的线程。使该列表对该线程有效地私有。
任何其他线程访问列表的方式都是通过向列表管理器线程发送消息。因此该线程有一个等待的消息队列。当队列非空时,它会按照消息的指示仔细检查消息。他们可能会说“添加新玩家”、“删除玩家”或“将该玩家的状态更新为‘不高兴’”。
该线程还可以定期扫描列表以将更新发送到客户端,或者它可以(甚至更好)利用它具有列表中发生的已知更改流的事实,并将这些更改转发到客户端。
基本原理:将数据私有给一个线程,线程之间通过消息队列进行通信。
您的基本数据结构是线程安全的队列类。已经有几十个关于 SO 的例子了。 (并避开任何声称“无锁”且线程安全的内容。这是不值得冒的风险。)
Given your description of the game structure, considering having a single thread that is the only thread that can directly access the list of players. Make the list effectively private to that one thread.
The way any other threads access the list is by sending messages to the list-manager thread. So that thread has a queue of messages that it waits on. While the queue is non-empty, it chews through the messages, following their instructions. They might say "Add a new player", or "Remove a player", or "Update the status of this player to 'unhappy'".
This thread can also periodically scan the list to send updates out to clients, or it can (even better) use the fact that it has a known stream of changes that are occurring to the list, and forward just those changes on to the clients.
Basic principle: make data private to one thread, and have the threads communicate via message queues.
Your basic data structure is a thread-safe queue class. There are dozens of examples on SO already. (And steer clear of any that claim to be "lock free" and yet thread safe. It's just not worth the risk.)
尽管有关重构的其他建议是可行的方法,但“线程安全”类中的一个错误会溢出(可能还有更多):
您之前从
_inner.GetEnumerator
创建枚举器构造函数已运行,因此任何线程都可以自由修改集合,直到您锁定构造函数为止。这是一个很小的时间间隙,但如果有 80 个线程,就会发生这种情况。您需要锁定return ..
语句以保护枚举器。编辑:也在其他使用相同模式的地方。
Even though the other suggestions about a refactoring is the way to go, one error in the 'thread-safe' class spills out (there might be more):
You create the enumerator from the
_inner.GetEnumerator
BEFORE the constructor is run, and thus any thread is free to modify the collection until you lock in the constructor. It is a small time slot, but with 80 threads it will happen. You need to lock around the thereturn ..
statement to protect the enumerator.EDIT: And also in the other places where you use the same pattern.