进程、线程、协程
进程、线程、协程本质上都是对Task
的执行规划。
CPU快过IO设备,人类作为统治阶级要压榨你,不让CPU闲着, 于是一个任务(程序) 对应一个进程就出来了。CPU要雨露均沾的执行这些程序。
然后呢? 人们发现程序还有很多可以细分的任务, 于是 [多线程]的设计方式出来了。
多线程的实现方案实在是太成熟了, 以至于大部分操作系统的实现是一个进程至少有一个执行线程, 于是各种桌面软件服务器软件冒出来了。
接着呢? 人们发现像Web Server
这种东西, 完全是靠IO
嘛, Thread Per Message 完全可以一波流, 但创建和销毁 Thread 成本依旧很高, 于是[协程]这种东西也就又开始流行了。协程(Coroutine),也可以被称为微线程,是一种用户态内的上下文切换技术。简而言之,其实就是通过一个线程实现代码块相互切换执行
进程、线程、协程的目的或说原因都一样:多任务。
只不过进程太笨重了,所以搞出了线程;线程还是太笨重了,就搞出了协程。
概括的说,进程是系统级的多任务,进程的切换,会带来大量内存的加载,所以很可能导致缺页中断,然后读硬盘加载相应数据到内存中。
每个线程是 CPU 使用的一个基本单元,它包括线程 ID、程序计数器、寄存器组和堆栈。线程由于共享进程空间,所以就大大减少了进程切换的开销,但依然是一个系统调用,需要从用户空间切换到系统空间然后再切换回用户空间,同时有可能导致cpu将时间片切给其它进程。
协程不是系统级线程,很多时候协程被称为“轻量级线程”、“微线程”、“纤程(fiber)”等。
简单来说可以认为协程是线程里不同的函数,这些函数之间可以相互快速切换。
协程是由编程语言所支持的在用户空间内的多任务,简单的说就是同一个线程中,系统维护一个程序列表,某个程序阻塞了,如果有其它程序等待执行,则不切线程而直接恢复那个程序的执行上下文。
一句话总结:进程是资源分配的最小单位,线程是CPU调度的最小单位。协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
进程=火车,
线程=车厢
线程在进程下行进(单纯的车厢无法运行)
一个进程可以包含多个线程(一辆火车可以有多个车厢)
不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
内存管理
一个系统中,有很多进程,它们都会使用内存。为了确保内存不被别人使用,每个进程所能访问的内存都是圈好的。一人一份,谁也不干扰谁。
可以把进程看出一个资源的容器,为进程里的所有线程提供共享资源(内存)。
线程是计算机最小的调度和运行单位,线程作为进程的一部分,扮演的角色就是怎么利用中央处理器去运行代码。
这其中牵扯到的最重要资源的是中央处理器和其中的寄存器,和线程的栈(stack)。这里想强调的是,线程关注的是中央处理器的运行,而不是内存等资源的管理。当只有一个中央处理器的时候,进程中只需要一个线程就够了。随着多处理器的发展,一个进程中可以有多个线程,来并行的完成任务。
比如说,一个web服务器,在接受一个新的请求的时候,可以大动干戈的fork一个子进程去处理这个请求,也可以只在进程内部创建一个新的线程来处理。线程更加轻便一点。线程可以有很多,但他们并不会改变进程对内存(heap)等资源的管理,线程之间会共享这些资源。
从计算机操作系统的角度来看,进程和线程不是同一个层面上的概念,线程是进程的一部分,线程主抓中央处理器执行代码的过程,其余的资源的保护和管理由整个进程去完成。
举例说明
假设你经营着一家物业管理公司。
最初,业务量很小,事事都需要你亲力亲为。
给老张家修完暖气管道,立马再去老李家换电灯泡——这叫单线程,所有的工作都得顺序执行。
后来业务拓展了,你雇佣了几个工人,这样,你的物业公司就可以同时为多户人家提供服务了——这叫多线程,你是主线程。
工人们使用的工具,是物业管理公司提供的,这些工具由大家共享,并不专属于某一个人——这叫多线程资源共享。
工人们在工作中都需要管钳,可是管钳只有一把——这叫冲突。
解决冲突的办法有很多,比如排队等候、等同事用完后的微信通知等——这叫线程同步。
你给工人布置任务——这叫创建线程。之后你还得要告诉他,可以开始了,不然他会一直停在那儿不动——这叫启动线程(start)。
如果某个工人(线程)的工作非常重要,你(主线程)也许会亲自监工一段时间,如果不指定时间,则表示你会一直监工到该项工作完成——这叫线程参与(join)。
业务不忙的时候,你就在办公室喝喝茶。下班时间一到,你群发微信,所有的工人不管手头的工作是否完成,都立马撂下工具,跟你走人。因此如果有必要,你得避免不要在工人正忙着的时候发下班的通知——这叫线程守护属性设置和管理(daemon)。
再后来,你的公司规模扩大了,同时为很多生活社区服务,你在每个生活社区设置了分公司,分公司由分公司经理管理,运营机制和你的总公司几乎一模一样——这叫多进程,总公司叫主进程,分公司叫子进程。
总公司和分公司,以及各个分公司之间,工具都是独立的,不能借用、混用——这叫进程间不能共享资源。
各个分公司之间可以通过专线电话联系——这叫管道。
各个分公司之间还可以通过公司公告栏交换信息——这叫进程间共享内存。
另外,各个分公司之间还有各种协同手段,以便完成更大规模的作业——这叫进程间同步。
分公司可以跟着总公司一起下班,也可以把当天的工作全部做完之后再下班——这叫守护进程设置。
线程的优点
-
响应性:如果一个交互程序采用多线程,那么即使部分阻塞或者执行冗长操作,它仍可以继续执行,从而增加对用户的响应程度。这对于用户界面设计尤其有用。例如,当用户点击一个按钮以便执行一个耗时操作时,想一想会发生什么事。一个单线程应用程序对用户反应会迟钝,直到该操作完成。与之相反,如果耗时操作在一个单独线程内执行,那么应用程序仍可响应用户。
-
资源共享:进程只能通过如共享内存和消息传递之类的技术共享资源。这些技术应由程序员显式地安排。不过,线程默认共享它们所属进程的内存和资源。代码和数据共享的优点是:它允许一个应用程序在同一地址空间内有多个不同活动线程。
-
经济:进程创建所需的内存和资源分配非常昂贵。由于线程能够共享它们所属进程的资源,所以创建和切换线程更加经济。虽然进程创建和管理与线程创建和管理的开销差异的实际测量较为困难,但是前者通常要比后者花费更多时间。例如,对于 Solaris,进程创建要比线程创建慢 30 倍,而且进程切换要比线程切换慢 5 倍。
-
可伸缩性:对于多处理器体系结构,多线程的优点更大,因为线程可在多处理核上并行运行。不管有多少可用 CPU,单线程进程只能运行在一个 CPU 上。
为什么需要协程?
我们都知道多线程,当需要同时执行多项任务的时候,就会采用多线程并发执行。
拿手机支付举例子,当收到付款信息的时候,需要查询数据库来判断余额是否充足,然后再进行付款。
假设最开始我们只有可怜的10个用户,收到10条付款消息之后,我们开启启动10个线程去查询数据库,由于用户量很少,结果马上就返回了。
第2天用户增加到了100人,你选择增加100个线程去查询数据库,等到第三天,你们加大了优惠力度,这时候有1000人同时在线付款,你按照之前的方法,继续采用1000个线程去查询数据库,并且隐隐觉察到有什么不对。
几天之后,见势头大好,运营部门开始不停的补贴消费券,展开了史无前例的大促销,你们的用户开始爆炸增长,这时候有10000人同时在线付款,你打算启动10000个线程来处理任务。
等等,问题来了,因为每个线程至少会占用4M的内存空间,10000个线程会消耗39G的内存,而服务器的内存配置只有区区8G,这时候你有2种选择,一是选择增加服务器,二是选择提高代码效率。
那么是否有方法能够提高效率呢?
我们知道操作系统在线程等待IO的时候,会阻塞当前线程,切换到其它线程,这样在当前线程等待IO的过程中,其它线程可以继续执行。
当系统线程较少的时候没有什么问题,但是当线程数量非常多的时候,却产生了问题:
-
一是系统线程会占用非常多的内存空间
-
二是过多的线程切换会占用大量的系统时间
协程刚好可以解决上述2个问题。
协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。
协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。
在有大量IO操作业务的情况下,我们采用协程替换线程,可以到达很好的效果,一是降低了系统内存,二是减少了系统切换开销,因此系统的性能也会提升。
在协程中尽量不要调用阻塞IO的方法,比如打印,读取文件,Socket接口等,除非改为异步调用的方式,并且协程只有在IO密集型的任务中才会发挥作用。
协程只有和异步IO结合起来才能发挥出最大的威力。
协程的特点
协程可以自动让出 CPU 时间片。
注意,不是当前线程让出 CPU 时间片,而是线程内的某个协程让出时间片供同线程内其他协程运行。
协程可以恢复 CPU 上下文。
当另一个协程继续执行时,其需要恢复 CPU 上下文环境。
协程有个管理者,管理者可以选择一个协程来运行,其他协程要么阻塞,要么ready,或者died。
运行中的协程将占有当前线程的所有计算资源。
进程,线程,协程区别
进程 | 线程 | 协程 | |
---|---|---|---|
概念 | 一段程序的执行过程,是资源分配的最小单位 | 操作系统(内核)运算调度的最小单位 | 用户态实现的运算调度单位,可暂停和恢复执行的过程(函数) |
独立资源 | 虚拟内存空间(代码段、数据段、堆栈等) -内核栈、thread_info、task_struct(pid,tgid等) - 寄存器组的值 | - 线程id(pid) - 堆栈 - 寄存器组的值 | - 栈 - 部分栈切换的寄存器 |
切换代价 | 高 | 中 | 低 |
通信(同步)方式 | 1. 管道/匿名管道 2. 有名管道 3. 信号 4. 消息队列 5. 共享内存 6. 信号量 7. 套接字 | 1. 锁机制 2. 信号量机制 3.信号机制 | Future,channel, pub/sub等 |
优点 | 1.进程间相互独立,一个进程出了问题不会影响其它进程 2. 可以利用多CPU的资源 | 1.线程之间共享内存和变量,通信比较方便 2. 程序逻辑和控制方式简单 3. 上下文切换资源消耗中等 | 1.无须原子操作锁定及同步的开销 2. 上下文切换资源小 3. 高并发性、高扩展性、低成本 |
缺点 | 1. 需要跨进程边界,当数据交流大是开销比较高 2. 创建、上下文切换开销大 | 1.线程之间的同步和加锁控制比较麻烦 2. 一个线程的崩溃可能影响到整个程序的稳定性 | 1. 不能使用多核资源 |
适用场景 | CPU密集型操作(如科学计算) | 1. 多核CPU 2. I/O密集操作(网络I/O,磁盘I/O) | 单核CPU I/O密集型操作(网络I/O,如秒杀系统,RPC服务器,即时通讯等) |
-
进程是具有一定独立功能的程序,关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位,上下文进程间的切换开销比较大。
-
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
-
协程是一种用户态的轻量级线程,协程的调度完全由用户控制,所以上下文的切换非常快。
线程进程都是同步机制(执行一个操作之后,等待结果,然后才继续执行后续的操作),而协程则是异步(执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作)。
协程与线程主要区别是它将不再被内核调度,协程是用户程序控制,线程是将自己交给内核调度。