从线程内分叉是否安全?

发布于 2024-11-08 23:24:05 字数 244 浏览 0 评论 0原文

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

现在我正在考虑对进程进行线程化以加快速度。让类函数的许多不同实例同时分叉和执行二进制文件(使用不同的参数),并使用它们自己独特的 shm 文件传达结果。

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

Let me explain: I have already been developing an application on Linux which forks and execs an external binary and waits for it to finish. Results are communicated by shm files that are unique to the fork + process. The entire code is encapsulated within a class.

Now I am considering threading the process in order to speed things up. Having many different instances of class functions fork and execute the binary concurrently (with different parameters) and communicate results with their own unique shm files.

Is this thread safe? If I fork within a thread, apart from being safe, is there something I have to watch for? Any advice or help is much appreciated!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

梦罢 2024-11-15 23:24:05

问题是 fork() 只复制调用线程,子线程中持有的任何互斥体将永远锁定在分叉的子线程中。 pthread 解决方案是 pthread_atfork() 处理程序。这个想法是您可以注册 3 个处理程序:一个预分叉、一个父处理程序和一个子处理程序。当 fork() 发生时,prefork 在 fork 之前被调用,并期望获得所有应用程序互斥体。父进程和子进程都必须分别释放父进程和子进程中的所有互斥体。

但这还不是故事的结局!库调用 pthread_atfork 来注册库特定互斥体的处理程序,例如 Libc 就是这样做的。这是一件好事:应用程序不可能知道第 3 方库所持有的互斥锁,因此每个库必须调用 pthread_atfork 以确保在发生 事件时清除其自己的互斥锁。 >fork()

问题在于,为不相关的库调用 pthread_atfork 处理程序的顺序是未定义的(它取决于程序加载库的顺序)。因此,这意味着从技术上讲,由于竞争条件,预分叉处理程序内部可能会发生死锁。

例如,考虑以下序列:

  1. 线程 T1 调用 fork()
  2. libc prefork 处理程序在 T1 中调用(例如,T1 现在持有所有 libc 锁)
  3. 接下来,在线程 T2 中,第 3 方库 A 获取其自己的锁mutex AM,然后进行需要互斥锁的 libc 调用。这会阻塞,因为 libc 互斥锁由 T1 持有。
  4. 线程 T1 运行库 A 的预分叉处理程序,该处理程序会阻塞等待获取由 T2 持有的 AM。

这是你的死锁,它与你自己的互斥体或代码无关。

这实际上发生在我曾经参与过的一个项目上。我当时发现的建议是选择分叉或线程,但不能同时选择两者。但对于某些应用程序来说这可能不切实际。

The problem is that fork() only copies the calling thread, and any mutexes held in child threads will be forever locked in the forked child. The pthread solution was the pthread_atfork() handlers. The idea was you can register 3 handlers: one prefork, one parent handler, and one child handler. When fork() happens prefork is called prior to fork and is expected to obtain all application mutexes. Both parent and child must release all mutexes in parent and child processes respectively.

This isn't the end of the story though! Libraries call pthread_atfork to register handlers for library specific mutexes, for example Libc does this. This is a good thing: the application can't possibly know about the mutexes held by 3rd party libraries, so each library must call pthread_atfork to ensure it's own mutexes are cleaned up in the event of a fork().

The problem is that the order that pthread_atfork handlers are called for unrelated libraries is undefined (it depends on the order that the libraries are loaded by the program). So this means that technically a deadlock can happen inside of a prefork handler because of a race condition.

For example, consider this sequence:

  1. Thread T1 calls fork()
  2. libc prefork handlers are called in T1 (e.g. T1 now holds all libc locks)
  3. Next, in Thread T2, a 3rd party library A acquires its own mutex AM, and then makes a libc call which requires a mutex. This blocks, because libc mutexes are held by T1.
  4. Thread T1 runs prefork handler for library A, which blocks waiting to obtain AM, which is held by T2.

There's your deadlock and its unrelated to your own mutexes or code.

This actually happened on a project I once worked on. The advice I had found at that time was to choose fork or threads but not both. But for some applications that's probably not practical.

听不够的曲调 2024-11-15 23:24:05

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

It's safe to fork in a multithreaded program as long as you are very careful about the code between fork and exec. You can make only re-enterant (aka asynchronous-safe) system calls in that span. In theory, you are not allowed to malloc or free there, although in practice the default Linux allocator is safe, and Linux libraries came to rely on it End result is that you must use the default allocator.

贪了杯 2024-11-15 23:24:05

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

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

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

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

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

Back at the Dawn of Time, we called threads "lightweight processes" because while they act a lot like processes, they're not identical. The biggest distinction is that threads by definition live in the same address space of one process. This has advantages: switching from thread to thread is fast, they inherently share memory so inter-thread communications are fast, and creating and disposing of threads is fast.

The distinction here is with "heavyweight processes", which are complete address spaces. A new heavyweight process is created by fork(2). As virtual memory came into the UNIX world, that was augmented with vfork(2) and some others.

A fork(2) copies the entire address space of the process, including all the registers, and puts that process under the control of the operating system scheduler; the next time the scheduler comes around, the instruction counter picks up at the next instruction -- the forked child process is a clone of the parent. (If you want to run another program, say because you're writing a shell, you follow the fork with an exec(2) call, which loads that new address space with a new program, replacing the one that was cloned.)

Basically, your answer is buried in that explanation: when you have a process with many LWPs threads and you fork the process, you will have two independent processes with many threads, running concurrently.

This trick is even useful: in many programs, you have a parent process that may have many threads, some of which fork new child processes. (For example, an HTTP server might do that: each connection to port 80 is handled by a thread, and then a child process for something like a CGI program could be forked; exec(2) would then be called to run the CGI program in place of the parent process close.)

篱下浅笙歌 2024-11-15 23:24:05

虽然您可以为您的程序使用 Linux 的 NPTL pthreads(7) 支持,但线程在 Unix 系统上不太合适,正如您在 fork( 2)问题。

由于在现代系统上 fork(2) 是一个非常便宜的操作,因此当您有以下情况时,最好只fork(2)您的进程:执行更多处理。这取决于您打算来回移动多少数据,分叉进程的无共享理念有利于减少共享数据错误,但也意味着您要么需要创建管道在进程之间移动数据或使用共享内存(shmget(2)shm_open( 3))。

但是,如果您选择使用线程,则可以使用 fork(2) 联机帮助页中的以下提示 fork(2) 一个新进程:

 * 子进程是用单个线程创建的 -
      一个调用 fork() 的方法。整个虚拟地址空间
      父项的内容会复制到子项中,包括
      互斥锁、条件变量和其他 pthread 的状态
      物体;使用 pthread_atfork(3) 可能会有所帮助
      处理这可能导致的问题。

While you can use Linux's NPTL pthreads(7) support for your program, threads are an awkward fit on Unix systems, as you've discovered with your fork(2) question.

Since fork(2) is a very cheap operation on modern systems, you might do better to just fork(2) your process when you have more handling to perform. It depends upon how much data you intend to move back and forth, the share-nothing philosophy of forked processes is good for reducing shared-data bugs but does mean you either need to create pipes to move data between processes or use shared memory (shmget(2) or shm_open(3)).

But if you choose to use threading, you can fork(2) a new process, with the following hints from the fork(2) manpage:

   *  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.
纸伞微斜 2024-11-15 23:24:05

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

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

Provided you quickly either call exec() or _exit() in the forked child process, you're ok in practice.

You might want to use posix_spawn() instead which will probably do the Right Thing.

所谓喜欢 2024-11-15 23:24:05

我在线程内 fork() 的经历非常糟糕。该软件通常很快就会失败。

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

  1. 先分叉

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

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

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

  2. 了解您的状态

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

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

  3. 选择其中之一

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

  4. 仅在必要时创建线程

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

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

正如其他人所提到的,如果您使用 fork() 然后调用 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 中,我们丢失了线程 1 和线程 2。来自进程 A 的线程 3。这意味着如果其中一个或两个都拥有互斥锁或类似的锁,那么进程 B 将很快锁定。锁是最糟糕的,但是在 fork() 发生时任一线程仍然拥有的任何资源都会丢失(套接字连接、内存分配、设备句柄等),这就是点 (2 )上面出现。您需要在 fork() 之前知道您的状态。如果您在一个地方定义了非常少量的线程或工作线程并且可以轻松地停止所有它们,那么这将很容易。

My experience of fork()'ing within threads is really bad. The software generally fails pretty quickly.

I've found several solutions to the matter, although you may not like them much, I think these are generally the best way to avoid close to undebuggable errors.

  1. Fork first

    Assuming you know the number of external processes you need at the start, you can create them upfront and just have them sit there waiting for an event (i.e. read from a blocking pipe, wait on a semaphore, etc.)

    Once you forked enough children you are free to use threads and communicate with those forked processes via your pipes, semaphores, etc. From the time you create a first thread, you cannot call fork anymore. Keep in mind that if you're using 3rd party libraries which may create threads, those have to be used/initialized after the fork() calls happened.

    Note that you can then start using threads within the main and fork()'ed processes.

  2. Know your state

    In some circumstances, it may be possible for you to stop all of your threads to start a process and then restart your threads. This is somewhat similar to point (1) in the sense that you do not want threads running at the time you call fork(), although it requires a way for you to know about all the threads currently running in your software (something not always possible with 3rd party libraries).

    Remember that "stopping a thread" using a wait is not going to work. You have to join with the thread so it is fully exited, because a wait require a mutex and those need to be unlocked when you call fork(). You just cannot know when the wait is going to unlock/re-lock the mutex and that's usually where you get stuck.

  3. Choose one or the other

    The other obvious possibility is to choose one or the other and not bother with whether you're going to interfere with one or the other. This is by far the simplest method if at all possible in your software.

  4. Create Threads only when Necessary

    In some software, one creates one or more threads in a function, use said threads, then joins all of them when exiting the function. This is somewhat equivalent to point (2) above, only you (micro-)manage threads as required instead of creating threads that sit around and get used when necessary. This will work too, just keep in mind that creating a thread is a costly call. It has to allocate a new task with a stack and its own set of registers... it is a complex function. However, this makes it easy to know when you have threads running and except from within those functions, you are free to call fork().

In my programming, I used all of these solutions. I used Point (2) because the threaded version of log4cplus and I needed to use fork() for some parts of my software.

As mentioned by others, if you are using a fork() to then call execve() then the idea is to use as little as possible between the two calls. That is likely to work 99.999% of the time (many people use system() or popen() with fairly good successes too and these do similar things). The fact is that if you do not hit any of the mutexes held by the other threads, then this will work without issue.

On the other hand, if, like me, you want to do a fork() and never call execve(), then it's not likely to work right while any thread is running.


What is actually happening?

The issue is that fork() create a separate copy of only the current task (a process under Linux is called a task in the kernel).

Each time you create a new thread (pthread_create()), you also create a new task, but within the same process (i.e. the new task shares the process space: memory, file descriptors, ownership, etc.). However, a fork() ignores those extra tasks when duplicating the currently running task.

+-----------------------------------------------+
|                                     Process A |
|                                               |
| +----------+    +----------+    +----------+  |
| | thread 1 |    | thread 2 |    | thread 3 |  |
| +----------+    +----+-----+    +----------+  |
|                      |                        |
+----------------------|------------------------+
                       | fork()
                       |
+----------------------|------------------------+
|                      v              Process B |
|               +----------+                    |
|               | thread 1 |                    |
|               +----------+                    |
|                                               |
+-----------------------------------------------+

So in Process B, we lose thread 1 & thread 3 from Process A. This means that if either or both have a lock on mutexes or something similar, then Process B is going to lock up quickly. The locks are the worst, but any resources that either thread still has at the time the fork() happens are lost (socket connection, memory allocations, device handle, etc.) This is where point (2) above comes in. You need to know your state before the fork(). If you have a very small number of threads or worker threads defined in one place and can easily stop all of them, then it will be easy enough.

想你的星星会说话 2024-11-15 23:24:05

如果您使用unix“fork()”系统调用,那么从技术上讲您并没有使用线程——您使用的是进程——它们将拥有自己的内存空间,因此不能互相干扰。

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

If you are using the unix 'fork()' system call, then you are not technically using threads- you are using processes- they will have their own memory space, and therefore cannot interfere with eachother.

As long as each process uses different files, there should not be any issue.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文