【问题标题】:Waiting on a condition (pthread_cond_wait) and a socket change (select) simultaneously同时等待条件 (pthread_cond_wait) 和套接字更改 (select)
【发布时间】:2012-01-25 10:52:08
【问题描述】:

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

更好的方法是让一个线程使用 select()/pselect() 函数处理所有通信。即,单个线程在任何套接字上等待可读,然后生成一个作业来处理输入,只要输入可用,该输入将由其他线程池处理。每当其他工作线程为连接产生输出时,它就会进入队列,并且通信线程在写入之前等待该套接字可写。

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

现在我看到了几个线程安全的解决方案。一种是让通信线程忙于等待输入并更新它等待每十分之一秒左右写入的套接字列表。这不是最佳选择,因为它涉及忙等待,但它会起作用。另一种选择是使用 pselect() 并在新输出排队时发送 USR1 信号(或等效的东西),允许通信线程立即更新它正在等待可写状态的套接字列表。我在这里更喜欢后者,但仍然不喜欢将信号用于应该是条件的东西(pthread_cond_t)。另一个选项是在 select() 正在等待的文件描述符列表中包含一个虚拟文件,每当需要将套接字添加到 select() 的可写 fd_set 时,我们都会向该文件写入一个字节;这将唤醒通信服务器,因为该特定的虚拟文件随后将是可读的,从而允许通信线程立即更新它的可写 fd_set。

我直觉地认为,第二种方法(使用信号)是对服务器进行编程的“最正确”方法,但我很好奇是否有人知道上述哪种方法最有效,一般来说,是否以上任何一种情况都会导致我不知道的竞争条件,或者如果有人知道这个问题的更通用的解决方案。我真正想要的是一个 pthread_cond_wait_and_select() 函数,它允许 comm 线程同时等待套接字的变化或来自条件的信号。

提前致谢。

【问题讨论】:

    标签: c++ multithreading thread-safety pthreads asyncsocket


    【解决方案1】:

    这是一个相当普遍的问题。

    一种常用的解决方案是使用管道作为从工作线程返回到 I/O 线程的通信机制。完成任务后,工作线程将指向结果的指针写入管道。 I/O 线程与其他套接字和文件描述符一起在管道的读取端等待,一旦管道准备好读取,它就会唤醒,检索指向结果的指针并继续将结果推送到非客户端连接中-阻塞模式。

    请注意,由于小于或等于PIPE_BUF 的管道读取和写入是原子的,因此指针会一次性写入和读取。由于原子性保证,甚至可以有多个工作线程将指针写入同一个管道。

    【讨论】:

      【解决方案2】:

      不幸的是,每个平台的最佳方法是不同的。规范的、可移植的方法是将 I/O 线程块放在poll 中。如果您需要让 I/O 线程离开poll,您可以在线程正在轮询的pipe 上发送一个字节。这将导致线程立即从poll 退出。

      在 Linux 上,epoll 是最好的方法。在 BSD 衍生的操作系统(我认为包括 OSX)上,kqueue。在 Solaris 上,它曾经是 /dev/poll,现在我忘记了它的名字。

      您可能只想考虑使用像 libeventBoost.Asio 这样的库。他们在他们支持的每个平台上为您提供最佳的 I/O 模型。

      【讨论】:

        【解决方案3】:

        您的第二种方法是更清洁的方法。在您的列表中包含 selectepoll 之类的自定义事件是完全正常的。这就是我们在我当前的项目中为处理此类事件所做的事情。我们还使用定时器(在 Linux timerfd_create 上)来处理周期性事件。

        在 Linux 上,eventfd 允许您为此目的创建任意用户事件——因此我认为这是一种完全被接受的做法。对于仅 POSIX 的功能,嗯,嗯,也许是管道命令之一或 socketpair 我也见过。

        忙碌的轮询不是一个好的选择。首先,您将扫描将被其他线程使用的内存,从而导致 CPU 内存争用。其次,您将始终必须返回您的 select 调用,这将创建大量系统调用和上下文切换,从而损害整体系统性能。

        【讨论】:

          猜你喜欢
          • 2017-06-04
          • 1970-01-01
          • 1970-01-01
          • 2012-08-07
          • 1970-01-01
          • 2020-05-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多