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

C++ 高吞吐量非阻塞服务器设计:繁忙等待的替代方案,c++,multithreading,algorithm,producer-consumer,busy-waiting,C++,Multithreading,Algorithm,Producer Consumer,Busy Waiting,我一直在为彩信构建一个高吞吐量的服务器应用程序,实现语言是C++。每台服务器可以独立使用,也可以将多台服务器连接在一起,以创建基于DHT的覆盖网络;这些服务器就像Skype中的超级对等服务器一样 这项工作正在进行中。目前,该服务器每秒可处理约200000条消息(256字节消息),在我的机器上(Intel i3 Mobile 2 GHz、Fedora Core 18(64位)、4GB RAM)处理长度为4096字节的消息的最大吞吐量约为256MB/s。服务器有两个线程,一个用于处理所有IOs(基于

我一直在为彩信构建一个高吞吐量的服务器应用程序,实现语言是C++。每台服务器可以独立使用,也可以将多台服务器连接在一起,以创建基于DHT的覆盖网络;这些服务器就像Skype中的超级对等服务器一样

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

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

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

现在我的问题是:Thread#1将序列化消息一次排队一条消息(实际上是指向消息对象的指针),而Thread#2将消息从队列中分块取出(分块32/64/128),并通过第二个循环缓冲区分块返回处理过的消息。如果没有新消息,线程#2会一直忙着等待,从而使其中一个CPU内核一直忙着。如何进一步改进设计?繁忙等待策略的替代方案是什么?我想优雅高效地完成这件事。我考虑过使用信号量,但我担心这不是最好的解决方案,原因很简单,每次我在线程1中排队时都必须调用“sem#u post”,这可能会大大降低吞吐量,第二个线程必须调用“sem#u post”相同的次数,以防止信号量溢出。我还担心信号量实现可能在内部使用互斥

第二个好的选择可能是使用signal,如果我可以发现一个算法,仅当第二个线程“清空队列并正在调用sigwait”或“已经在等待sigwait”时,才可以发出信号,简言之,信号必须至少被发出几次,不过,如果信号比需要的信号高出几倍,也不会有什么坏处。是的,我确实使用谷歌搜索,但我在互联网上找到的解决方案都不令人满意。以下是一些注意事项:

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

B.必须有非常低的开销,并且算法必须有效

C.任何时候都不能上锁

我想把所有选项都放在桌面上。

以下是我共享服务器信息的网站链接,以便更好地了解其功能和用途: www.wanhive.com

当队列为空时,您可以让线程2进入睡眠状态X毫秒

X可以由您想要的队列长度+一些保护带来确定

顺便说一句,在用户模式(ring3)下,您不能使用MONITOR/MWAIT指令,这将非常适合您的问题

编辑

你一定要试试(有免费的版本)。听起来像你要找的

Edit2


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

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


如果您需要尽快唤醒线程2,那么忙碌的等待是很好的。事实上,这是通知一个处理器另一个处理器所做更改的最快方式。您需要在两端生成内存围栏(一边是写围栏,另一边是读围栏)。但只有当两个线程都在专用处理器上执行时,此语句才成立。在这种情况下,不需要切换上下文,只需缓存一致性通信

有一些改进是可以做的

  • 如果线程#2通常是CPU受限的,并且等待很忙,那么调度程序可能会惩罚它(至少在windows和linux上是这样)。OS调度器动态调整线程优先级,以提高总体系统性能。它降低了占用大量CPU时间的CPU绑定线程的优先级,以防止线程不足。您需要手动增加线程#2的优先级以防止出现这种情况
  • 如果您有多核或多处理器机器,那么最终将导致处理器订阅不足,并且您的应用程序将无法利用硬件并发性。您可以通过使用多个处理器线程(线程#2)来缓解这一问题
  • 处理步骤的并行化。 那里
    // 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