同时等待条件(pthread_cond_wait)和套接字变化(select)

10

我正在用c/c++编写一个符合POSIX标准的多线程服务器,它必须能异步接受、读取和写入大量连接。该服务器有多个工作线程执行任务,并且偶尔(且不可预测地)将数据排队以写入套接字。客户端也会偶尔(且不可预测地)向套接字写入数据,因此服务器也必须异步读取。一个明显的方法是为每个连接分配一个线程,该线程从其套接字读取和写入数据;但是,这种方式很丑陋,因为每个连接可能会持续很长时间,因此服务器可能需要保持数百或数千个线程来跟踪连接。

更好的方法是使用select()/pselect()函数处理所有通信的单个线程。即,单个线程等待任何套接字变为可读状态,然后生成一个任务以处理输入,该任务将在其他工作线程池中处理每当输入可用时。每当其他工作线程为一个连接产生输出时,它会被排队,通信线程将在可写时等待该套接字,然后进行写操作。

问题在于,当服务器的工作线程排队输出时,通信线程可能在select()或pselect()函数中等待。如果几秒钟或几分钟内没有输入到达,则排队的输出将等待通信线程完成select()。然而,这不应该发生--数据应尽快写入。

目前我看到有几种线程安全的解决方案。其中一种是让通信线程在输入上忙等待并更新它每秒钟左右等待写入的套接字列表,这并不是最优的选择,因为它涉及到忙等待,但它可以工作。另一个选择是使用pselect(),并在新输出排队时发送USR1信号(或类似信号),允许通信线程立即更新它等待可写状态的套接字列表。我更喜欢后者,但仍然不喜欢将信号用于应该是条件(pthread_cond_t)的东西。还有另一个选择是在select()等待的文件描述符列表中包括一个虚拟文件,当需要添加套接字到可写fd_set以进行select()调用时,我们向其写入单个字节;这将唤醒通信服务器,因为特定虚拟文件会变成可读状态,从而允许通信线程立即更新可写fd_set。

直观上,我感觉第二种方法(使用信号)是编写服务器的“最正确”方法,但我很好奇是否有人知道上述哪种方法通常最有效,是否会导致我不知道的竞态条件,或者是否有人知道此问题的更通用解决方案。我真正想要的是一个pthread_cond_wait_and_select()函数,它允许comm线程等待套接字变化或条件信号。

提前感谢您的帮助。

3个回答

6
这是一个非常普遍的问题。
经常使用的解决方案之一是将管道作为从工作线程返回到 I/O 线程的通信机制。工作线程完成任务后,将结果指针写入管道中。I/O 线程等待管道的读取端和其他套接字和文件描述符一起等待,一旦管道准备好读取,它就会被唤醒,检索结果指针,然后以非阻塞方式继续将结果推送到客户端连接中。
请注意,由于小于或等于 PIPE_BUF 的管道读取和写入是原子性的,所以指针会一次性地被写入和读取。甚至可以有多个工作线程将指针写入同一个管道,因为原子性得到了保证。

4
很遗憾,每个平台都有不同的最佳实践。通用、可移植的方法是在I/O线程中使用poll进行阻塞。如果需要让I/O线程离开poll,则需要在线程正在轮询的pipe上发送一个字节。这将立即导致线程从poll退出。
在Linux上,epoll是最佳选择。在基于BSD的操作系统(包括OSX,我想),kqueue是最佳选择。在Solaris上,曾经使用/dev/poll,现在有其他名称的方法,但我忘记了。
您可能只需考虑使用像libeventBoost.Asio这样的库。它们为支持的每个平台提供了最佳的I/O模型。

3
你的第二种方法更加简洁明了。在你的列表中包含自定义事件,比如 select 或者 epoll ,是很正常的事情。我们在我的当前项目中也使用定时器(在 Linux 上是 timerfd_create )来处理这些事件。在 Linux 上,eventfd 允许你创建这样任意用户事件,因此我认为这是非常被接受的做法。对于仅限于 POSIX 函数的情况,嗯,也许可以使用其中的一个管道命令或者 socketpair 命令。
忙碌轮询不是一个好的选择。首先,你将扫描其他线程使用的内存,从而导致 CPU 内存争用。其次,你将始终需要返回到你的 select 调用,这将创建大量的系统调用和上下文切换,从而损害整个系统的性能。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接