2011-12-21 23 views
8

我正在写一个POSIX兼容的多线程服务器在C/C + +必须能够接受,读取和写入大量连接异步。服务器有几个工作线程执行任务,偶尔(不可预知地)将队列数据写入套接字。数据偶尔(不可预知地)被客户端写入套接字,所以服务器也必须异步读取。这样做的一个显而易见的方法是为每个连接提供一个线程,以便从其socket读写数据;但这很丑陋,因为每个连接可能会持续很长时间,因此服务器可能不得不保持数百或数千个线程来跟踪连接。等待条件(pthread_cond_wait)和套接字更改(选择)

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

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

现在我看到一些对线程安全的解决方案。一个是让通信线程忙 - 等待输入并更新它每隔十分之一秒等待写入的套接字列表。这不是最佳的,因为它涉及到忙等待,但它会起作用。另一种方法是在新输出排队时使用pselect()并发送USR1信号(或类似的东西),以允许通信线程立即更新正在等待可写状态的套接字列表。我更喜欢后者,但仍然不喜欢使用信号来表示应该成为条件的东西(pthread_cond_t)。还有一种选择是在select()正在等待的文件描述符列表中包含一个虚拟文件,当我们需要将套接字添加到select()的可写fd_set中时,我们会写入一个单独的字节;这会唤醒通信服务器,因为该特定的虚拟文件会被读取,从而允许通信线程立即更新它的可写fd_set。我觉得第二种方法(使用信号)是编程服务器的“最正确”方式,但我很好奇,如果有人知道上述哪一种方法是最高效的,一般来说,上述任何一种情况是否会导致我不知道的竞争条件,或者是否有人知道这个问题的更一般的解决方案。我真正想要的是一个pthread_cond_wait_and_select()函数,它允许comm线程等待套接字中的变化或来自条件的信号。

在此先感谢。

回答

6

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

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

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

3

第二种方法是更干净的方法。像selectepoll这样的东西在您的列表中包含自定义事件是完全正常的。这是我目前处理这类事件的项目。我们还使用定时器(在Linux timerfd_create上)定期处理事件。

在Linux上,eventfd可以让你为此目的创建这样的任意用户事件 - 因此我认为这是非常被接受的做法。对于POSIX唯一的功能,嗯,也许是其中一个管道命令或socketpair我也见过。

忙轮询不是一个好的选择。首先您将扫描将被其他线程使用的内存,从而导致CPU内存争用。其次,你将永远不得不返回到你的select调用,这将会产生大量的系统调用和上下文切换,这会伤害整个系统的性能。

3

不幸的是,要做到这一点,最好的办法是为每个平台不同。规范的,便携式的方法是让poll中的I/O线程块。如果您需要让I/O线程离开poll,则可以在线程正在轮询的pipe上发送单个字节。这将导致线程立即从poll退出。

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

你可能只是想考虑使用一个像库或libeventBoost.Asio。他们为您提供他们支持的每个平台上最好的I/O模型。