高吞吐量无阻塞服务器设计:繁忙等待的替代方案

时间:2013-12-02 06:51:52

标签: c++ multithreading algorithm producer-consumer busy-waiting

我一直在构建用于多媒体消息传递的高吞吐量服务器应用程序,实现语言是 C ++ 。每个服务器可以在独立模式下使用,或者许多服务器可以连接在一起以创建基于DHT的覆盖网络;服务器就像超级同行一样,就像Skype一样。

工作正在进行中。目前,服务器每秒可处理大约200,000条消息(256字节消息),并且在我的机器上具有大约256 MB / s的最大吞吐量(Intel i3 Mobile 2 GHz,Fedora Core 18(64位),4 GB RAM)消息长度为4096字节。服务器有两个线程,一个线程用于处理所有IO(基于epoll,边缘触发),另一个用于处理传入消息。还有另一个用于叠加管理的线程,但在当前的讨论中并不重要。

讨论中的两个线程使用两个循环缓冲区共享数据。线程号1使用一个循环缓冲区为线程号2排队新消息,而线程号2通过另一个循环缓冲区返回已处理消息。服务器完全无锁。我没有使用任何同步原语,甚至不使用原子操作。

循环缓冲区永远不会溢出,因为消息是合并的(在启动时预先分配)。实际上,所有重要/经常使用的数据结构都被合并以减少内存碎片并提高缓存效率,因此我们知道服务器启动时我们将要创建的最大消息数,因此我们可以预先计算最大值缓冲区的大小,然后相应地初始化循环缓冲区。

现在我的问题: 线程#1一次一条消息将序列化消息排入队列(实际上是指向消息对象的指针),而线程#2从消息对象中取出消息块中的队列(32/64/128的块),并通过第二个循环缓冲区以块的形式返回已处理的消息。如果没有新消息,则线程#2保持忙等待,因此始终保持其中一个CPU核心忙。如何进一步改进设计?繁忙的等待策略有哪些替代方案?我想优雅而有效地做到这一点。我考虑使用信号量,但我担心这不是最好的解决方案,原因很简单,我每次在线程#1中排队消息时都必须调用“sem_post”,这可能会大大减少吞吐量,第二个线程必须调用“sem_post”相等的次数,以防止信号量溢出。另外我担心信号量实现可能在内部使用互斥锁。

第二个不错的选择可能是使用信号如果只有当第二个线程“清空队列并且正在调用sigwait”时才能发现用于引发信号的算法,或者是“已经等待sigwait“,简而言之,信号必须提升最少次数,尽管如果信号比需要的频率提高几倍也不会受到伤害。是的,我确实使用过Google搜索,但我在互联网上找到的解决方案都不令人满意。以下是一些注意事项:

一个。在进行系统调用时,服务器必须浪费最少的CPU周期,并且系统调用必须使用最少次数。

B中。必须有非常低的开销,算法必须高效。

℃。没有锁定的东西。

我希望所有选项都放在桌面上。

以下是我分享有关我的服务器信息的网站的链接,以便更好地了解功能和目的: www.wanhive.com

3 个答案:

答案 0 :(得分:2)

听起来您想协调由某个共享状态连接的生产者和消费者。至少在Java中,对于这种模式,避免忙等待的一种方法是使用wait和notify。使用这种方法,如果线程#2通过调用wait发现队列为空并且避免旋转CPU,则它可以进入阻塞状态。一旦线程#1将一些东西放入队列,它就可以进行通知。在C ++中快速搜索这样的机制产生了这个:

wait and notify in C/C++ shared memory

答案 1 :(得分:2)

如果你需要尽可能快地唤醒2号线,那么忙碌的等待是好的。实际上,这是向一个处理器通知另一个处理器所做更改的最快方法。您需要在两端生成内存栅栏(在一侧写入栅栏,在另一侧读取栅栏)。但只有当您的两个线程都在专用处理器上执行时,此语句才成立。在这种情况下,不需要上下文切换,只需缓存一致性流量。

可以做出一些改进。

  1. 如果线程#2通常是CPU绑定并且忙于等待 - 它可能会受到调度程序的惩罚(至少在Windows和Linux上)。 OS调度程序动态调整线程优先级以提高整体系统性能。它减少了CPU绑定线程的优先级,这会消耗大量的CPU时间以防止线程不足。您需要手动增加线程#2的优先级以防止这种情况发生。
  2. 如果你有多核或多处理器机器,你最终会得不到处理器的订阅,你的应用程序将无法利用硬件并发。您可以通过使用多个处理器线程(线程#2)来缓解这种情况。
  3. 处理步骤的并行化。 有两种选择。

    1. 您的邮件已完全订购,需要按照到达时的顺序进行处理。
    2. 可以重新排序邮件。处理可以按任何顺序进行。
    3. 在第一种情况下,您需要N个循环缓冲区和N个处理线程以及N个输出缓冲区和一个消费者线程。线程#1在该循环缓冲区中以循环顺序排列消息。

      // Thread #1 pseudocode
      auto message = recv()
      auto buffer_index = atomic_increment(&message_counter);
      buffer_index = buffer_index % N;  // N is the number of threads
      // buffers is an array of cyclic buffers - Buffer* buffers[N];
      Buffer* current_buffer = buffers[buffer_index];
      current_buffer->euqueue(message);
      

      每个线程使用来自其中一个缓冲区的消息,并将结果排入其专用输出缓冲区。

      // Thread #i pseudocode
      auto message = my_buffer->dequeue();
      auto result = process(message);
      my_output_buffer->enqueue(result);
      

      现在您需要在到达顺序中处理所有这些消息。您可以通过循环方式从输出循环缓冲区中取出已处理的消息来使用专用的消费者线程执行此操作。

      // Consumer thread pseudocode
      // out_message_counter is equal to message_counter at start
      auto out_buffer_index = atomic_increment(&out_message_counter);
      out_buffer_index = out_buffer_index % N;
      // out_buffers is array of output buffers that is used by processing
      // threads
      auto out_buffer = out_buffers[out_buffer_index];
      auto result = out_buffer->dequeue();
      send(result);  // or whatever you need to do with result
      

      在第二种情况下,当您不需要保留消息顺序时 - 您不需要消费者线程并输出循环缓冲区。你只需要处理处理线程中的结果就可以了。

      N必须等于num CPU's - 在第一种情况下为3(“ - 3”是一个I / O线程+一个消费者线程+一个DHT线程)和num CPU's - 在第二种情况下为2(“ - 2“是一个I / O线程+一个DHT线程)。这是因为如果你有处理器的超额认购,那么忙等待就不会有效。

答案 2 :(得分:1)

当队列为空时,你可以让线程#2进入休眠状态X毫秒。

X可以通过你想要的队列长度+一些保护带来确定。

BTW,在用户模式(ring3)中,您无法使用适用于您的问题的MONITOR / MWAIT指令。

修改

你一定要试试TBB's RWlock(有免费版)。听起来像你正在寻找的东西。

<强> EDIT2

另一种选择是使用条件变量。它们涉及互斥和条件。基本上你等待条件成为“真实”。可以找到低级别的pthread内容here