关闭。这个问题是基于意见的。它目前不接受答案。想改进这个问题?更新问题,以便可以通过编辑这篇文章用事实和引用来回答它。 5个月前关闭。社区在 5 个月前审查了是否重新打开此问题并将其关闭:原始关闭原因未解决 改进此问题
我最近听到一些人说,在 Linux 中,使用进程而不是线程几乎总是更好,因为 Linux 在处理进程方面非常高效,并且因为与线程相关的问题(例如锁定)非常多。但是,我很怀疑,因为在某些情况下,线程似乎可以带来相当大的性能提升。
所以我的问题是,当面对线程和进程都可以很好地处理的情况时,我应该使用进程还是线程?例如,如果我正在编写 Web 服务器,我应该使用进程还是线程(或组合)?
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_fork
和 SYS_clone
系统调用都存在,但在内核中,sys_fork
和 sys_clone
都是围绕同一 do_fork
函数的非常薄的包装器,它本身是围绕 copy_process
的薄包装器。是的,术语 process
、thread
和 task
在 Linux 内核中可以互换使用...
Linux(实际上是 Unix)为您提供了第三种选择。
选项 1 - 流程
创建一个独立的可执行文件来处理应用程序的某些部分(或所有部分),并为每个进程单独调用它,例如,程序运行自身的副本以委派任务。
选项 2 - 线程
创建一个独立的可执行文件,它以单个线程启动并创建额外的线程来执行某些任务
选项 3 - 分叉
仅在 Linux/Unix 下可用,这有点不同。分叉的进程实际上是它自己的进程,有自己的地址空间——子进程(通常)不能做任何事情来影响其父或兄弟的地址空间(不像线程)——所以你得到了额外的健壮性。
但是,内存页面不会被复制,它们是写时复制的,因此通常使用的内存比您想象的要少。
考虑一个包含两个步骤的 Web 服务器程序:
读取配置和运行时数据服务页面请求
如果您使用线程,则第 1 步将执行一次,第 2 步将在多个线程中完成。如果您使用“传统”流程,则需要为每个流程重复步骤 1 和 2,并且需要复制用于存储配置和运行时数据的内存。如果您使用了 fork(),那么您可以执行第 1 步一次,然后执行 fork(),将运行时数据和配置保留在内存中,不受影响,不复制。
所以真的有三个选择。
这取决于很多因素。进程比线程更重,启动和关闭成本更高。进程间通信(IPC)也比线程间通信更难更慢。
相反,进程比线程更安全,因为每个进程都在自己的虚拟地址空间中运行。如果一个进程崩溃或缓冲区溢出,它根本不会影响任何其他进程,而如果一个线程崩溃,它会关闭进程中的所有其他线程,如果一个线程有缓冲区溢出,它就会打开所有线程中的安全漏洞。
因此,如果您的应用程序的模块可以大部分独立运行而几乎没有通信,那么如果您能够承担启动和关闭成本,您可能应该使用进程。 IPC 对性能的影响将是最小的,并且您对错误和安全漏洞会稍微安全一些。如果您需要获得或拥有大量共享数据(例如复杂的数据结构)的所有性能,请使用线程。
其他人已经讨论了这些考虑因素。
也许重要的区别在于,与线程相比,Windows 中的进程繁重且昂贵,而在 Linux 中,差异要小得多,因此等式在不同的点上取得平衡。
曾几何时,有 Unix,在这个古老的 Unix 中,进程有很多开销,所以一些聪明的人所做的就是创建线程,这些线程将与父进程共享相同的地址空间,他们只需要减少上下文switch,这将使上下文切换更有效。
在当代 Linux (2.6.x) 中,与线程相比,进程的上下文切换在性能上没有太大差异(只有 MMU 的东西是线程的附加内容)。共享地址空间存在问题,这意味着线程中的错误指针可能会破坏父进程或同一地址空间中另一个线程的内存。
进程受 MMU 保护,因此错误的指针只会导致信号 11 而不会损坏。
我通常会使用进程(在 Linux 中没有太多的上下文切换开销,但由于 MMU 的内存保护),但是如果我需要一个实时调度程序类,则使用 pthreads,这完全是另一杯茶。
为什么你认为线程在 Linux 上会有如此大的性能提升?你有这方面的任何数据,还是只是一个神话?
如果您想创建一个尽可能纯的进程,您将使用 clone()
并设置所有克隆标志。 (或者省去打字的麻烦,然后调用 fork()
)
如果您想尽可能地创建一个纯线程,您可以使用 clone()
并清除所有克隆标志(或者省去打字工作并调用 pthread_create()
)
有 28 个标志指示资源共享的级别。这意味着您可以创建超过 2.68 亿种任务,具体取决于您要共享的内容。
当我们说 Linux 不区分进程和线程,而是将程序中的任何控制流作为任务来暗示时,这就是我们的意思。不区分两者的理由是,嗯,不是唯一定义超过 2.68 亿种口味!
因此,做出是否使用进程或线程的“完美决定”实际上就是决定克隆 28 个资源中的哪一个。
https://i.stack.imgur.com/PwTDC.png
你的任务有多紧密耦合?
如果他们可以彼此独立生活,那么使用流程。如果它们相互依赖,则使用线程。这样你就可以杀死并重新启动一个错误的进程,而不会干扰其他任务的运行。
我认为每个人都在回答您的问题方面做得很好。我只是在 Linux 中添加有关线程与进程的更多信息,以澄清和总结内核上下文中的一些先前响应。所以,我的回应是关于 Linux 中的内核特定代码。根据 Linux Kernel 文档,线程与进程之间没有明显的区别,只是线程使用共享虚拟地址空间而不是进程。另请注意,Linux 内核使用术语“任务”来指代进程和线程。
“没有实现进程或线程的内部结构,而是有一个结构 task_struct 描述了一个称为任务的抽象调度单元”
此外,根据 Linus Torvalds 的说法,您根本不应该考虑进程与线程,因为它太受限制了,唯一的区别是 COE 或执行上下文,就“将地址空间与父级分离”或共享地址空间而言。事实上,他使用一个 Web 服务器示例来说明他的观点 here(强烈推荐阅读)。
完全归功于 linux kernel documentation
更复杂的是,还有 thread-local storage 和 Unix 共享内存之类的东西。
线程局部存储允许每个线程拥有一个单独的全局对象实例。我唯一一次使用它是在 linux/windows 上构建仿真环境时,用于在 RTOS 中运行的应用程序代码。在 RTOS 中,每个任务都是一个具有自己地址空间的进程,在仿真环境中,每个任务都是一个线程(具有共享地址空间)。通过对单例等事物使用 TLS,我们能够为每个线程拥有一个单独的实例,就像在“真实”RTOS 环境下一样。
共享内存可以(显然)为您提供让多个进程访问同一内存的性能优势,但代价是必须正确同步进程。一种方法是让一个进程在共享内存中创建一个数据结构,然后通过传统的进程间通信(如命名管道)向该结构发送一个句柄。
在我最近使用 LINUX 的工作中,需要注意的一件事是库。如果您使用线程,请确保您可以跨线程使用的任何库都是线程安全的。这让我烧了几次。值得注意的是 libxml2 不是开箱即用的线程安全的。它可以用线程安全的方式编译,但这不是你通过 aptitude install 得到的。
我必须同意你所听到的。当我们对集群(xhpl
等)进行基准测试时,我们总是会通过线程获得显着更好的进程性能。 </anecdote>
线程/进程之间的决定取决于您将使用它来做什么。进程的好处之一是它有一个 PID,可以在不终止父进程的情况下被杀死。
对于 Web 服务器的真实示例,apache 1.3 过去仅支持多个进程,但在 2.0 中,他们添加了 an abstraction,以便您可以在其中一个之间切换。 Comments seems to 同意进程更健壮,但线程可以提供更好的性能(除了进程性能很差并且您只想使用线程的窗口)。
对于大多数情况,我更喜欢进程而不是线程。当您有一个相对较小的任务(进程开销>>每个划分的任务单元所花费的时间)并且需要它们之间的内存共享时,线程可能很有用。想想一个大数组。另外(离题),请注意,如果您的 CPU 利用率为 100% 或接近 100%,则多线程或处理将没有任何好处。 (实际上它会恶化)
线程 --> 线程共享内存空间,是对 CPU 的抽象,是轻量级的。进程 --> 进程有自己的内存空间,它是计算机的抽象。要并行化任务,您需要抽象一个 CPU。然而,使用进程而不是线程的优点是安全性、稳定性,而线程使用的内存比进程少,并且延迟更小。网络方面的一个例子是 chrome 和 firefox。在 Chrome 的情况下,每个选项卡都是一个新进程,因此 chrome 的内存使用率高于 firefox,而提供的安全性和稳定性优于 firefox。 chrome提供的安全性更好,因为每个选项卡都是一个新进程,不同的选项卡无法窥探给定进程的内存空间。
多线程适用于受虐狂。 :)
如果您担心不断创建线程/分叉的环境,可能就像处理请求的 Web 服务器一样,您可以预先分叉进程,必要时可以使用数百个。由于它们在写入时复制并使用相同的内存,直到发生写入,所以速度非常快。它们都可以阻塞,侦听同一个套接字,第一个接受传入 TCP 连接的套接字就可以运行。使用 g++,您还可以将函数和变量指定为紧密放置在内存中(热段),以确保当您写入内存时,并导致复制整个页面,至少随后的写入活动将发生在同一页面上。您确实必须使用分析器来验证这类东西,但如果您担心性能,无论如何您都应该这样做。
由于共享对象上的微妙交互、您没有想到的线程“陷阱”以及由于无法随意重现线程交互问题而非常难以调试,因此线程应用程序的开发时间要长 3 倍到 10 倍。您可能必须进行各种性能检查,例如在每个函数之前和之后检查的所有类中都有不变量,如果出现问题,您可以停止进程并加载调试器。大多数情况下,在生产过程中发生令人尴尬的崩溃,您必须仔细研究核心转储,试图找出哪些线程做了什么。坦率地说,除非您明确共享某些内容,否则当分叉进程同样快速且隐式线程安全时,这不值得头疼。至少通过显式共享,如果发生线程样式问题,您确切知道在哪里查看。
如果性能如此重要,请添加另一台计算机并进行负载平衡。对于调试多线程应用程序的开发人员成本,即使是由经验丰富的多线程程序编写的应用程序,您也可以购买 4 块 40 核英特尔主板,每块 64GB 内存。
话虽如此,在某些不对称情况下并行处理是不合适的,例如,您希望前台线程接受用户输入并立即显示按钮按下,而无需等待一些笨重的后端 GUI 跟上。多处理在几何上不适合的线程的性感使用。许多类似的东西只是变量或指针。它们不是可以在分叉中共享的“句柄”。你必须使用线程。即使您进行了分叉,您也将共享相同的资源并受到线程样式问题的影响。
如果你需要共享资源,你真的应该使用线程。
还要考虑线程之间的上下文切换比进程之间的上下文切换便宜得多的事实。
我认为没有理由明确地使用单独的流程,除非您有充分的理由这样做(安全性、经过验证的性能测试等......)
socket
、bind
、listen
、fork
,然后在同一个侦听套接字上拥有多个进程accept
连接。如果一个进程很忙,它可以停止接受,并且内核会将传入的连接路由到另一个进程(如果没有人在监听,内核将排队或丢弃,具体取决于listen
积压)。您对工作分配的控制不多,但通常这已经足够了!clone()
的标志确定共享哪些资源。任务还可以在以后的任何时间点unshare()
资源。task_struct
。这通常在整个内核代码中称为“进程”,但它对应于每个可运行线程。没有process_struct
;如果一堆task_struct
通过它们的thread_group
列表链接在一起,那么它们对于用户空间来说是同一个“进程”。对“线程”有一些特殊处理,例如,所有同级线程都在 fork 和 exec 上停止,只有“主”线程出现在ls /proc
中。但是,无论是否在/proc
中列出,每个线程都可以通过/proc/pid
访问。clone(CLONE_THREAD | CLONE_VM | CLONE_SIGHAND))
将为您提供一个不共享工作目录、文件或锁的新“线程”,而clone(CLONE_FILES | CLONE_FS | CLONE_IO)
将为您提供一个共享的“进程”。底层系统通过克隆创建任务;fork()
和pthread_create()
只是以不同方式调用clone()
的库函数(正如我在此答案中所写的那样)。