accept() 线程安全吗?
Posted
技术标签:
【中文标题】accept() 线程安全吗?【英文标题】:Is accept() thread-safe? 【发布时间】:2011-07-04 16:54:00 【问题描述】:我目前正在为我正在做的一门课程用 C 语言编写一个简单的网络服务器。一个要求是我们实现一个线程池来处理使用 pthread 的连接。
我知道我将如何大致执行此操作(在主线程中调用 accept 并将文件描述符传递给 freee 线程),但是我的朋友建议了一种替代方法,而不是我想到的方法:创建我的所有线程在前面,让他们都在接受电话时永远循环。想法是接受将阻塞所有空闲线程,并且当连接进入时,仅将文件描述符提供给一个。然后,当给定线程完成连接时,它会循环回来并阻塞再次接受的调用。本质上使用对 accept() 的调用作为信号量。他认为这将大大简化实现,因为您不需要跟踪哪些线程很忙,哪些线程已准备好进行连接。理论上延迟也会更低,因为线程可以立即开始执行。
我的问题是,这安全吗?我打算实施它并尝试一下,但我还没有准备好,我很想知道答案。我在谷歌和***上搜索过,但找不到任何人这样做。接受线程安全吗?我认为这种方法会产生更多开销,因为您一直在运行所有线程,这两种方法只是简单的内存/延迟权衡吗?
编辑:我不确定这是否应该是社区维基,如果应该是道歉,我找不到按钮:P
【问题讨论】:
这里要小心一点,“线程安全”的 POSIX 定义意味着您可以从不同的线程同时调用该函数。这并不意味着您一定可以同时调用函数改变相同的数据,结果可预测,甚至可以定义行为。strcpy
是线程安全的,但它不会占用任何类型的锁。在 POSIX 中,“非线程安全”意味着 非常 不安全。根据 POSIX 定义,accept()
是线程安全的(即不在此列表中:pubs.opengroup.org/onlinepubs/009695399/functions/…),但您需要的不止这些。
@Steve:虽然这对于您传递指针的接口是一个有效的警告,但它不适用于文件描述符/套接字。 OP 的使用非常安全。
@R..:我认为它可能是安全的,但 POSIX 在哪里解释/定义它?除了“线程安全”之外,它还有一个特定的术语吗?
@EJP:你会想,不是吗?但是例如 opengroup 没有在它定义“线程安全”的地方定义“原子”:pubs.opengroup.org/onlinepubs/9699919799/basedefs/…。我无权访问 IEEE 发布的 POSIX 标准以进行正确检查。
@Rick:系统调用应该始终是线程安全的。内核必须具有该级别的安全性才能正确运行。这是因为所有进程共享同一个内核,并且在内核内部时,来自两个独立进程的系统调用的执行看起来就像内核中的两个执行线程。 POSIX 不定义系统调用,只定义 API 的行为。如果 API 在特定实现中被实现为系统调用,则需要使其成为线程安全的。
【参考方案1】:
是的。这是设计多线程服务器和公认的设计实践的常用方法。
您也可以多次fork
并让子进程调用accept
,这将允许您在不需要线程库的情况下执行多线程。较旧的服务器会这样做。
【讨论】:
嘿@Dietrich,你能提供一些参考吗?没找到相关的权威资料。在这里查看我的问题Is there any authoritative place that says multi processes or threads accepting a listening socket is an atomic operation?。谢谢 @Rick - 这是一个系统调用。它不是库函数。没有合理的方式让 accept() 变得不安全。如果它不安全,内核本身就是不安全的。如果您愿意,您可以资助 MT 安全功能列表。我不会那样做,因为我已经知道它在那个名单上。这就是在 pthread 存在之前程序一直在拆分工作的方式。 @Rick - 见pubs.opengroup.org/onlinepubs/9699919799/functions/… - 接受被列为异步信号安全 在某些方面我现在明白了。线程安全将是满足行为的属性。我在想线程安全必须是同一进程中的线程。但是现在我认为同一进程内的线程或不同进程之间的线程之间没有太大区别。在某些方面,它们是相同的。所以线程安全的概念可以应用于这两种场景。 我认为原子和线程安全是两个不同但相关的术语。线程安全将是满足这种情况的合适术语,而不是原子的。 Difference between Atomic Operation and Thread Safety?.【参考方案2】:由于这是赏金,请求参考:
是的,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指定它是线程安全的。 p>
【讨论】:
在某些方面我现在明白了。 thread-safe 将是满足行为的属性。我在想线程安全必须是同一进程中的线程。但是现在我认为同一进程内的线程或不同进程之间的线程之间没有太大区别。在某些方面,它们是相同的。所以线程安全的概念可以应用于这两种场景。 @Rick,同一个进程中的线程共享进程的状态。这使得关于属于同一进程的线程的线程安全性比关于不同进程的线程的线程安全性严格地更强。在大多数情况下,进程隔离使跨进程线程安全不再是问题。【参考方案3】:在 cmets 中,@Rick(赏金提供者)说:
在某些方面我现在明白了。线程安全将是满足行为的属性。我在想线程安全必须是同一进程中的线程。但是现在我认为同一进程内的线程或不同进程之间的线程之间没有太大区别。在某些方面,它们是相同的。所以线程安全的概念可以应用于这两种场景。
关于线程安全的 POSIX 定义确实指的是同一进程中的线程(请参阅:§2.9)。
如果您询问fork()
之后会发生什么,并且父母和孩子同时调用accept()
是安全的,我们首先注意到POSIX 定义了一个称为connection indication queue 的系统资源。然后我们注意到fork()
的子代获得了父代描述符的副本,因此子代和父代将访问同一个连接指示队列(就像文本文件的重复文件描述符将访问同一个文件一样)。
此时accept()
对每个进程(子进程和父进程)所做的定义是相同的。
【讨论】:
啊。我知道那个连接指示队列。这就是int listen(int sockfd, int backlog)
中的backlog
所指的内容。
另外,感谢您解决了我对父母和孩子同时拨打accept()
的一些疑问。我刚刚阅读了§2.9 部分。是的,线程安全被定义为同一进程中的线程。而我关于“不同进程之间的线程安全”的想法正朝着错误的方向发展。该问题更可能与多个线程或进程同时访问您提到的连接指示队列系统范围的资源有关。
@Rick:对,listen()
会将连接指示队列与套接字相关联。因此,如果一个套接字被复制(使用dup()
或通过fork()
复制到子节点),则套接字引用相同的连接指示队列。
@Rick:API 定义给出了进程调用的行为。每个进程都应该看到与指定相同的行为。所以来自子进程和父进程的accept()
应该可以正常工作。 OP 从同一进程中的多个线程询问,这也被定义为工作,因为accept()
是线程安全的。
抱歉学究了。您说“API 定义给出了来自进程的调用行为。每个进程都应该看到指定的相同行为。”我不明白你在这里到底指的是什么,因为当我用关键字process
搜索man 2 accept
时,我没有找到任何相关或有用的东西。【参考方案4】:
应用程序链接到系统上的 libc
实现,以便调用 accept()
和其他与套接字相关的函数 (#include <sys/socket.h>
)。你想阅读它的文档。
libc
在 Linux 上最常见的实现来自 GNU(或者在 android 上可能来自 Google 的bionic
),它被称为glibc
,它很可能是您(将要)使用的。正如accept
documentation for glibc
中所述:
函数:int accept (int socket, struct sockaddr *addr, socklen_t *length_ptr)
初步:| MT-安全 | AS-安全 |交流安全 fd | See POSIX Safety Concepts.
如 POSIX 安全概念中所述,初步部分列举了以下属性:
根据 POSIX 标准中针对线程安全、异步信号和异步取消安全等安全上下文中规定的标准进行评估。
接下来是对这些概念的解释(另外,请查看"Thread Safety" on wikipedia 了解实现线程安全的不同方法)。根据文档,accept
被声明为 MT-Safe:
MT-Safe 或 Thread-Safe 函数在存在其他线程的情况下可以安全调用。 MT,在 MT-Safe 中,代表多线程。 MT-Safe 并不意味着函数是原子的,也不意味着它使用 POSIX 向用户公开的任何内存同步机制。甚至有可能按顺序调用 MT-Safe 函数不会产生 MT-Safe 组合。例如,让一个线程一个接一个地调用两个 MT-Safe 函数并不能保证等同于原子执行这两个函数的组合的行为,因为其他线程中的并发调用可能会以破坏性方式干扰。 可以跨库接口内联函数的整个程序优化可能会暴露不安全的重新排序,因此不建议跨 GNU C 库接口执行内联。在整个程序优化下,不保证记录的 MT-Safety 状态。但是,在用户可见的标头中定义的函数被设计为内联安全。
glibc
的 accept
实现只是重定向到内核系统调用这一事实,使得这个描述对于 Linux 系统上的其他 libc
实现也很有用(可能只是重定向到系统调用)也)。
另一方面,更通用的方法是检查系统上的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 系统相关标准的描述)。正如在其他答案中已经说过的那样,POSIX.1
标准将 accept
函数指定为 (POSIX-
)线程安全(如 基本定义,第 3.399 节线程安全中所定义)通过不在系统接口,第 2.9.1 节线程安全中列出它。
最后,由于glibc
只是代表内核的accept()
,因此最有信誉的源代码是内核源代码(当然)。 This answer 在accept()
ing 时通过内核代码路径:看看并说服自己共享资源受自旋锁保护,特别是等待应用程序accept
ance 的连接的socket state 和queue .
【讨论】:
以上是关于accept() 线程安全吗?的主要内容,如果未能解决你的问题,请参考以下文章
java priorityblockingqueue 线程安全吗