我最近听到一些人说,在Linux中,使用进程而不是线程几乎总是更好的,因为Linux在处理进程方面非常有效,并且与线程相关的问题很多(例如锁定)。但是,我感到怀疑,因为在某些情况下,线程似乎可以带来很大的性能提升。
所以我的问题是,当线程和进程都可以很好地处理时,我应该使用进程还是线程?例如,如果我正在编写Web服务器,是否应该使用进程或线程(或组合)?
我最近听到一些人说,在Linux中,使用进程而不是线程几乎总是更好的,因为Linux在处理进程方面非常有效,并且与线程相关的问题很多(例如锁定)。但是,我感到怀疑,因为在某些情况下,线程似乎可以带来很大的性能提升。
所以我的问题是,当线程和进程都可以很好地处理时,我应该使用进程还是线程?例如,如果我正在编写Web服务器,是否应该使用进程或线程(或组合)?
Answers:
Linux使用1-1线程模型,(对于内核)进程和线程之间没有区别-一切都只是可运行的任务。*
在Linux上,系统调用会clone
克隆一个具有可配置共享级别的任务,其中包括:
CLONE_FILES
:共享相同的文件描述符表(而不是创建副本)CLONE_PARENT
:不要在新任务和旧任务之间建立父子关系(否则,child's getppid()
= parent's getpid()
)CLONE_VM
:共享相同的内存空间(而不是创建COW副本)fork()
呼叫clone(
最少共享)
,pthread_create()
呼叫clone(
最多共享)
。**
fork
pthread_create
因为复制表和为内存创建COW映射,所以ing的成本比ing 贵一点,但是Linux内核开发人员已尝试(并成功)使这些成本最小化。
如果任务共享相同的内存空间和各种表,则在任务之间进行切换将比不共享任务便宜一些,因为数据可能已经加载到缓存中。但是,即使没有共享,切换任务仍然非常快-这是Linux内核开发人员试图确保(并成功确保)的其他事情。
实际上,如果您使用的是多处理器系统,则不共享实际上可能会对性能有所帮助:如果每个任务都在不同的处理器上运行,则同步共享内存的成本很高。
*简化。 CLONE_THREAD
导致信号传递被共享(需要CLONE_SIGHAND
共享信号处理程序表)。
**简化。SYS_fork
和SYS_clone
syscall 都存在,但是在内核中,sys_fork
和sys_clone
都是围绕同一do_fork
函数的非常薄的包装器,而后者本身就是的薄包装器copy_process
。是的,术语process
,thread
以及task
相当互换使用Linux内核...
socket
,bind
,listen
,fork
,然后有多个进程accept
相同的监听套接字上的连接。进程可以在忙碌时停止接受,并且内核会将进入的连接路由到另一个进程(如果没有人在监听,则内核将排队或挂断,具体取决于listen
积压)。您对工作分配没有更多的控制权,但是通常这已经足够了!
clone()
确定共享哪些资源。任务也可以unshare()
在以后的任何时间点进行资源分配。
task_struct
每个任务都有一个。在整个内核代码中,这通常称为“进程”,但它对应于每个可运行线程。没有process_struct
; 如果一串task_struct
s通过其thread_group
列表链接在一起,则它们是用户空间的相同“进程”。对“线程”有一些特殊处理,例如,所有同级线程都在fork和exec上停止,并且只有“ main”线程显示在中ls /proc
。每个线程都可以通过访问/proc/pid
,无论是否列出/proc
。
clone(CLONE_THREAD | CLONE_VM | CLONE_SIGHAND))
将为您提供一个不共享工作目录,文件或锁的新“线程”,而clone(CLONE_FILES | CLONE_FS | CLONE_IO)
为您提供一个不共享工作目录,文件或锁的“线程” 。底层系统通过克隆创建任务;fork()
并且pthread_create()
只是库函数调用clone()
不同(如我在此答案中所写)。
Linux(实际上是Unix)为您提供了第三种选择。
创建一个独立的可执行文件来处理应用程序的某些部分(或所有部分),并为每个进程分别调用它,例如,程序运行其自身的副本以将任务委托给它。
创建一个以单个线程启动的独立可执行文件,并创建其他线程来执行某些任务
仅在Linux / Unix下可用,这有点不同。分叉的进程实际上是具有自己的地址空间的自己的进程-子进程(通常)不会影响父进程或同级进程的兄弟进程(与线程不同),因此您将获得更多的鲁棒性。
但是,内存页不会被复制,而是写时复制的,因此通常使用的内存比您想象的要少。
考虑一个包含两个步骤的Web服务器程序:
如果使用线程,则步骤1将执行一次,步骤2将在多个线程中执行。如果使用“传统”进程,则每个进程都需要重复步骤1和2,并且存储配置和运行时数据的内存也将重复。如果使用fork(),则可以一次执行步骤1,然后再进行fork(),则将运行时数据和配置保留在内存中,保持不变,不被复制。
因此,确实有三个选择。
这取决于很多因素。进程比线程更重,并且具有更高的启动和关闭成本。进程间通信(IPC)也比线程间通信更困难,更慢。
相反,进程比线程更安全,因为每个进程都在其自己的虚拟地址空间中运行。如果一个进程崩溃或缓冲区溢出,那么它根本不会影响任何其他进程,而如果一个线程崩溃,它将删除该进程中的所有其他线程,并且如果一个线程有缓冲区溢出,它将打开所有线程中都有一个安全孔。
因此,如果您的应用程序模块可以在几乎没有通信的情况下基本上独立运行,那么您可以在负担得起启动和关闭成本的情况下使用进程。IPC的性能影响将是最小的,并且您将更安全地避免错误和安全漏洞。如果您需要获得性能的每一点,就可以获取或拥有很多共享数据(例如复杂的数据结构),请使用线程。
其他人已经讨论了考虑因素。
也许重要的区别在于,与线程相比,Windows进程笨重且昂贵,而在Linux中,差异要小得多,因此方程式在另一个点平衡。
曾经有Unix,在这个好的旧Unix中,进程有很多开销,所以一些聪明的人所做的就是创建线程,该线程将与父进程共享相同的地址空间,而他们只需要一个简化的上下文切换,这将使上下文切换更加有效。
在现代Linux(2.6.x)中,与线程相比,进程的上下文切换之间的性能差异不大(只有MMU才是线程的附加属性)。共享地址空间存在问题,这意味着线程中的错误指针可能损坏父进程或同一地址空间内另一个线程的内存。
进程受MMU保护,因此错误的指针只会导致信号11而不会损坏。
我通常会使用进程(在Linux中不会有太多的上下文切换开销,而是由于MMU而导致的内存保护),但如果我需要实时调度程序类,则使用pthreads,这完全是另一回事。
您为什么认为线程在Linux上具有如此大的性能提升?您是否有任何数据,还是只是神话?
更复杂的是,有诸如线程本地存储和Unix共享内存之类的东西。
线程本地存储允许每个线程拥有一个单独的全局对象实例。我唯一使用过的方法是在linux / windows上构建用于RTOS中运行的应用程序代码的仿真环境时。在RTOS中,每个任务都是一个具有自己地址空间的进程,在仿真环境中,每个任务都是一个线程(具有共享地址空间)。通过将TLS用于单例,我们可以为每个线程使用单独的实例,就像在“真实” RTOS环境下一样。
共享内存可以(显然)为您带来多个进程访问同一内存的性能优势,但是以必须正确同步这些进程的代价/风险为代价。一种方法是让一个进程在共享内存中创建一个数据结构,然后通过传统的进程间通信(如命名管道)将句柄发送到该结构。
线程->线程共享一个内存空间,它是CPU的抽象,是轻量级的。进程->进程有自己的内存空间,它是计算机的抽象。要并行化任务,您需要抽象一个CPU。但是,在线程上使用进程的优点是安全性,稳定性,而线程使用的内存少于进程,并且提供的等待时间较短。就网络而言,例如chrome和firefox。如果使用Chrome,则每个选项卡都是新进程,因此chrome的内存使用率高于firefox,而提供的安全性和稳定性优于firefox。chrome提供的安全性更好,因为每个选项卡都是一个新进程,因此不同的选项卡无法监听到给定进程的内存空间。
我认为每个人在回答您的问题方面都做得很好。我只是添加有关Linux中线程与进程的更多信息,以阐明和总结以前在内核上下文中的一些响应。因此,我的回应是关于Linux中内核特定的代码。根据Linux Kernel文档,线程与进程之间没有明显的区别,除了线程使用不同于进程的共享虚拟地址空间。另请注意,Linux内核通常使用术语“任务”来指代进程和线程。
“没有实现流程或线程的内部结构,而是有一个struct task_struct描述了一个称为task的抽象调度单元”
同样,根据Linus Torvalds的观点,您根本不应该考虑进程与线程的关系,因为它太局限了,唯一的区别就是COE或执行上下文在“将地址空间与父级分开”或共享地址空间方面。实际上,他使用一个Web服务器示例来说明自己的观点。在这里(其中强烈建议你阅读)。
完全归功于Linux内核文档