0

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

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

还有一个std::unordered_map<string, Socket*>允许查找用户名的相应套接字。当客户端使用它的用户名和密码登录时,我们会在哈希映射中添加一个条目。当用户注销时,我们删除哈希图中的相应条目。

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

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

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

4

4 回答 4

2

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

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

于 2019-10-23T06:23:07.317 回答
1

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

不必要。由于您的映射是指针映射,而不是对象映射访问表与访问套接字不同,保护前者并不意味着后者也需要保护,即使它位于数据结构。

但是,您需要确保对象的生命周期得到安全处理。这是您的朋友的情况之一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在同时使用同一个套接字时,还需要有一个内部互斥体来处理争用。但是,如果用户 1 向用户 2 发送消息,而用户 3 向用户 4 发送消息,则争用将仅限于地图内的查找,而其余操作将是并发的。

于 2019-10-22T21:18:36.340 回答
1

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

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

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

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

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

接收队列调度逻辑:

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

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

于 2019-10-23T05:11:37.043 回答
0

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

于 2019-10-23T05:01:46.717 回答