向多线程 iocp 服务器中的新连接用户发送连接用户列表
Posted
技术标签:
【中文标题】向多线程 iocp 服务器中的新连接用户发送连接用户列表【英文标题】:Sending list of connected users to newly connected user in multithreaded iocp server 【发布时间】:2014-09-10 18:59:35 【问题描述】:我需要一些建议如何正确发送已连接用户的双向链接列表。到目前为止,关于我的代码和方法的一些基本信息:
我将所有连接用户的信息保存在双向链表中,在线程之间共享。我将列表的头部存储在全局变量中:*PPER_user g_usersList
,用户的结构如下:
typedef struct _user
char id;
char status;
struct _user *pCtxtBack;
struct _user *pCtxtForward;
user, *PPER_user;
当新用户连接到服务器时,从链表中收集连接用户的数据并发送给他:
WSABUF wsabuf; PPER_player pTemp1, pTemp2; unsigned int c=0;
.....
EnterCriticalSection(&g_CSuserslist);
pTemp1 = g_usersList;
while( pTemp1 )
pTemp2 = pTemp1->pCtxtBack;
wsabuf.buf[c++]=pTemp1->id; // fill buffer with data about all users
wsabuf.buf[c++]=pTemp1->status; //
pTemp1 = pTemp2;
;
WSASend(...,wsabuf,...);
LeaveCriticalSection(&g_CSuserslist);
但是关于上面代码的一些事情让我感到困惑:
链表被其他线程大量使用。连接的用户越多(例如 100,1000),锁定数据的整个持续时间的时间列表就越长。我应该接受还是找到更好的方法来做到这一点?
似乎当一个线程锁定列表而 while 循环遍历所有链式结构(用户)收集所有 id、status 时,当用户想要更改自己的 id 时,其他线程应该使用相同的 CriticalSection(&g_CSuserslist),状态等。但这可能会破坏性能。也许我应该改变我的应用程序的所有设计或什么?
如果您有任何见解,我们将不胜感激。提前致谢。
【问题讨论】:
【参考方案1】:我在您的代码中(以及更普遍地在您的应用程序描述中)看到的唯一问题是保护g_usersList
的关键部分的大小。规则是在临界区避免任何耗时的操作。
所以你必须保护:
添加新用户 在断开连接时删除用户 获取列表的快照以进行进一步处理所有这些操作都只是内存,所以除非你在非常重的条件下,一切都应该没问题只要你把所有的 IO 放在临界区之外 (1),因为它只发生在用户连接/断开连接时。如果您将 WSASend 放在关键部分之外,一切都会好起来的,恕我直言,这就足够了。
根据评论编辑:
您的结构 user
相当小,我会说 10 到 18 个有用字节(取决于指针大小 4 或 8 个字节),总共 24 个字节中的 12 个,包括填充。对于 1000 个连接的用户,您只需复制少于 24k 字节的内存,并且只需要测试下一个 user
是否为空(或者最多保持当前连接用户的数量以实现更简单的循环)。无论如何,维护这样的缓冲区也应该在关键部分完成。恕我直言,直到您拥有超过 1000 个用户(介于 10k 和 100k 之间,但您可能会遇到其他问题......)围绕user
的整个双链表的简单全局锁定(如您的关键部分)应该足够了。但是所有这些都需要探索,因为它可能取决于硬件等外部事物......
太长不要阅读讨论:
当您描述您的应用程序时,您只会在新用户连接时收集已连接用户的列表,因此每两次写入(一次连接时和断开连接时一次)恰好有一次完整读取:恕我直言,尝试这样做是没有用的实现读共享锁和写独占锁。如果您在连接和断开连接之间进行了多次读取,这将是不同的,您应该尝试允许并发读取。
如果你真的发现争用太重,因为你有非常多的连接用户和非常频繁的连接/断开连接,你可以尝试实现一个行级别的锁定。而不是锁定整个列表,只锁定您正在处理的内容:top 和 first 用于插入,当前记录加上 previous 和 next 用于删除,以及 current 和 next 读取时。但是它会很难编写和测试,更耗时,因为您在阅读列表时必须执行许多锁定/释放,并且您必须非常小心以避免出现死锁情况。所以我的建议是除非真的需要,否则不要这样做。
(1) 在您显示的代码中,WSASend(...,wsabuf,...);
是 inside 临界区,而它应该 在外部。改写:
...
LeaveCriticalSection(&g_CSuserslist);
WSASend(...,wsabuf,...);
【讨论】:
The rule is avoid any time consuming operation** while** in critical section
那么当新用户连接到服务器时,我应该维护某种带有 id、所有用户状态的缓冲区,并复制这个缓冲区(例如在关键部分中使用 memcpy)吗?
+1 我自己倾向于使用“行级锁定”方法,仅在我从列表中的一个节点到下一个节点时才锁定。这确实要求节点稍微复杂一点,以允许删除另一个线程当前可能用作其迭代器的节点,但这并不太复杂。【参考方案2】:
第一个性能问题是链表本身:遍历链表比遍历数组/std::vector<>
花费的时间要长得多。单个链表的优点是允许通过原子类型/比较和交换操作线程安全地插入/删除元素。如果不使用互斥锁(它总是大而重的武器),双链表更难以线程安全的方式维护。
因此,如果您使用互斥锁来锁定列表,请使用std::vector<>
,但您也可以通过单链表的无锁实现来解决您的问题:
您有一个单链表,其中一个头是全局原子变量。
所有条目一经发布便不可变。
添加用户时,获取当前头部并将其存储在线程局部变量中(原子读取)。由于条目不会更改,因此您有所有时间来遍历此列表,即使其他线程在您遍历它时添加了更多用户。
要添加新用户,请创建一个包含该用户的新列表头,然后使用比较和交换操作将旧的列表头指针替换为新用户。如果失败,请重试。
要删除用户,请遍历列表,直到在列表中找到该用户。在遍历列表时,将其内容复制到新链表中新分配的节点。找到要删除的用户后,将新列表中最后一个用户的下一个指针设置为已删除用户的下一个指针。现在,新列表包含旧列表的所有用户,但删除的用户除外。因此,您现在可以通过列表头上的另一个比较和交换来发布该列表。不幸的是,如果发布操作失败,您将不得不重做工作。
不要将已删除对象的 next 指针设置为 NULL,另一个线程可能仍需要它来查找列表的其余部分(在它看来,该对象还没有被删除)。
不要立即删除旧的列表头,另一个线程可能仍在使用它。最好的办法是将其节点排入另一个列表以进行清理。这个清理列表应该不时地用一个新的替换,并且旧的应该在所有线程都给出他们的OK后清理(你可以通过传递一个令牌来实现这一点,当它返回到原始进程时,您可以安全地销毁旧对象。
由于列表头指针是唯一可以更改的全局可见变量,并且由于该变量是原子的,因此这样的实现保证了所有添加/删除操作的总排序。
【讨论】:
【参考方案3】:“正确”的答案可能是向您的用户发送更少的数据。他们真的需要知道每个其他用户的 id 和状态,还是只需要知道可以动态更新的汇总信息。
如果您的应用必须发送此信息(或此类更改被认为工作量太大),那么您可以通过仅进行此计算(例如每秒一次(甚至每分钟)一次)来显着减少处理。然后,当有人登录时,他们会收到一份最多 1 秒前的信息副本。
【讨论】:
是的,当新用户第一次登录时,他需要加载每个其他用户的id,状态(也包括昵称),因为在客户端它是活跃用户列表(某种聊天服务器)。但在我的代码中,我可以看到非常大的线程争用。即使某些已连接的客户端只想更改他自己的 id 或状态(它们是结构的成员,在双向链表中相互链接),他也不能使用自己的 Per user CriticalSection,但 CriticalSection(&g_CSuserslist) 相同,用于发送用户列表。 ...如果某个客户端在不使用相同的CS的情况下更改了他的id,那么其他客户端同时从链表中收集数据可以获得随机id。我仍然不知道如何处理。 @maciekm 您将需要一个关键部分来围绕每一个读取或写入 g_usersList 或其中任何内容的最小代码段。这是否会影响性能,直到它被测量之前没有人知道 - 但是例如在任何情况下,将 WSASend() 移出关键部分都是一个不错的举措。【参考方案4】:这里真正的问题是,将列表的每个字节发送给新用户有多紧急?
客户端跟踪此列表数据的情况如何?
如果客户端可以处理部分更新,那么将数据“涓流”给每个用户不是更有意义吗?也许使用时间戳来指示数据的新鲜度,而不必以如此庞大的方式锁定列表?
您还可以切换到 rwsem 样式锁,其中列表访问权限仅在用户打算修改列表时是独占的。
【讨论】:
以上是关于向多线程 iocp 服务器中的新连接用户发送连接用户列表的主要内容,如果未能解决你的问题,请参考以下文章