从线程内部分叉安全吗?
- 2024-10-21 09:14:00
- admin 原创
- 89
问题描述:
让我解释一下:我已经在 Linux 上开发了一个应用程序,它派生并执行外部二进制文件并等待它完成。结果通过 fork + 进程独有的 shm 文件传达。整个代码都封装在一个类中。
现在,我正在考虑对进程进行线程化,以加快速度。让许多不同的类函数实例分叉并同时执行二进制文件(使用不同的参数),并使用它们自己独特的 shm 文件传达结果。
这个线程安全吗?如果我在线程内分叉,除了安全之外,还有什么需要注意的吗?任何建议或帮助都非常感谢!
解决方案 1:
问题是 fork() 仅复制调用线程,子线程中持有的任何互斥锁都将永远锁定在分叉的子线程中。 pthread 解决方案是pthread_atfork()
处理程序。这个想法是您可以注册 3 个处理程序:一个 prefork、一个父处理程序和一个子处理程序。fork()
发生时,prefork 先于 fork 调用,并有望获得所有应用程序互斥锁。父进程和子进程都必须分别释放父进程和子进程中的所有互斥锁。
但这并不是故事的结束!库调用pthread_atfork
注册特定于库的互斥锁的处理程序,例如 Libc 就是这样做的。这是一件好事:应用程序不可能知道第三方库持有的互斥锁,因此每个库都必须调用pthread_atfork
以确保在发生时清除自己的互斥锁fork()
。
问题在于,pthread_atfork
调用不相关库的处理程序的顺序是不确定的(这取决于程序加载库的顺序)。因此,从技术上讲,这意味着由于竞争条件,prefork 处理程序内部可能会发生死锁。
例如,考虑以下序列:
线程 T1 调用
fork()
libc prefork 处理程序在 T1 中被调用(例如 T1 现在拥有所有 libc 锁)
接下来,在线程 T2 中,第三方库 A 获取其自己的互斥锁 AM,然后进行需要互斥锁的 libc 调用。这会阻塞,因为 libc 互斥锁由 T1 持有。
线程 T1 运行库 A 的 prefork 处理程序,该处理程序阻塞等待获取 T2 持有的 AM。
这是您的死锁,但它与您自己的互斥锁或代码无关。
这实际上发生在我曾经参与的一个项目中。当时我找到的建议是选择 fork 或线程,但不要同时选择两者。但对于某些应用程序来说,这可能不切实际。
解决方案 2:
只要你非常小心 fork 和 exec 之间的代码,在多线程程序中 fork 是安全的。你只能在这段时间内进行可重入(又称异步安全)系统调用。理论上,你不能在那里 malloc 或 free,尽管实际上默认的 Linux 分配器是安全的,并且 Linux 库开始依赖它。最终结果是你必须使用默认分配器。
解决方案 3:
回到时间的起源,我们称线程为“轻量级进程”,因为虽然它们的行为与进程非常相似,但它们并不完全相同。最大的区别在于,线程按定义位于同一个进程的同一地址空间中。这有以下优点:线程之间的切换速度很快,它们本质上共享内存,因此线程间通信速度很快,并且创建和释放线程的速度也很快。
这里的区别在于“重量级进程”,它们是完整的地址空间。一个新的重量级进程由fork(2)创建。随着虚拟内存进入 UNIX 世界,它通过vfork(2)和其他一些方法得到了增强。
fork (2)复制进程的整个地址空间(包括所有寄存器),并将该进程置于操作系统调度程序的控制之下;下次调度程序启动时,指令计数器将从下一条指令开始 - 分叉的子进程是父进程的克隆。(如果您想运行另一个程序,比如说您正在编写 shell,您可以在 fork 之后使用 exec (2)调用,该调用将新程序加载到新地址空间,替换克隆的程序。)
基本上,您的答案就隐藏在这样的解释中:当您有一个具有许多LWP线程的进程并且您分叉该进程时,您将拥有两个具有许多线程的独立进程,同时运行。
这个技巧甚至很有用:在许多程序中,您有一个父进程,该进程可能有许多线程,其中一些线程会派生新的子进程。(例如,HTTP 服务器可能会这样做:每个到端口 80 的连接都由一个线程处理,然后可以派生出类似 CGI 程序的子进程;然后将调用exec(2)来运行 CGI 程序来代替关闭父进程。)
解决方案 4:
虽然您可以为您的程序使用 Linux 的 NPTLpthreads(7)
支持,但正如您在问题中发现的那样,线程在 Unix 系统上并不合适fork(2)
。
由于在现代系统中fork(2)
这是一项非常廉价的fork(2)
操作,因此当您要执行更多处理时,最好只使用进程。这取决于您打算来回移动多少数据, fork
ed 进程的无共享理念有利于减少共享数据错误,但这确实意味着您需要创建管道以在进程之间移动数据或使用共享内存(shmget(2)
或shm_open(3)
)。
但是如果您选择使用线程,您可以创建 fork(2)
一个新进程,并按照手册页中的以下提示进行操作fork(2)
:
* The child process is created with a single thread — the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.
解决方案 5:
只要您快速调用exec()
或_exit()
在分叉的子进程中,实践上就没问题。
您可能想使用posix_spawn()
它来做正确的事情。
解决方案 6:
我在线程内使用的经验fork()
非常糟糕。软件通常很快就会失败。
我已经找到了解决此问题的几种方法,尽管您可能不太喜欢它们,但我认为这些通常是避免接近无法调试的错误的最佳方法。
先分叉
假设您知道开始时所需的外部进程数量,您可以预先创建它们并让它们坐在那里等待事件(即从阻塞管道读取、等待信号量等)。
一旦分叉了足够多的子进程,您就可以自由使用线程并通过管道、信号量等与这些分叉进程进行通信。从创建第一个线程开始,您就不能再调用 fork 了。请记住,如果您使用可能创建线程的第三方库,则必须在fork()
调用发生后使用/初始化这些线程。
请注意,您随后可以开始在主进程和fork()
被编辑进程中使用线程。
了解你的州
在某些情况下,您可以停止所有线程以启动进程,然后重新启动线程。这有点类似于第 (1) 点,因为您不希望在调用时运行线程fork()
,尽管这需要一种方法来了解软件中当前正在运行的所有线程(第三方库并不总是能够做到这一点)。
请记住,使用等待来“停止线程”是行不通的。您必须加入线程,以便它完全退出,因为等待需要互斥锁,而调用时需要解锁这些互斥锁fork()
。您无法知道等待何时会解锁/重新锁定互斥锁,这通常是您陷入困境的地方。
选择其中一个
另一个明显的可能性是选择其中一个,而不必担心是否要干扰其中一个。这是软件中最简单的方法(如果可能的话)。
仅在必要时创建线程
在某些软件中,人们可以在函数中创建一个或多个线程,使用这些线程,然后在退出函数时将它们全部连接起来。这在某种程度上等同于上面的第 (2) 点,只是您可以根据需要(微观)管理线程,而不是创建闲置并在必要时使用的线程。这也可以行得通,但请记住,创建线程是一个昂贵的调用。它必须分配一个具有堆栈和自己的一组寄存器的新任务……这是一个复杂的函数。但是,这让您很容易知道何时有线程在运行,并且除了在这些函数中之外,您可以自由调用fork()
。
在我的编程中,我使用了所有这些解决方案。我使用了 Point (2),因为log4cplus
我需要将 和的线程版本fork()
用于我的软件的某些部分。
正如其他人提到的,如果您使用fork()
to then 调用execve()
,那么想法是在两次调用之间尽可能少地使用。这很可能在 99.999% 的时间内有效(许多人也使用system()
或popen()
取得了相当不错的成功,它们的作用类似)。事实是,如果您没有触及其他线程持有的任何互斥锁,那么这将毫无问题地工作。
另一方面,如果像我一样,您想要执行fork()
但从不调用execve()
,那么它在任何线程运行时都可能无法正常工作。
到底发生了什么?
问题是仅创建当前任务fork()
的单独副本(Linux 下的进程在内核中称为任务)。
每次创建新线程(pthread_create()
)时,您还会创建一个新任务,但在同一进程内(即新任务共享进程空间:内存、文件描述符、所有权等)。但是,fork()
在复制当前正在运行的任务时,会忽略这些额外任务。
+-----------------------------------------------+
| Process A |
| |
| +----------+ +----------+ +----------+ |
| | thread 1 | | thread 2 | | thread 3 | |
| +----------+ +----+-----+ +----------+ |
| | |
+----------------------|------------------------+
| fork()
|
+----------------------|------------------------+
| v Process B |
| +----------+ |
| | thread 1 | |
| +----------+ |
| |
+-----------------------------------------------+
因此,在进程 B 中,我们丢失了进程 A 中的线程 1 和线程 3。这意味着,如果其中一个或两个进程都锁定了互斥锁或类似的东西,则进程 B 将很快锁定。锁定是最糟糕的,但fork()
发生这种情况时任一线程仍拥有的任何资源都会丢失(套接字连接、内存分配、设备句柄等)。这就是上述第 (2) 点的作用所在。您需要在之前了解您的状态fork()
。如果您在一个地方定义了非常少量的线程或工作线程,并且可以轻松停止所有线程,那么这将很容易。
解决方案 7:
如果您使用unix“fork()”系统调用,那么从技术上讲您就没有使用线程,而是使用进程,它们有自己的内存空间,因此不会互相干扰。
只要每个进程使用不同的文件,就不会有任何问题。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件