【问题标题】:Design multi-threaded chat server with low thread contention设计具有低线程争用的多线程聊天服务器
【发布时间】:2019-10-22 21:07:21
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

  • 目标平台是什么?
  • 您希望同时支持多少用户? (我问是因为通常人们不会很快打字,所以即使有大量用户,您的服务器也可能花费 99.9% 的时间来等待任何人向它发送一些文本,这意味着多线程设计可能会矫枉过正,因为单核会很快)
  • @JeremyFriesner 这只是出于学习目的,我不希望很快有用户。我认为你是对的,我最好使用单线程,但我很好奇人们如何有效地执行多线程

标签: c++ multithreading network-programming


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

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

    不一定。由于您的地图是指针的地图,而不是对象的地图访问表与访问套接字不同,保护前者并不意味着后者也需要受到保护,即使它存在于数据结构中。

    但是,您需要确保对象的生命周期得到安全处理。这是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 内的查找,而其余操作将是并发的。

    【讨论】:

      【解决方案3】:

      简单的解决方案是创建一个引用计数消息类并使用消息队列。如果 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 来为我完成大部分工作。

      【讨论】:

        【解决方案4】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-01-22
          • 2015-02-17
          • 1970-01-01
          • 2013-04-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多