繁体   English   中英

设计具有低线程争用的多线程聊天服务器

[英]Design multi-threaded chat server with low thread contention

我在考虑如何以某种方式使用 C++ 制作一个多线程聊天服务器,从而最大限度地减少线程争用。

在我的初始设计中,我在服务器中有一个 sockets 的std::vector 当客户端连接到服务器时,套接字被添加到 sockets 的这个向量中。

还有一个std::unordered_map<string, Socket*>允许为用户名查找相应的套接字。 当客户端使用其用户名和密码登录时,我们向 hash map 添加一个条目。 当用户注销时,我们删除 hash map 中的相应条目。

客户端将发送以用户名为地址的消息。 当他们到达服务器时,我们使用 hash map 查找套接字,并通过该套接字发送消息。

由于服务器是多线程的,并且提到的数据结构可以从不同的线程读取/写入,我们现在需要使用一些线程同步机制来保护它们,例如互斥锁。 但我认为这样做会因为线程争用而降低性能。 基本上,所有线程都需要访问这些数据结构才能发送消息,但只有其中一个线程可以同时使用它们。 我认为使用这种方法的性能不会比使用单线程更好。

如何改进我的设计以获得更好的性能?

我认为使用这种方法的性能不会比使用单线程更好。

不必要。 由于您的 map 是指针的 map,而不是对象的 map数据结构。

但是,您需要确保安全处理 object 的生命周期。 这是std::shared_ptr<>是您的朋友的情况之一,因为它保证了线程安全的所有权安全。

例如:

std::mutex table_mtx;
std::unordered_map<string, std::shared_ptr<Socket>> sockets;

void send(const std::string& msg, const std::string& dst_name) {
  std::shared_ptr<Socket> dst;
  {
    std::lock_guard<std::mutex> lock(table_mtx);

    // Increments the ref-count on the socket, so even if it's removed 
    // from the map, it won't be deleted until we are done with it. 
    dst = sockets.at(dst_name);
  }

  if(dst) {
    dst->send(msg);
  }
}

显然,当同时使用同一个套接字时, Socket也需要有一个内部互斥体来处理争用。 但是,如果 user1 向 user2 发送消息,而 user3 向 user4 发送消息,则争用将仅限于 map 内的查找,而操作的 rest 将是并发的。

由于聊天具有高度的时间相关性(因为对话),逻辑答案是缓存结果。 您需要一个弱 ptr 来进行失效。

简单的解决方案是创建一个引用计数消息 class 并使用消息队列。 如果 Alice 想向 Bob 和 Charlie 发送消息,则创建引用计数消息 class 的实例,然后调用“队列消息”function 将同一消息的实例排队发送给 Bob 和 Charlie。

“队列消息”功能的工作原理如下:

  1. 获取客户端 map 锁。
  2. 找到客户。
  3. 锁定客户端发送队列锁。
  4. 释放客户端 map 锁。
  5. 将消息添加到客户端的发送队列。
  6. 如果发送队列为空,则调用异步发送 function。
  7. 释放客户端发送队列锁。

您的服务器所做的大部分工作将完全不在此“队列消息” function 之外。 所有的发送、解析和接收都可以在没有任何锁的情况下进行。 当你收到一条消息时,你可以遵循同样的逻辑:

  1. 接收数据。
  2. 将其解析为消息。
  3. 获取客户端的接收队列锁。
  4. 将消息放在客户端的接收队列中。
  5. 如果接收队列为空,则调度客户端的消息处理引擎。
  6. 释放客户端的接收队列锁。

接收队列调度逻辑:

  1. 获取客户端的接收队列锁。
  2. 如果队列为空,则释放锁并停止。
  3. 从客户端的接收队列中拉出一条消息。
  4. 释放客户端的接收队列锁,以便新接收的消息可以排队。
  5. 处理收到的消息。
  6. Go 到步骤 1。

顺便说一句,我是 WebMaster 的 ConferenceRoom 软件的主要开发者。 所以我做了这个。 在十多年前的硬件上以这种方式处理一万个客户是没有问题的。 今天,我会使用 boost 为我完成大部分工作。

第一个简单的解决方案:
如果服务器上有足够的资源或客户端不多,我建议在这里避免大部分的多线程复杂性,并将所有发送或接收功能放在一个线程中(一个用于发送,另一个用于接收操作)。 因此,线程有它们的工作套接字,并且只保留发送和接收客户端队列的锁。 这些锁可以由生产者/消费者模式处理。

更高级但也更复杂的解决方案:您必须使用更优化的结构。 使用“unordered_map” object 会使您的套接字搜索机制非常低效。 此外,您不应该在需要锁的任何地方使用排他锁,也可以考虑在任何可能的地方使用非排他锁。
无论如何,最好利用现有的线程安全和无锁库。 你可以在网上找到很多。 我在 Google 上为您搜索一个:
https://github.com/khizmax/libcds

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM