ChatGPT解决这个技术问题 Extra ChatGPT

Linux中的线程与进程[关闭]

关闭。这个问题是基于意见的。它目前不接受答案。想改进这个问题?更新问题,以便可以通过编辑这篇文章用事实和引用来回答它。 5个月前关闭。社区在 5 个月前审查了是否重新打开此问题并将其关闭:原始关闭原因未解决 改进此问题

我最近听到一些人说,在 Linux 中,使用进程而不是线程几乎总是更好,因为 Linux 在处理进程方面非常高效,并且因为与线程相关的问题(例如锁定)非常多。但是,我很怀疑,因为在某些情况下,线程似乎可以带来相当大的性能提升。

所以我的问题是,当面对线程和进程都可以很好地处理的情况时,我应该使用进程还是线程?例如,如果我正在编写 Web 服务器,我应该使用进程还是线程(或组合)?

与 Linux 2.4 有区别吗?
Linux 2.4 下的进程和线程之间的区别在于,线程比进程共享更多的状态部分(地址空间、文件句柄等),而进程通常不共享。 Linux 2.6 下的 NPTL 赋予它们“线程组”,这有点像 win32 和 Solaris 中的“进程”,从而使这一点更加清晰。
并发编程很困难。除非您需要非常高的性能,否则您权衡中最重要的方面通常是调试的难度。在这方面,流程使解决方案更容易,因为所有通信都是明确的(易于检查、记录等)。相比之下,线程的共享内存会产生无数个地方,一个线程可能会错误地影响另一个线程。
@LutzPrechelt - 并发编程可以是多线程的,也可以是多进程的。我不明白你为什么假设并发编程只是多线程的。这可能是由于某些特定的语言限制,但通常两者兼而有之。
我链接 Lutz 只是表示,无论选择哪个 - 进程或线程 - 并发编程都很困难 - 但在许多情况下,使用进程的并发编程使得调试更容易。

R
Ryan Emerle

Linux 使用 1-1 线程模型,(对内核而言)进程和线程之间没有区别——一切都只是一个可运行的任务。 *

在 Linux 上,系统调用 clone 克隆一个任务,具有可配置的共享级别,其中包括:

CLONE_FILES:共享同一个文件描述符表(而不是创建副本)

CLONE_PARENT:不要在新任务和旧任务之间建立父子关系(否则,孩子的getppid() = 父母的getpid())

CLONE_VM:共享相同的内存空间(而不是创建COW副本)

fork() 称为clone(最少分享)pthread_create() 称为clone(最多分享)。 **

fork由于复制表和为内存创建 COW 映射,其成本比pthread_create高一点,但 Linux 内核开发人员已尝试(并成功)将这些成本降至最低。

在任务之间切换,如果它们共享相同的内存空间和不同的表,将比不共享它们便宜一点,因为数据可能已经加载到缓存中。然而,即使没有共享任何内容,切换任务仍然非常快——这是 Linux 内核开发人员试图确保(并成功确保)的另一件事。

事实上,如果你在一个多处理器系统上,不共享实际上可能对性能有好处:如果每个任务都在不同的处理器上运行,同步共享内存的成本很高。

* 简化。 CLONE_THREAD 导致信号传递被共享(需要 CLONE_SIGHAND,它共享信号处理程序表)。

** 简化。 SYS_forkSYS_clone 系统调用都存在,但在内核中,sys_forksys_clone 都是围绕同一 do_fork 函数的非常薄的包装器,它本身是围绕 copy_process 的薄包装器。是的,术语 processthreadtask 在 Linux 内核中可以互换使用...


我认为我们少了 1 分。如果您为 Web 服务器创建多个进程,那么您必须编写另一个进程来打开套接字并将“工作”传递给不同的线程。 Threading 提供单进程多线程,简洁的设计。在许多情况下,线程是自然而然的,而在其他情况下,新进程是自然而然的。当问题落入灰色区域时,由 ehemient 解释的其他权衡变得很重要。
@Saurabh 不是真的。您可以轻松地 socketbindlistenfork,然后在同一个侦听套接字上拥有多个进程 accept 连接。如果一个进程很忙,它可以停止接受,并且内核会将传入的连接路由到另一个进程(如果没有人在监听,内核将排队或丢弃,具体取决于 listen 积压)。您对工作分配的控制不多,但通常这已经足够了!
@Bloodcount Linux 上的所有进程/线程都是由相同的机制创建的,该机制克隆了现有的进程/线程。传递给 clone() 的标志确定共享哪些资源。任务还可以在以后的任何时间点unshare() 资源。
@KarthikBalaguru 在内核本身中,每个任务都有一个 task_struct。这通常在整个内核代码中称为“进程”,但它对应于每个可运行线程。没有process_struct;如果一堆 task_struct 通过它们的 thread_group 列表链接在一起,那么它们对于用户空间来说是同一个“进程”。对“线程”有一些特殊处理,例如,所有同级线程都在 fork 和 exec 上停止,只有“主”线程出现在 ls /proc 中。但是,无论是否在 /proc 中列出,每个线程都可以通过 /proc/pid 访问。
@KarthikBalaguru 内核支持线程和进程之间的连续行为;例如,clone(CLONE_THREAD | CLONE_VM | CLONE_SIGHAND)) 将为您提供一个不共享工作目录、文件或锁的新“线程”,而 clone(CLONE_FILES | CLONE_FS | CLONE_IO) 将为您提供一个共享的“进程”。底层系统通过克隆创建任务; fork()pthread_create() 只是以不同方式调用 clone() 的库函数(正如我在此答案中所写的那样)。
M
MarkR

Linux(实际上是 Unix)为您提供了第三种选择。

选项 1 - 流程

创建一个独立的可执行文件来处理应用程序的某些部分(或所有部分),并为每个进程单独调用它,例如,程序运行自身的副本以委派任务。

选项 2 - 线程

创建一个独立的可执行文件,它以单个线程启动并创建额外的线程来执行某些任务

选项 3 - 分叉

仅在 Linux/Unix 下可用,这有点不同。分叉的进程实际上是它自己的进程,有自己的地址空间——子进程(通常)不能做任何事情来影响其父或兄弟的地址空间(不像线程)——所以你得到了额外的健壮性。

但是,内存页面不会被复制,它们是写时复制的,因此通常使用的内存比您想象的要少。

考虑一个包含两个步骤的 Web 服务器程序:

读取配置和运行时数据服务页面请求

如果您使用线程,则第 1 步将执行一次,第 2 步将在多个线程中完成。如果您使用“传统”流程,则需要为每个流程重复步骤 1 和 2,并且需要复制用于存储配置和运行时数据的内存。如果您使用了 fork(),那么您可以执行第 1 步一次,然后执行 fork(),将运行时数据和配置保留在内存中,不受影响,不复制。

所以真的有三个选择。


@Qwertie 分叉并不是那么酷,它以微妙的方式破坏了许多库(如果您在父进程中使用它们)。它会产生意想不到的行为,即使是有经验的程序员也会感到困惑。
@MarkR您能否提供一些示例或链接,说明分叉如何破坏库并产生意外行为?
如果一个进程使用打开的 mysql 连接进行分叉,则会发生坏事,因为套接字在两个进程之间共享。即使只有一个进程使用该连接,另一个进程也会阻止它被关闭。
fork() 系统调用由 POSIX 指定(这意味着它在任何 Unix 系统上都可用),如果您使用底层 Linux API,即 clone() 系统调用,那么您实际上在 Linux 中拥有的选择不止这三个.
@MarkR套接字的共享是设计使然。此外,任何一个进程都可以在对套接字调用 close() 之前使用 linux.die.net/man/2/shutdown 关闭套接字。
A
Adam Rosenfield

这取决于很多因素。进程比线程更重,启动和关闭成本更高。进程间通信(IPC)也比线程间通信更难更慢。

相反,进程比线程更安全,因为每个进程都在自己的虚拟地址空间中运行。如果一个进程崩溃或缓冲区溢出,它根本不会影响任何其他进程,而如果一个线程崩溃,它会关闭进程中的所有其他线程,如果一个线程有缓冲区溢出,它就会打开所有线程中的安全漏洞。

因此,如果您的应用程序的模块可以大部分独立运行而几乎没有通信,那么如果您能够承担启动和关闭成本,您可能应该使用进程。 IPC 对性能的影响将是最小的,并且您对错误和安全漏洞会稍微安全一些。如果您需要获得或拥有大量共享数据(例如复杂的数据结构)的所有性能,请使用线程。


亚当的回答很适合作为执行简报。对于更多细节,MarkR 和 ehemient 提供了很好的解释。可以在 cs.cf.ac.uk/Dave/C/node29.html 中找到带有示例的非常详细的解释,但它的部分内容似乎有些过时。
CyberFonic 适用于 Windows。正如 ehemient 所说,Linux 下的进程并不重。在 Linux 下,所有可用于线程之间通信的机制(futex、共享内存、管道、IPC)也可用于进程并以相同的速度运行。
IPC 更难使用,但如果有人使用“共享内存”怎么办?
d
dmckee --- ex-moderator kitten

其他人已经讨论了这些考虑因素。

也许重要的区别在于,与线程相比,Windows 中的进程繁重且昂贵,而在 Linux 中,差异要小得多,因此等式在不同的点上取得平衡。


r
robert.berger

曾几何时,有 Unix,在这个古老的 Unix 中,进程有很多开销,所以一些聪明的人所做的就是创建线程,这些线程将与父进程共享相同的地址空间,他们只需要减少上下文switch,这将使上下文切换更有效。

在当代 Linux (2.6.x) 中,与线程相比,进程的上下文切换在性能上没有太大差异(只有 MMU 的东西是线程的附加内容)。共享地址空间存在问题,这意味着线程中的错误指针可能会破坏父进程或同一地址空间中另一个线程的内存。

进程受 MMU 保护,因此错误的指针只会导致信号 11 而不会损坏。

我通常会使用进程(在 Linux 中没有太多的上下文切换开销,但由于 MMU 的内存保护),但是如果我需要一个实时调度程序类,则使用 pthreads,这完全是另一杯茶。

为什么你认为线程在 Linux 上会有如此大的性能提升?你有这方面的任何数据,还是只是一个神话?


是的,我确实有一些数据。我运行了一个创建 100,000 个进程的测试和一个创建 100,000 个线程的测试。线程版本的运行速度提高了大约 9 倍(进程为 17.38 秒,线程为 1.93 秒)。现在这仅测试创建时间,但对于短期任务,创建时间可能是关键。
@ user17918 - 您是否可以共享您用于计算上述时间的代码..
一个很大的不同,内核为每个进程创建页表,而theads只使用一个页表,所以我认为线程比进程快是正常的
另一个简单的看待它的方法是 TCB 比 PCB 小得多,因此很明显涉及 PCB 的进程上下文切换将比线程切换消耗更多时间。
M
Maggyero

如果您想创建一个尽可能纯的进程,您将使用 clone() 并设置所有克隆标志。 (或者省去打字的麻烦,然后调用 fork()

如果您想尽可能地创建一个纯线程,您可以使用 clone() 并清除所有克隆标志(或者省去打字工作并调用 pthread_create()

有 28 个标志指示资源共享的级别。这意味着您可以创建超过 2.68 亿种任务,具体取决于您要共享的内容。

当我们说 Linux 不区分进程和线程,而是将程序中的任何控制流作为任务来暗示时,这就是我们的意思。不区分两者的理由是,嗯,不是唯一定义超过 2.68 亿种口味!

因此,做出是否使用进程或线程的“完美决定”实际上就是决定克隆 28 个资源中的哪一个。

https://i.stack.imgur.com/PwTDC.png


R
Robert Deml

你的任务有多紧密耦合?

如果他们可以彼此独立生活,那么使用流程。如果它们相互依赖,则使用线程。这样你就可以杀死并重新启动一个错误的进程,而不会干扰其他任务的运行。


g
grepit

我认为每个人都在回答您的问题方面做得很好。我只是在 Linux 中添加有关线程与进程的更多信息,以澄清和总结内核上下文中的一些先前响应。所以,我的回应是关于 Linux 中的内核特定代码。根据 Linux Kernel 文档,线程与进程之间没有明显的区别,只是线程使用共享虚拟地址空间而不是进程。另请注意,Linux 内核使用术语“任务”来指代进程和线程。

“没有实现进程或线程的内部结构,而是有一个结构 task_struct 描述了一个称为任务的抽象调度单元”

此外,根据 Linus Torvalds 的说法,您根本不应该考虑进程与线程,因为它太受限制了,唯一的区别是 COE 或执行上下文,就“将地址空间与父级分离”或共享地址空间而言。事实上,他使用一个 Web 服务器示例来说明他的观点 here(强烈推荐阅读)。

完全归功于 linux kernel documentation


K
KeyserSoze

更复杂的是,还有 thread-local storage 和 Unix 共享内存之类的东西。

线程局部存储允许每个线程拥有一个单独的全局对象实例。我唯一一次使用它是在 linux/windows 上构建仿真环境时,用于在 RTOS 中运行的应用程序代码。在 RTOS 中,每个任务都是一个具有自己地址空间的进程,在仿真环境中,每个任务都是一个线程(具有共享地址空间)。通过对单例等事物使用 TLS,我们能够为每个线程拥有一个单独的实例,就像在“真实”RTOS 环境下一样。

共享内存可以(显然)为您提供让多个进程访问同一内存的性能优势,但代价是必须正确同步进程。一种方法是让一个进程在共享内存中创建一个数据结构,然后通过传统的进程间通信(如命名管道)向该结构发送一个句柄。


我使用线程本地存储进行一些统计数据收集,上次我编写线程网络程序时:每个线程都写入自己的计数器,不需要锁,只有在收到消息时,每个线程才会将其统计信息合并到全局总数中。但是,是的,TLS 不是很常用或没有必要。另一方面,共享内存......除了有效地发送数据之外,您还可以通过将它们放在共享内存中来在进程之间共享 POSIX 信号量。这真是太神奇了。
a
aal8

在我最近使用 LINUX 的工作中,需要注意的一件事是库。如果您使用线程,请确保您可以跨线程使用的任何库都是线程安全的。这让我烧了几次。值得注意的是 libxml2 不是开箱即用的线程安全的。它可以用线程安全的方式编译,但这不是你通过 aptitude install 得到的。


e
eduffy

我必须同意你所听到的。当我们对集群(xhpl 等)进行基准测试时,我们总是会通过线程获得显着更好的进程性能。 </anecdote>


h
hlovdal

线程/进程之间的决定取决于您将使用它来做什么。进程的好处之一是它有一个 PID,可以在不终止父进程的情况下被杀死。

对于 Web 服务器的真实示例,apache 1.3 过去仅支持多个进程,但在 2.0 中,他们添加了 an abstraction,以便您可以在其中一个之间切换。 Comments seems to 同意进程更健壮,但线程可以提供更好的性能(除了进程性能很差并且您只想使用线程的窗口)。


n
neal aise

对于大多数情况,我更喜欢进程而不是线程。当您有一个相对较小的任务(进程开销>>每个划分的任务单元所花费的时间)并且需要它们之间的内存共享时,线程可能很有用。想想一个大数组。另外(离题),请注意,如果您的 CPU 利用率为 100% 或接近 100%,则多线程或处理将没有任何好处。 (实际上它会恶化)


没有好处是什么意思?在 GUI 线程中执行繁重的计算怎么样?从用户体验的角度来看,将它们移动到并行线程会好得多,无论 CPU 是如何加载的。
J
Jubin Antony Thykattil

线程 --> 线程共享内存空间,是对 CPU 的抽象,是轻量级的。进程 --> 进程有自己的内存空间,它是计算机的抽象。要并行化任务,您需要抽象一个 CPU。然而,使用进程而不是线程的优点是安全性、稳定性,而线程使用的内存比进程少,并且延迟更小。网络方面的一个例子是 chrome 和 firefox。在 Chrome 的情况下,每个选项卡都是一个新进程,因此 chrome 的内存使用率高于 firefox,而提供的安全性和稳定性优于 firefox。 chrome提供的安全性更好,因为每个选项卡都是一个新进程,不同的选项卡无法窥探给定进程的内存空间。


W
Walt Howard

多线程适用于受虐狂。 :)

如果您担心不断创建线程/分叉的环境,可能就像处理请求的 Web 服务器一样,您可以预先分叉进程,必要时可以使用数百个。由于它们在写入时复制并使用相同的内存,直到发生写入,所以速度非常快。它们都可以阻塞,侦听同一个套接字,第一个接受传入 TCP 连接的套接字就可以运行。使用 g++,您还可以将函数和变量指定为紧密放置在内存中(热段),以确保当您写入内存时,并导致复制整个页面,至少随后的写入活动将发生在同一页面上。您确实必须使用分析器来验证这类东西,但如果您担心性能,无论如何您都应该这样做。

由于共享对象上的微妙交互、您没有想到的线程“陷阱”以及由于无法随意重现线程交互问题而非常难以调试,因此线程应用程序的开发时间要长 3 倍到 10 倍。您可能必须进行各种性能检查,例如在每个函数之前和之后检查的所有类中都有不变量,如果出现问题,您可以停止进程并加载调试器。大多数情况下,在生产过程中发生令人尴尬的崩溃,您必须仔细研究核心转储,试图找出哪些线程做了什么。坦率地说,除非您明确共享某些内容,否则当分叉进程同样快速且隐式线程安全时,这不值得头疼。至少通过显式共享,如果发生线程样式问题,您确切知道在哪里查看。

如果性能如此重要,请添加另一台计算机并进行负载平衡。对于调试多线程应用程序的开发人员成本,即使是由经验丰富的多线程程序编写的应用程序,您也可以购买 4 块 40 核英特尔主板,每块 64GB 内存。

话虽如此,在某些不对称情况下并行处理是不合适的,例如,您希望前台线程接受用户输入并立即显示按钮按下,而无需等待一些笨重的后端 GUI 跟上。多处理在几何上不适合的线程的性感使用。许多类似的东西只是变量或指针。它们不是可以在分叉中共享的“句柄”。你必须使用线程。即使您进行了分叉,您也将共享相同的资源并受到线程样式问题的影响。


e
ephemient

如果你需要共享资源,你真的应该使用线程。

还要考虑线程之间的上下文切换比进程之间的上下文切换便宜得多的事实。

我认为没有理由明确地使用单独的流程,除非您有充分的理由这样做(安全性、经过验证的性能测试等......)


我确实有代表要编辑,但我不太同意。 Linux 上进程之间的上下文切换几乎与线程之间的上下文切换一样便宜。