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

时间:2011-05-21 00:07:59

标签: c++ linux multithreading process fork

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

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

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

8 个答案:

答案 0 :(得分:54)

问题是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处理程序(例如,T1现在保存所有libc锁)
  3. 接下来,在线程T2中,第三方库A获取其自己的互斥锁AM,然后进行需要互斥锁的libc调用。这会阻塞,因为libc互斥锁由T1保存。
  4. 线程T1运行库A的prefork处理程序,它阻止等待获取由T2持有的AM。
  5. 你的僵局与你自己的互斥锁或代码无关。

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

答案 1 :(得分:10)

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

答案 2 :(得分:6)

虽然 可以使用Linux的NPTL pthreads(7)支持您的程序,但在您的fork(2)问题中,您发现线程在Unix系统上是不合适的。 / p>

由于fork(2)是现代系统上的非常便宜的操作,因此当您需要执行更多处理时,最好只对fork(2)进程执行操作。这取决于你打算来回移动多少数据,fork ed进程的无共享理念有利于减少共享数据错误,但确实意味着你need to create pipes to move data between processes或使用共享内存( 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.

答案 3 :(得分:3)

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

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

fork(2)复制进程的整个地址空间,包括所有寄存器,并将该进程置于操作系统调度程序的控制之下;下次调度程序到来时,指令计数器会在下一条指令处获取 - 分叉子进程是父进程的克隆。 (如果你想运行另一个程序,比如因为你正在编写一个shell,你可以使用 exec(2)调用来跟随fork,它会用新程序加载新的地址空间,替换一个被克隆的。)

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

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

答案 4 :(得分:1)

如果您在分叉子进程中快速调用exec或_exit,那么您在实践中就可以了。

您可能希望使用posix_spawn()代替可能正确的东西。

答案 5 :(得分:0)

如果你正在使用unix'fork()'系统调用,那么你在技术上并没有使用线程 - 你正在使用进程 - 它们将拥有自己的内存空间,因此不会相互干扰。

只要每个进程使用不同的文件,就不会有任何问题。

答案 6 :(得分:0)

即使有线程,

fork也是安全的。分叉后,每个进程的线程都是独立的。 (也就是说,线程与分叉正交)。但是,如果不同进程中的线程使用相同的共享内存进行通信,则必须设计同步机制。

答案 7 :(得分:0)

我在线程中fork()的使用经验确实很糟糕。该软件通常很快就会失效。

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

  1. 先叉

    假设您知道开始时需要的外部进程数量,则可以先创建它们,然后让它们坐在那里等待事件(例如,从阻塞管道中读取,等待信号量等)

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

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

  2. 了解您的状态

    在某些情况下,您可能会停止所有线程来启动进程,然后重新启动线程。从某种意义上说,这与点(1)有点类似,尽管您需要一种方法来了解软件中当前正在运行的所有线程(某些情况下,您不想在调用fork()时运行线程)第三方库并不总是可能的。)

    请记住,使用等待来“停止线程”是行不通的。这样的等待需要互斥锁,并且在您调用fork()时需要将其解锁。您只是不知道等待什么时候才能解锁/重新锁定互斥锁。

  3. 选择一个或另一个

    另一种明显的可能性是选择一个或另一个,而不用担心是否要干扰另一个。到目前为止,这是最简单的方法。

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

如其他人所述,如果您使用fork()来调用execve(),则想法是在两次调用之间使用尽可能少的内容。这很可能在99.999%的时间内都有效(很多人也使用system()popen()取得了相当不错的成功,而这些人做类似的事情)。

另一方面,如果像我一样,您想做一个fork()而从不调用execve(),那么在任何线程运行时都不可能正常工作。