从一个线程中分叉是否安全?

Æle*_*lex 41 c++ linux multithreading fork process

让我解释一下:我已经在Linux上开发了一个应用程序,它分叉并执行外部二进制文件并等待它完成.结果由fork +进程独有的shm文件传递.整个代码封装在一个类中.

现在我正在考虑线程化这个过程以加快速度.拥有许多不同的类函数实例,并行地(使用不同的参数)分叉和执行二进制文件,并使用自己独特的shm文件传递结果.

这个线程安全吗?如果我在一个线程中分叉,除了安全之外,还有什么我需要注意的吗?任何建议或帮助非常感谢!

Kev*_*vin 56

问题是fork()只复制调用线程,子线程中保存的任何互斥锁将永远锁定在分叉子节点中.pthread解决方案是pthread_atfork()处理程序.这个想法是你可以注册3个处理程序:一个prefork,一个父处理程序和一个子处理程序.如果fork()发生prefork在fork之前被调用,并且有望获得所有应用程序互斥量.父级和子级都必须分别释放父进程和子进程中的所有互斥锁.

这不是故事的结局!库调用pthread_atfork为库特定的互斥锁注册处理程序,例如Libc执行此操作.这是一件好事:应用程序不可能知道第三方库pthread_atfork所拥有的互斥锁,因此每个库必须调用以确保在发生的情况下清除它自己的互斥锁fork().

问题是pthread_atfork为不相关的库调用处理程序的顺序是未定义的(它取决于程序加载库的顺序).所以这意味着技术上由于竞争条件,在prefork处理程序内部会发生死锁.

例如,请考虑以下顺序:

  1. 线程T1调用 fork()
  2. 在T1中获得的libc的prefork处理程序
  3. 接下来,在线程T2中,第三方库A获取其自己的互斥锁AM,然后进行需要互斥锁的libc调用.这会阻塞,因为libc互斥锁由T1保存.
  4. 线程T1运行库A的prefork处理程序,它阻止等待获取由T2持有的AM.

你的死锁与你自己的互斥锁或代码无关.

这实际上发生在我曾经做过的一个项目上.我当时发现的建议是选择fork或thread而不是两者.但对于某些可能不实用的应用程序.


Igo*_*nko 10

只要你非常小心fork和exec之间的代码,在多线程程序中分叉是安全的.您只能在该范围内进行重新进入(也称为异步安全)系统调用.理论上,你不允许malloc或free在那里,虽然在实践中默认的Linux分配器是安全的,并且Linux库依赖于它.最终结果是你必须使用默认的分配器.


sar*_*old 6

虽然您可以pthreads(7)对您的程序使用Linux的NPTL 支持,但线程在Unix系统上是一个尴尬的方法,正如您在fork(2)问题中发现的那样.

由于fork(2)是一个非常便宜的现代系统操作,你可能会做的更好,只是fork(2)当你有更多的处理来执行的过程.这取决于你打算来回移动多少数据,forked进程的无共享理念有利于减少共享数据错误,但这意味着您需要创建管道以在进程之间移动数据或使用共享内存(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.
Run Code Online (Sandbox Code Playgroud)


Cha*_*tin 6

回到时间的黎明,我们称线程为“轻量级进程”,因为虽然它们的行为很像进程,但它们并不完全相同。最大的区别是线程根据定义位于一个进程的同一地址空间中。这样做的优点是:从线程到线程的切换速度很快,它们固有地共享内存,因此线程间通信速度很快,并且线程的创建和处理速度也很快。

这里的区别在于“重量级进程”,它们是完整的地址空间。fork(2)创建了一个新的重量级进程。随着虚拟内存进入 UNIX 世界,它被vfork(2)和其他一些扩展。

叉(2)复制处理的整个地址空间,包括所有的寄存器,并提出该操作系统调度程序的控制下处理; 下一次调度程序出现时,指令计数器会在下一条指令处启动——分叉的子进程是父进程的克隆。(如果你想运行另一个程序,比如说因为你正在编写一个 shell,你可以通过exec(2)调用跟随 fork ,它用一个新程序加载新的地址空间,替换被克隆的那个。)

基本上,您的答案隐藏在该解释中:当您有一个具有许多LWP线程的进程并分叉该进程时,您将有两个具有许多线程的独立进程并发运行。

这个技巧甚至很有用:在许多程序中,您的父进程可能有许多线程,其中一些线程会派生出新的子进程。(例如,HTTP 服务器可能会这样做:到端口 80 的每个连接都由一个线程处理,然后可以分叉 CGI 程序之类的子进程;然后将调用exec(2)来运行 CGI 程序代替父进程关闭。)

  • @Alex,复制实际上是*按需*完成的-大多数页面在父级和子级之间共享并标记为*copy-on-write*。 (5认同)
  • @Charlie,您的陈述“您将有两个具有多个线程的独立进程,并发运行”是模棱两可的或不正确的。`fork()` 的 POSIX 指定行为是只有调用线程在子进程中处于非挂起状态。但是,某些平台(例如 Solaris)实现了 `forkall()`。 (5认同)
  • 这句话是“两个独立的进程......同时运行”。这是正确的。“具有许多线程”是一个从句,指的是进程所拥有的内容,而不是“同时运行”。“这只是模棱两可,因为他们不再教你们小孩子如何解析英语句子了,”他暴躁地说。 (2认同)
  • CMIIAW,父线程保留线程,子线程只有一个线程。还是只在Linux中? (2认同)
  • 不仅是 Linux — [POSIX](http://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html) 表示子进程是一个单线程进程。[在 Linux 上,`fork()` 函数实际上使用 `clone` 系统调用,但是以 `fork` 等效的方式。] (2认同)

Mar*_*rkR 5

只要您快速调用exec()_exit()进入分叉子进程,那么在实践中就可以了。

您可能想使用它posix_spawn()来代替它,这可能会做正确的事情。


Ale*_*lke 5

fork()在线程中的体验非常糟糕。该软件通常很快就会失败。

我已经找到了解决这个问题的几种解决方案,尽管您可能不太喜欢它们,但我认为这些通常是避免接近不可调试错误的最佳方法。

  1. 先叉

    假设您一开始就知道需要的外部进程的数量,您可以预先创建它们并让它们坐在那里等待事件(即从阻塞管道读取,等待信号量等)

    一旦你分叉了足够多的子进程,你就可以自由地使用线程并通过管道、信号量等与那些分叉的进程进行通信。从你创建第一个线程开始,你就不能再调用 fork 了。请记住,如果您使用可能创建线程的第三方库,则必须在调用fork()发生后使用/初始化这些线程。

    请注意,然后您可以开始在 main 和fork()'ed 进程中使用线程。

  2. 了解您的状态

    在某些情况下,您可能会停止所有线程来启动进程,然后重新启动线程。这有点类似于第 (1) 点,因为您不希望线程在调用时运行fork(),尽管它需要一种方法让您了解当前在软件中运行的所有线程(这对于第三种方法并不总是可能的)党图书馆)。

    请记住,使用等待“停止线程”是行不通的。您必须加入线程,以便它完全退出,因为等待需要互斥锁,并且在您调用 时需要解锁它们fork()。您只是不知道等待何时会解锁/重新锁定互斥体,这通常是您陷入困境的地方。

  3. 选择其中之一

    另一种明显的可能性是选择其中之一,而不用担心是否会干扰其中之一。如果您的软件可能的话,这是迄今为止最简单的方法。

  4. 仅在必要时创建线程

    在某些软件中,人们在函数中创建一个或多个线程,使用所述线程,然后在退出函数时连接所有线程。这在某种程度上相当于上面的第 (2) 点,只是您根据需要(微观)管理线程,而不是创建闲置并在必要时使用的线程。这也可以工作,只要记住创建线程是一个代价高昂的调用。它必须使用堆栈和自己的一组寄存器分配一个新任务......这是一个复杂的函数。然而,这使得您可以轻松地知道何时有线程正在运行,并且除了在这些函数内之外,您可以自由地调用fork().

在我的编程中,我使用了所有这些解决方案。我使用点 (2) 是因为我的软件的某些部分log4cplus需要使用 和 的线程版本。fork()

正如其他人所提到的,如果您使用fork()to then 调用execve(),那么我们的想法是在两个调用之间尽可能少地使用。这可能在 99.999% 的情况下有效(许多人也使用system()popen()取得了相当好的成功,并且这些人做了类似的事情)。事实是,如果您没有命中其他线程持有的任何互斥体,那么这将毫无问题地工作。

另一方面,如果像我一样,您想做 afork()并且从不 call execve(),那么在任何线程运行时它都不太可能正常工作。


到底发生了什么?

问题是fork()仅创建当前任务的单独副本(Linux 下的进程在内核中称为任务)。

每次创建新线程 ( pthread_create()) 时,也会创建一个新任务,但在同一进程内(即新任务共享进程空间:内存、文件描述符、所有权等)。但是,fork()在复制当前正在运行的任务时,a 会忽略这些额外的任务。

+-----------------------------------------------+
|                                     Process A |
|                                               |
| +----------+    +----------+    +----------+  |
| | thread 1 |    | thread 2 |    | thread 3 |  |
| +----------+    +----+-----+    +----------+  |
|                      |                        |
+----------------------|------------------------+
                       | fork()
                       |
+----------------------|------------------------+
|                      v              Process B |
|               +----------+                    |
|               | thread 1 |                    |
|               +----------+                    |
|                                               |
+-----------------------------------------------+
Run Code Online (Sandbox Code Playgroud)

因此,在进程 B 中,我们失去了进程 A 的线程 1 和线程 3。这意味着,如果其中一个或两个都拥有互斥锁或类似的锁,那么进程 B 将很快锁定。锁是最糟糕的,但是在发生这种情况时,任何一个线程仍然拥有的任何资源fork()都会丢失(套接字连接、内存分配、设备句柄等),这就是上面第 (2) 点的用武之地。您需要知道您的之前的状态fork()。如果您在一个地方定义了非常少量的线程或工作线程并且可以轻松地停止所有它们,那么这将很容易。