在多线程iocp服务器中向新连接的用户发送已连接用户的列表

时间:2014-09-10 18:59:35

标签: c++ c multithreading critical-section iocp

我需要一些建议如何发送正确的双向链接用户列表。到目前为止,有关我的代码和方法的一些基本信息:

我将所有连接用户的信息保存在双向链表中,这是在线程之间共享的。我将列表的头部存储在全局变量中:*PPER_user g_usersList,用户的struct看起来像:

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);

但是有关上述代码的一些事情让我感到困惑:

  1. 链表很少被其他线程使用。连接的用户越多(例如100,1000),在整个ghatering数据持续时间内锁定的时间段越长。我应该与此协调还是找到更好的方法来做到这一点?

  2. 似乎当一个线程锁定列表而while循环通过所有链接结构(用户)收集所有id,status时,其他线程应该使用相同的CriticalSection(& g_CSuserslist)当用户想要更改自己的id,status等。但这可能会破坏性能。也许我应该改变我的应用程序的所有设计或什么?

  3. 您可能会有任何见解,我们将不胜感激。提前谢谢。

4 个答案:

答案 0 :(得分:3)

我在您的代码中看到的唯一问题(更常见的是在您的应用说明中)是保护g_usersList的关键部分的大小。规则是在关键部分中避免任何耗时的操作。

所以你必须保护:

  • 添加新用户
  • 在deconnexion删除用户
  • 获取列表的快照以进行进一步处理

所有这些操作都只是内存,所以除非你处于非常繁重的条件下,否则一切都应该没问题如果你把所有IO放在关键部分之外 (1),因为它只发生在用户连接/断开连接时。如果你把WSASend放在临界区之外,一切都应该没问题,恕我直言就够了。

根据评论编辑:

您的结构user相当小,我会说10到18个有用字节(取决于指针大小4或8个字节),以及24个字节中的12个字节,包括填充。对于1000个连接用户,您只需要复制少于24k字节的内存,并且只需测试下一个user是否为空(或者最多保持当前连接用户数为更简单的循环)。无论如何,维护这样的缓冲区也应该在关键部分进行。恕我直言,直到你有超过1000个用户(10k到100k之间,但你可能会遇到其他问题...)一个简单的全局锁(如你的关键部分)围绕user的整个双链表应该足够了。但所有这些都需要进行探测,因为它可能取决于外部事物,如硬件......


太长了不要阅读讨论:

当您描述您的应用程序时,您只会在新用户连接时收集已连接用户的列表,因此每两次写入只有一次完整读取(一次在连接时,一次在断开连接时):恕我直言是没有用的尝试实现用于阅读的共享锁和用于写入的独占锁。如果你在连接和断开连接之间做了很多次读取,那就不一样了,你应该尝试允许并发读取。

如果您确实发现争用太大,因为您拥有大量连接用户且频繁连接/断开连接,您可以尝试实现行级别,如锁定。而不是锁定整个列表,只锁定您正在处理的内容:插入的顶部和第一个,当前记录加上删除的前一个和下一个,以及读取时的当前和下一个。但是编写和测试会比较费时,因为在阅读列表时你必须做很多锁定/释放,而且你必须非常谨慎以避免死锁状态。所以我的建议是不要那样做,除非确实需要。


(1)在您显示的代码中,当 在外面时,WSASend(...,wsabuf,...); 是关键部分。改为写:

...
LeaveCriticalSection(&g_CSuserslist);
WSASend(...,wsabuf,...);

答案 1 :(得分:2)

第一个性能问题是链表本身:遍历链表比遍历数组/ std::vector<>要花费更长的时间。单个链表的优点是允许通过原子类型/比较和交换操作来安全地插入/删除元素。一个双链表很难以线程安全的方式维护,而不需要使用互斥锁(它总是大而重的枪)。

因此,如果您使用互斥锁来锁定列表,请使用std::vector<>,但您也可以使用单个链表的无锁实现来解决您的问题:

  • 您有一个链接列表,其中一个头是一个全局的原子变量。

  • 所有条目在发布后都是不可变的。

  • 添加用户时,请获取当前头并将其存储在线程局部变量(原子读取)中。由于条目不会更改,因此即使其他线程在您遍历时添加了更多用户,您也有时间遍历此列表。

  • 要添加新用户,请创建包含它的新列表头,然后使用比较和交换操作将旧列表头指针替换为新列表头指针。如果失败,请重试。

  • 要删除用户,请遍历列表,直到在列表中找到该用户。在您遍历列表时,将其内容复制到新链接列表中新分配的节点。找到要删除的用户后,将新列表中最后一个用户的下一个指针设置为已删除用户的下一个指针。现在,新列表包含除已删除用户之外的旧用户的所有用户。因此,您现在可以通过列表头上的另一个比较和交换来发布该列表。不幸的是,如果发布操作失败,你将不得不重做工作。

    不要将已删除对象的下一个指针设置为NULL,另一个线程可能仍需要它来查找列表的其余部分(在其视图中,该对象尚未被删除)。

    不要立即删除旧列表头,另一个线程可能仍在使用它。最好的办法是将其节点排入另一个列表进行清理。这个清理列表应该不时地用新的清单替换,并且在所有线程都给出它们之后应该清理旧清单(你可以通过传递令牌来实现它,当它返回到原始进程时,你可以安全地销毁旧物件。

由于列表头指针是唯一可以更改的全局可见变量,并且由于该变量是原子的,因此这种实现保证了所有添加/删除操作的总排序。

答案 2 :(得分:1)

“正确”的答案可能是向用户发送较少的数据。他们真的需要知道每个其他用户的身份和状态,还是只需要知道可以动态保持最新的汇总信息。

如果您的应用必须发送此信息(或此类更改被视为过多工作),那么您可以通过仅进行此计算(例如每秒一次(甚至每分钟)一次)来显着减少处理。然后,当有人登录时,他们会收到此信息的副本,最多只有1秒钟。

答案 3 :(得分:1)

这里的真正问题是如何紧急将该列表的每个字节发送给新用户?

客户端如何跟踪此列表数据?

如果客户端可以处理部分更新,那么“涓流”会更有意义吗?每个用户的数据 - 可能使用时间戳来指示数据的新鲜度,而不必以如此庞大的方式锁定列表?

您还可以切换到rwsem样式锁,其中列表访问仅在用户打算修改列表时是独占的。