从线内分叉是否安全?

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

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

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

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

问题是fork()只复制调用线程,并且在子线程中保持的任何互斥体将永远locking在分叉的子节点中。 pthread解决scheme是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。

有你的僵局和它与你自己的互斥或代码无关。

这实际上发生在我曾经从事的一个项目上。 当时我发现的build议是select叉或线,但不是两者。 但对于一些可能不实用的应用程序。

只要你非常小心fork和exec之间的代码,分叉multithreading程序是安全的。 在该范围内,您只能进行重新进入(又名asynchronous安全)系统调用。 理论上,你不允许malloc或free在那里,尽pipe实际上默认的Linux分配器是安全的,而Linux库依靠它最终的结果是你必须使用默认的分配器。

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

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

但是,如果您select使用线程,则可以使用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. 

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

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

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

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

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

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

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

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

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