Linux中的线程与进程

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

因此,我的问题是,当遇到线程和进程都可以很好地处理的情况时,我应该使用进程还是线程?例如,如果我正在编写一个web服务器,我应该使用进程还是线程(或组合)?

127991 次浏览

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

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

还要考虑这样一个事实:线程之间的上下文切换比进程之间的上下文切换代价要小得多。

我认为没有理由明确地使用单独的进程,除非你有一个很好的理由这样做(安全,经过验证的性能测试,等等……)

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

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

因此,如果应用程序的模块可以在很少通信的情况下独立运行,那么如果能够负担得起启动和关闭成本,则可能应该使用进程。IPC对性能的影响将是最小的,并且您在漏洞和安全漏洞方面会稍微安全一些。如果您需要获得或拥有大量共享数据(例如复杂的数据结构),那么请使用线程。

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

对于一个真实世界的web服务器的例子,apache 1.3过去只支持多个进程,但在2.0中他们添加了一个抽象,这样你就可以在两者之间切换。评论 似乎 同意进程更健壮,但线程可以提供更好的性能(除了那些进程性能糟糕且你只想使用线程的窗口)。

你的任务有多紧密耦合?

如果它们可以彼此独立,那么就使用流程。如果它们相互依赖,则使用线程。这样,您就可以终止并重新启动坏进程,而不会影响其他任务的操作。

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

也许重要的区别是,在Windows中,与线程相比,进程是沉重和昂贵的,而在Linux中,差异要小得多,所以等式在不同的点上平衡。

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

选项1 -流程

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

选项2 -线程

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

选项3 -分叉

仅在Linux/Unix下可用,这有点不同。fork进程实际上是拥有自己地址空间的进程——子进程(通常)无法影响其父进程或兄弟进程的地址空间(不像线程)——因此您获得了额外的健壮性。

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

考虑一个web服务器程序,它包含两个步骤:

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

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

所以实际上有三种选择。

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

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

  • CLONE_FILES:共享相同的文件描述符表(而不是创建一个副本)
  • CLONE_PARENT:不要在新任务和旧任务之间建立父子关系(否则,child的getppid() = parent的getpid())
  • CLONE_VM:共享相同的内存空间(而不是创建一个副本)

fork()调用__abc1最少共享__abc2, pthread_create()调用__abc1最多共享__abc2。**

由于复制表和为内存创建COW映射,forking的成本比pthread_createing略高,但Linux内核开发人员已经尝试(并成功)将这些成本最小化。

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

事实上,如果你是在多处理器系统上,共享实际上可能对性能有益:如果每个任务都运行在不同的处理器上,同步共享内存的开销很大。


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

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

更复杂的是,还有诸如线程本地存储和Unix共享内存这样的东西。

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

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

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

在当代的Linux (2.6.x)中,进程的上下文切换与线程的上下文切换在性能上没有太大区别(只有MMU的东西对线程来说是额外的)。 共享地址空间存在问题,这意味着线程中的错误指针可能破坏同一地址空间中的父进程或另一个线程的内存。< / p >

一个进程受到MMU的保护,所以一个错误的指针只会导致一个信号11,而不会损坏。

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

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

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

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

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

我认为每个人都很好地回答了你的问题。我只是添加了更多关于Linux中线程与进程的信息,以澄清和总结之前在内核上下文中的一些响应。因此,我的回答是关于Linux中特定于内核的代码。根据Linux内核文档,除了线程使用共享虚拟地址空间不同于进程之外,线程与进程之间没有明确的区别。还要注意,Linux内核使用术语“任务”来泛指进程和线程。

没有实现进程或线程的内部结构,而是有一个结构体task_struct,它描述了一个称为task的抽象调度单元。

另外,根据Linus Torvalds的说法,你根本不应该考虑进程和线程,因为这太有限了,唯一的区别是COE或执行上下文在“从父地址空间分离”或共享地址空间方面的区别。事实上,他使用了一个web服务器的例子来说明他的观点在这里(强烈推荐阅读)。

完全归功于Linux内核文档

如果你想创建一个纯a进程,你可以使用clone()并设置所有的克隆标志。(或者调用fork()来节省打字的时间)

如果你想尽可能地创建一个纯线程,你可以使用clone()并清除所有克隆标志(或者节省你自己的输入工作并调用pthread_create())

有28个标志指示资源共享的级别。这意味着你可以创建超过2.68亿种类型的任务,这取决于你想分享什么。

这就是我们所说的Linux不区分进程和线程,而是指程序中的任何控制流都是任务的意思。不区分两者的理由是,嗯,并不是唯一定义了超过2.68亿种口味!

因此,做出“完美的决定”;是使用进程还是线程实际上是关于决定克隆28种资源中的哪一种。

enter image description here

多线程是为受虐狂准备的。:)

如果你担心不断创建线程/fork的环境,比如处理请求的web服务器,你可以预fork进程,必要时可以预fork数百个进程。因为它们是写时拷贝,并且在写发生之前使用相同的内存,所以速度非常快。它们都可以阻塞,侦听同一个套接字,第一个接受传入TCP连接的套接字就可以运行它。使用g++,您还可以将函数和变量分配到内存中(热段),以确保当您写入内存时,并导致整个页面被复制,至少后续的写入活动将发生在同一页上。你真的必须使用分析器来验证这类东西,但如果你关心性能,你无论如何都应该这样做。

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

如果性能如此重要,那就增加另一台计算机和负载平衡。对于开发人员调试一个多线程应用程序的成本,即使是由一个有经验的多线程程序编写的应用程序,你可能会买4块40核的英特尔主板,每块都有64g内存。

也就是说,在不对称的情况下,并行处理是不合适的,比如,你希望前台线程接受用户输入并立即显示按钮按下,而不需要等待一些笨拙的后端GUI来跟上。多处理在几何上不合适的地方使用线程。很多类似的东西都是变量或指针。它们不是“句柄”。可以在fork中共享。你必须使用线程。即使你使用了fork,你也会共享相同的资源并受到线程样式问题的影响。