从线内分叉是否安全?

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

现在我正在考虑线程化,以加快速度。 具有许多不同的类函数实例,并发地(使用不同的参数)分离和执行二进制文件,并与自己的独特的shm文件交stream结果。

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

Solutions Collecting From Web of "从线内分叉是否安全?"

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

问题是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和exec之间的代码,分叉多线程程序是安全的。 在该范围内,您只能进行重新进入(又名异步安全)系统调用。 理论上,你不允许malloc或free在那里,尽管实际上默认的Linux分配器是安全的,而Linux库依赖它。最终的结果是你必须使用默认的分配器。

虽然您可以使用Linux的NPTL pthreads(7)支持您的程序,但在fork(2)系统中,线程在Unix系统上是一个尴尬的问题。

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

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

这里的区别是“重量级进程”,这是完整的地址空间。 fork(2)创建了一个新的重量级过程。 随着虚拟内存进入UNIX世界, vfork(2)等增强了虚拟内存。

fork(2)复制整个进程的地址空间,包括所有的寄存器,并将这个进程置于操作系统调度程序的控制之下; 下一次调度程序出现时,指令计数器会在下一条指令处拾取 – 分叉的子进程是父进程的克隆。 (如果你想运行另一个程序,比如说你正在编写一个shell,那么你需要用exec(2)调用来跟踪fork,然后用新程序加载新的地址空间,替换被克隆的地址空间。

基本上,你的答案是埋在这个解释:当你有一个LWP线程的许多进程,你叉进程,你将有两个独立的进程与许多线程同时运行。

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

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

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

如果你在分叉的子进程中快速地调用exec或_exit,你可以在实践中使用。

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