Linux中的线程与进程


252

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

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


Linux 2.4有区别吗?
mouviciel 2009年

3
在Linux 2.4下,进程与线程之间的区别在于,线程共享状态的更多部分(地址空间,文件句柄等)比进程通常不共享。Linux 2.6下的NPTL通过为它们提供“线程组”(在Win32和Solaris中类似于“进程”)使这一点变得更加清晰。
MarkR

6
并行编程很困难。除非您需要非常高的性能,否则折衷中最重要的方面通常是调试困难。流程使这方面的解决方案更加容易,因为所有通信都是明确的(易于检查,记录等)。相反,线程的共享内存在一个线程可能错误影响另一个线程的地方创建了数不胜数的位置。
Lutz Prechelt 2015年

1
@LutzPrechelt-并发编程可以是多线程的,也可以是多进程的。我不明白为什么您假设并发编程仅是多线程的。可能是由于某些特定的语言限制,但通常可以两者兼而有之。
iankit 2015年

2
我链接Lutz只是说,并发编程很难选择(进程或线程),但是在许多情况下,使用进程进行并发编程使调试更加容易。
user2692263

Answers:


322

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

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

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

fork()呼叫clone(最少共享)pthread_create()呼叫clone(最多共享)。**

forkpthread_create因为复制表和为内存创建COW映射,所以ing的成本比ing 贵一点,但是Linux内核开发人员已尝试(并成功)使这些成本最小化。

如果任务共享相同的内存空间和各种表,则在任务之间进行切换将比不共享任务便宜一些,因为数据可能已经加载到缓存中。但是,即使没有共享,切换任务仍然非常快-这是Linux内核开发人员试图确保(并成功确保)的其他事情。

实际上,如果您使用的是多处理器系统,则不共享实际上可能会对性能有所帮助:如果每个任务都在不同的处理器上运行,则同步共享内存的成本很高。


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

**简化。SYS_forkSYS_clonesyscall 都存在,但是在内核中,sys_forksys_clone都是围绕同一do_fork函数的非常薄的包装器,而后者本身就是的薄包装器copy_process。是的,术语processthread以及task相当互换使用Linux内核...


6
我认为我们缺少1分。如果为Web服务器进行多个进程,则必须编写另一个进程来打开套接字并将“工作”传递给不同的线程。线程提供单个进程多个线程,设计简洁。在许多情况下,线程是自然的,而在其他情况下,新过程是自然的。当问题落在灰色区域时,如短暂性所解释的其他折衷就变得很重要。
萨拉巴

26
@Saurabh不是。您可以轻松地socketbindlistenfork,然后有多个进程accept相同的监听套接字上的连接。进程可以在忙碌时停止接受,并且内核会将进入的连接路由到另一个进程(如果没有人在监听,则内核将排队或挂断,具体取决于listen积压)。您对工作分配没有更多的控制权,但是通常这已经足够了!
短暂

2
@Bloodcount Linux上的所有进程/线程都是通过相同的机制创建的,该机制会克隆现有的进程/线程。传递标志以clone()确定共享哪些资源。任务也可以unshare()在以后的任何时间点进行资源分配。
ephemient 2014年

4
@KarthikBalaguru在内核本身中,task_struct每个任务都有一个。在整个内核代码中,这通常称为“进程”,但它对应于每个可运行线程。没有process_struct; 如果一串task_structs通过其thread_group列表链接在一起,则它们是用户空间的相同“进程”。对“线程”有一些特殊处理,例如,所有同级线程都在fork和exec上停止,并且只有“ main”线程显示在中ls /proc。每个线程都可以通过访问/proc/pid,无论是否列出/proc
2014年

5
@KarthikBalaguru内核支持线程与进程之间行为的连续性;例如,clone(CLONE_THREAD | CLONE_VM | CLONE_SIGHAND))将为您提供一个不共享工作目录,文件或锁的新“线程”,而clone(CLONE_FILES | CLONE_FS | CLONE_IO)为您提供一个不共享工作目录,文件或锁的“线程” 。底层系统通过克隆创建任务;fork()并且pthread_create()只是库函数调用clone()不同(如我在此答案中所写)。
2014年

60

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

选项1-流程

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

选项2-线程

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

选项3-前叉

仅在Linux / Unix下可用,这有点不同。分叉的进程实际上是具有自己的地址空间的自己的进程-子进程(通常)不会影响父进程或同级进程的兄弟进程(与线程不同),因此您将获得更多的鲁棒性。

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

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

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

如果使用线程,则步骤1将执行一次,步骤2将在多个线程中执行。如果使用“传统”进程,则每个进程都需要重复步骤1和2,并且存储配置和运行时数据的内存也将重复。如果使用fork(),则可以一次执行步骤1,然后再进行fork(),则将运行时数据和配置保留在内存中,保持不变,不被复制。

因此,确实有三个选择。


7
@Qwertie分叉不是很酷,它以微妙的方式破坏了很多库(如果您在父进程中使用它们)。它产生了意想不到的行为,甚至使有经验的程序员都感到困惑。
MarkR 2012年

2
@MarkR您能否举一些例子或链接,说明如何分叉破坏库并创建意外行为?
Ehtesh Choudhury

18
如果一个进程使用打开的mysql连接进行分叉,则会发生不好的事情,因为套接字在两个进程之间共享。即使只有一个进程使用该连接,另一个进程也会阻止其关闭。
MarkR 2012年

1
fork()系统调用是由POSIX指定的(这意味着它可以在任何Unix系统上使用),如果您使用了底层Linux API(即clone()系统调用),那么在Linux中,实际上除了三个选项外,您还有更多选择。
Lie Ryan

2
@MarkR套接字的共享是设计使然。此外,这两个进程中的任何一个都可以在调用套接字上的close()之前使用linux.die.net/man/2/shutdown关闭套接字。
Lelanthran'5

53

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

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

因此,如果您的应用程序模块可以在几乎没有通信的情况下基本上独立运行,那么您可以在负担得起启动和关闭成本的情况下使用进程。IPC的性能影响将是最小的,并且您将更安全地避免错误和安全漏洞。如果您需要获得性能的每一点,就可以获取或拥有很多共享数据(例如复杂的数据结构),请使用线程。


9
亚当的回答很适合作为高管简报。有关更多详细信息,MarkR和ephemient提供了很好的解释。可以在cs.cf.ac.uk/Dave/C/node29.html上找到带有示例的非常详细的说明,但部分内容似乎有些过时。
Cyber​​Fonic 2010年

2
Cyber​​Fonic适用于Windows。正如人们所说,在Linux下,进程并不繁重。在Linux下,所有可用于线程之间通信的机制(futex的,共享的内存,管道,IPC)也可用于进程并以相同的速度运行。
罗素·斯图尔特

IPC较难使用,但是如果有人使用“共享内存”怎么办?
abhiarora

11

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

也许重要的区别在于,与线程相比,Windows进程笨重且昂贵,而在Linux中,差异要小得多,因此方程式在另一个点平衡。


9

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

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

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

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

您为什么认为线程在Linux上具有如此大的性能提升?您是否有任何数据,还是只是神话?


1
是的,我确实有一些数据。我运行了一个创建100,000个进程的测试和一个创建100,000个线程的测试。线程版本的运行速度大约快9倍(进程为17.38秒,线程为1.93)。现在,这仅测试创建时间,但是对于短暂的任务,创建时间可能是关键。
user17918

4
@ user17918-您是否可以共享您用于计算上述计时的代码..
11

一个很大的不同,对于进程,内核为每个进程创建页表,而thead只使用一个页表,因此我认为正常的线程速度比进程快
c4f4t0r

另一种简单的查看方法是TCB比PCB小得多,因此很明显,涉及PCB的过程上下文切换比线程切换要消耗更多的时间。
Karthik Balaguru 2014年

5

您的任务有多紧密?

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


4

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

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

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


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

4

在我最近使用LINUX的工作中,需要注意的一件事是库。如果您正在使用线程,请确保可以跨线程使用的任何库都是线程安全的。这烧了我几次。显然,libxml2不是线程安全的。可以使用线程安全的方式对其进行编译,但这不是通过aptitude install获得的。


3

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


3

线程/进程之间的决定在某种程度上取决于您将使用它。进程的好处之一是它具有PID,可以在不终止父进程的情况下被杀死。

对于Web服务器的真实示例,apache 1.3曾经仅支持多个进程,但是在2.0中,它们添加了一个抽象,以便您可以在两者之间切换。评论 似乎 同意,工艺都比较稳健,但线程可以给一点点更好的性能(除了其中的工艺性能很烂的窗口,你只需要使用线程)。


2

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


没什么意思 如何在GUI线程中执行大量计算?从用户体验的角度来看,无论如何加载CPU,将它们移动到并行线程都将更好。
olegst

2

线程->线程共享一个内存空间,它是CPU的抽象,是轻量级的。进程->进程有自己的内存空间,它是计算机的抽象。要并行化任务,您需要抽象一个CPU。但是,在线程上使用进程的优点是安全性,稳定性,而线程使用的内存少于进程,并且提供的等待时间较短。就网络而言,例如chrome和firefox。如果使用Chrome,则每个选项卡都是新进程,因此chrome的内存使用率高于firefox,而提供的安全性和稳定性优于firefox。chrome提供的安全性更好,因为每个选项卡都是一个新进程,因此不同的选项卡无法监听到给定进程的内存空间。


2

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

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

同样,根据Linus Torvalds的观点,您根本不应该考虑进程与线程的关系,因为它太局限了,唯一的区别就是COE或执行上下文在“将地址空间与父级分开”或共享地址空间方面。实际上,他使用一个Web服务器示例来说明自己的观点。在这里(其中强烈建议你阅读)。

完全归功于Linux内核文档


-3

如果需要共享资源,则确实应该使用线程。

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

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


3
我确实有代表可以编辑,但是我不太同意。在Linux上的进程之间进行上下文切换几乎和在线程之间进行上下文切换一样便宜。
ephemient
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.