我正在用C编写一个简单的网络服务器,用于我正在做的课程。一个要求是我们实现一个线程池来处理使用pthreads的连接。
我知道我将如何大致这样做(在主线程中调用accept并将文件描述符传递给freee线程),但是我的朋友提出了一种替代方法而不是我想到的方法:创建我的所有线程在前面,并让他们全部循环接听电话。接受的想法是阻塞所有空闲线程,当连接进入时,只给出一个文件描述符。然后当一个给定的线程完成一个连接时,它会循环回来并阻塞一个调用再次接受。使用accept()作为信号量的调用。这样可以简化其实现的实现,因为您不需要跟踪哪些线程正忙,哪些线程已准备好进行连接。理论上它的延迟也会更低,因为线程可以立即开始执行。
我的问题是,这样安全吗?我打算实施它并尝试一下,但我还没准备好,我很想知道答案。我在谷歌和这里搜索stackoverflow,但找不到任何人这样做。接受线程安全吗?我假设这种方法会有更多的开销,因为你一直在运行所有的线程,这两种方法只是一个简单的内存/延迟权衡吗?
编辑:我不确定这应该是社区维基,如果它应该是道歉,我找不到按钮:P
答案 0 :(得分:13)
是。这是设计多线程服务器和接受设计实践的常用方法。
您还可以多次fork
并让子进程调用accept
,这将允许您在不需要线程库的情况下执行多线程处理。较旧的服务器就是这样做的。
答案 1 :(得分:3)
由于受到赏识,请寻求参考:
是的,accept()
是线程安全的,因为POSIX定义了该术语。
相关参考为section 2.9.1 of POSIX.1,其当前版本为:
此POSIX.1-2017卷定义的所有功能应为 线程安全的,除了以下功能不需要 线程安全的。
[不包含
accept()
的列表]
为了完整起见,POSIX 确实定义了accept()
:https://pubs.opengroup.org/onlinepubs/9699919799/functions/accept.html,因此作为未出现在例外列表中的POSIX函数,POSIX指定它是线程-安全。
答案 2 :(得分:2)
在评论中,@ Rick(赏金提供者)说:
从某些方面来说,我现在明白了。线程安全是符合行为的属性。我当时认为线程安全必须是同一进程中的线程。但是现在我认为同一进程中的线程之间或不同进程之间的线程之间并没有太大差异。在某些方面,它们是相同的。因此,线程安全性的概念可以同时应用于两种情况。
有关线程安全性的POSIX定义确实引用了同一进程中的线程(请参阅:§2.9)。
如果您要询问fork()
之后发生了什么,并且父母和孩子并发调用accept()
是否安全,我们首先要注意POSIX定义了称为{ {3}}。然后我们注意到connection indication queue的子级获得父级描述符的副本,因此,子级和父级将访问相同的连接指示队列(就像文本文件的重复文件描述符将访问同一文件一样) )。
此时accept()
对每个进程(子进程和父进程)的定义是相同的。
答案 3 :(得分:2)
应用程序针对系统上的libc
实现链接,以调用accept()
和其他与套接字相关的功能(#include <sys/socket.h>
)。您想阅读其文档。
在Linux上最常见的libc
实现来自GNU(或者在Android上是Google的bionic
),它被称为glibc
,很可能是您所要做的(会被)使用。如accept
documentation中针对glibc
所述:
功能:int接受(int套接字,struct sockaddr * addr,socklen_t * length_ptr)
初步:| MT安全| AS安全交流安全fd | See POSIX Safety Concepts.
如 POSIX安全概念中所述,初步部分枚举了以下属性:
根据POSIX标准中针对诸如线程安全,异步信号安全和异步取消安全之类的安全性上下文中列出的标准进行评估。
随后将对此类概念进行解释(另请参见"Thread Safety" on wikipedia,以获取实现线程安全性的不同方法)。根据文档,accept
被声明为 MT安全:
在存在其他线程的情况下,可以安全地调用MT安全或线程安全功能。 MT-Safe中的MT代表多线程。
成为MT-Safe并不意味着功能是原子的,也不意味着它使用POSIX向用户提供的任何内存同步机制。甚至有可能依次调用MT-Safe功能不会产生MT-Safe组合。例如,让一个线程一个接一个地调用两个MT-Safe函数不能保证等同于原子执行两个函数组合的行为,因为其他线程中的并发调用可能会以破坏性方式进行干扰。
可跨库接口内联函数的整个程序优化可能会暴露不安全的重新排序,因此不建议跨GNU C库接口执行内联。在整个程序优化中,不能保证已记录的MT安全状态。但是,在用户可见的标头中定义的函数旨在安全地进行内联。
glibc
的{{1}}实现只是重定向到内核系统调用,这一事实使得该描述对于Linux系统上的其他accept
实现也很有用(可能只是执行也重定向到系统调用)。
另一方面,更通用的方法是检查系统上的man-pages project
(如果可用)(在大多数系统上,这是与官方文档最接近的东西),>
[...]介绍了用户空间程序采用的Linux内核和C库接口。关于C库,主要关注点是GNU C库(glibc),尽管众所周知,该文件还包括可用于Linux的其他C库的变体文档。
通过在命令行中输入man 2 accept
:
[...] 符合
accept():POSIX.1-2001,POSIX.1-2008,SVr4、4.4BSD(首先使用accept() 出现在4.2BSD中。
我们看到POSIX.1-2008
是可行的参考(请检查this以获取有关Linux系统相关标准的描述)。正如在其他答案中已经说过的那样,libc
标准将POSIX.1
函数指定为(accept
)线程安全(如 Base Definitions中的3.399节中所定义)安全),因为它没有在系统接口第2.9.1节“线程安全” 中列出。
最后,由于POSIX-
仅代表内核的glibc
,因此,信誉最好的源代码是内核源代码(当然)。 This answer在进行accept()
时会经过内核代码路径:看一下并说服自己共享资源受到自旋锁的保护,特别是socket state和queue连接等待申请accept()
余额。