最近我听到一些人说,在Linux中,使用进程几乎总是比使用线程更好,因为Linux在处理进程方面非常高效,而且与线程相关的问题太多了(比如锁)。然而,我对此持怀疑态度,因为在某些情况下,线程似乎可以带来相当大的性能提升。
因此,我的问题是,当遇到线程和进程都可以很好地处理的情况时,我应该使用进程还是线程?例如,如果我正在编写一个web服务器,我应该使用进程还是线程(或组合)?
我不得不同意你所听到的。当我们对我们的集群(xhpl等)进行基准测试时,我们总是通过进程而不是线程获得明显更好的性能。</anecdote>
xhpl
</anecdote>
如果你需要共享资源,你真的应该使用线程。
还要考虑这样一个事实:线程之间的上下文切换比进程之间的上下文切换代价要小得多。
我认为没有理由明确地使用单独的进程,除非你有一个很好的理由这样做(安全,经过验证的性能测试,等等……)
这取决于很多因素。进程比线程更重,启动和关闭成本更高。进程间通信(IPC)也比线程间通信更困难、更慢。
相反,进程比线程更安全,因为每个进程都运行在自己的虚拟地址空间中。如果一个进程崩溃或缓冲区溢出,它根本不会影响任何其他进程,而如果一个线程崩溃,它会关闭进程中的所有其他线程,如果一个线程缓冲区溢出,它会在所有线程中打开一个安全漏洞。
因此,如果应用程序的模块可以在很少通信的情况下独立运行,那么如果能够负担得起启动和关闭成本,则可能应该使用进程。IPC对性能的影响将是最小的,并且您在漏洞和安全漏洞方面会稍微安全一些。如果您需要获得或拥有大量共享数据(例如复杂的数据结构),那么请使用线程。
对于一个真实世界的web服务器的例子,apache 1.3过去只支持多个进程,但在2.0中他们添加了一个抽象,这样你就可以在两者之间切换。评论 似乎 来同意进程更健壮,但线程可以提供更好的性能(除了那些进程性能糟糕且你只想使用线程的窗口)。
你的任务有多紧密耦合?
如果它们可以彼此独立,那么就使用流程。如果它们相互依赖,则使用线程。这样,您就可以终止并重新启动坏进程,而不会影响其他任务的操作。
其他人讨论了这些考虑因素。
也许重要的区别是,在Windows中,与线程相比,进程是沉重和昂贵的,而在Linux中,差异要小得多,所以等式在不同的点上平衡。
Linux(实际上还有Unix)为您提供了第三种选择。
创建一个独立的可执行文件来处理应用程序的某些部分(或所有部分),并为每个进程分别调用它,例如,程序运行自己的副本来委托任务。
创建一个独立的可执行文件,它由一个线程启动,并创建额外的线程来执行一些任务
仅在Linux/Unix下可用,这有点不同。fork进程实际上是拥有自己地址空间的进程——子进程(通常)无法影响其父进程或兄弟进程的地址空间(不像线程)——因此您获得了额外的健壮性。
但是,内存页不是复制的,它们是写时复制的,因此通常使用的内存比您想象的要少。
考虑一个web服务器程序,它包含两个步骤:
如果您使用线程,第1步将完成一次,第2步将在多个线程中完成。如果您使用“传统”流程,那么每个流程都需要重复步骤1和步骤2,存储配置和运行时数据的内存也需要重复。如果您使用fork(),那么您可以执行第1步,然后fork(),将运行时数据和配置保留在内存中,不受影响,不复制。
所以实际上有三种选择。
Linux使用1-1线程模型,(对内核来说)没有进程和线程的区别——一切都只是一个可运行的任务。*
在Linux上,系统调用clone克隆一个任务,具有可配置的共享级别,其中包括:
clone
CLONE_FILES
CLONE_PARENT
getppid()
getpid()
CLONE_VM
fork()调用__abc1最少共享__abc2, pthread_create()调用__abc1最多共享__abc2。**
fork()
pthread_create()
由于复制表和为内存创建COW映射,forking的成本比pthread_createing略高,但Linux内核开发人员已经尝试(并成功)将这些成本最小化。
fork
pthread_create
如果任务共享相同的内存空间和不同的表,那么它们之间的切换将比不共享的任务稍微便宜一些,因为数据可能已经加载到缓存中了。然而,即使没有任何共享,切换任务仍然非常快——这是Linux内核开发人员试图确保(并成功确保)的另一件事。
事实上,如果你是在多处理器系统上,不共享实际上可能对性能有益:如果每个任务都运行在不同的处理器上,同步共享内存的开销很大。
*简化。CLONE_THREAD导致信号传递被共享(这需要CLONE_SIGHAND,它共享信号处理程序表)。
CLONE_THREAD
CLONE_SIGHAND
* *简化。存在SYS_fork和SYS_clone系统调用,但在内核中,sys_fork和sys_clone都是对同一个do_fork函数的非常薄的包装,而do_fork函数本身也是对copy_process的薄包装。是的,术语process、thread和task在Linux内核中是可以互换使用的……
SYS_fork
SYS_clone
sys_fork
sys_clone
do_fork
copy_process
process
thread
task
更复杂的是,还有诸如线程本地存储和Unix共享内存这样的东西。
线程本地存储允许每个线程拥有一个单独的全局对象实例。我唯一一次使用它是在linux/windows上为运行在RTOS中的应用程序代码构建模拟环境时。在RTOS中,每个任务都是一个具有自己地址空间的进程,在模拟环境中,每个任务都是一个线程(具有共享地址空间)。通过将TLS用于像单例这样的事情,我们能够为每个线程拥有一个单独的实例,就像在“真正的”RTOS环境下一样。
共享内存(显然)可以为您带来让多个进程访问相同内存的性能优势,但代价是必须正确地同步进程。一种方法是让一个进程在共享内存中创建一个数据结构,然后通过传统的进程间通信(如命名管道)向该结构发送句柄。
从前有Unix,在这个好的旧Unix中,进程有很多开销,所以一些聪明的人所做的是创建线程,这些线程将与父进程共享相同的地址空间,他们只需要减少上下文切换,这将使上下文切换更有效。
一个进程受到MMU的保护,所以一个错误的指针只会导致一个信号11,而不会损坏。
我通常会使用进程(在Linux中没有太多的上下文切换开销,但是由于MMU的内存保护),但是如果我需要一个实时调度器类,则会使用pthreads,这完全是另一回事。
为什么你认为线程在Linux上有这么大的性能提升?你有这方面的数据吗,还是说这只是一个神话?
在我最近的LINUX工作中,需要注意的一件事是库。如果您正在使用线程,请确保跨线程使用的所有库都是线程安全的。这让我疼了好几次。值得注意的是,libxml2并不是开箱即用的线程安全的。它可以用线程安全编译,但这不是你用aptitude install得到的。
我认为每个人都很好地回答了你的问题。我只是添加了更多关于Linux中线程与进程的信息,以澄清和总结之前在内核上下文中的一些响应。因此,我的回答是关于Linux中特定于内核的代码。根据Linux内核文档,除了线程使用共享虚拟地址空间不同于进程之外,线程与进程之间没有明确的区别。还要注意,Linux内核使用术语“任务”来泛指进程和线程。
没有实现进程或线程的内部结构,而是有一个结构体task_struct,它描述了一个称为task的抽象调度单元。
另外,根据Linus Torvalds的说法,你根本不应该考虑进程和线程,因为这太有限了,唯一的区别是COE或执行上下文在“从父地址空间分离”或共享地址空间方面的区别。事实上,他使用了一个web服务器的例子来说明他的观点在这里(强烈推荐阅读)。
完全归功于Linux内核文档
如果你想创建一个纯a进程,你可以使用clone()并设置所有的克隆标志。(或者调用fork()来节省打字的时间)
clone()
如果你想尽可能地创建一个纯线程,你可以使用clone()并清除所有克隆标志(或者节省你自己的输入工作并调用pthread_create())
有28个标志指示资源共享的级别。这意味着你可以创建超过2.68亿种类型的任务,这取决于你想分享什么。
这就是我们所说的Linux不区分进程和线程,而是指程序中的任何控制流都是任务的意思。不区分两者的理由是,嗯,并不是唯一定义了超过2.68亿种口味!
因此,做出“完美的决定”;是使用进程还是线程实际上是关于决定克隆28种资源中的哪一种。
多线程是为受虐狂准备的。:)
如果你担心不断创建线程/fork的环境,比如处理请求的web服务器,你可以预fork进程,必要时可以预fork数百个进程。因为它们是写时拷贝,并且在写发生之前使用相同的内存,所以速度非常快。它们都可以阻塞,侦听同一个套接字,第一个接受传入TCP连接的套接字就可以运行它。使用g++,您还可以将函数和变量分配到内存中(热段),以确保当您写入内存时,并导致整个页面被复制,至少后续的写入活动将发生在同一页上。你真的必须使用分析器来验证这类东西,但如果你关心性能,你无论如何都应该这样做。
线程应用程序的开发时间是3倍到10倍,因为共享对象上的微妙交互,线程“陷阱”;您没有想到,并且很难调试,因为您不能随意重现线程交互问题。你可能不得不做各种性能扼杀检查,比如在你所有的类中,在每个函数之前和之后检查不变量,如果有什么不对,你就会停止进程并加载调试器。大多数情况下,在生产过程中会出现令人尴尬的崩溃,您必须仔细研究核心转储,试图找出哪些线程做了什么。坦率地说,如果分叉进程同样快速且隐式地线程安全,除非显式地共享某些内容,那么就不值得为此头疼。至少在显式共享中,如果出现线程样式的问题,您可以确切地知道在哪里查找。
如果性能如此重要,那就增加另一台计算机和负载平衡。对于开发人员调试一个多线程应用程序的成本,即使是由一个有经验的多线程程序编写的应用程序,你可能会买4块40核的英特尔主板,每块都有64g内存。
也就是说,在不对称的情况下,并行处理是不合适的,比如,你希望前台线程接受用户输入并立即显示按钮按下,而不需要等待一些笨拙的后端GUI来跟上。多处理在几何上不合适的地方使用线程。很多类似的东西都是变量或指针。它们不是“句柄”。可以在fork中共享。你必须使用线程。即使你使用了fork,你也会共享相同的资源并受到线程样式问题的影响。