由于简历上写了一个有关协程的项目,结果面试的时候疯狂被问协程(然而我不太懂呜呜呜),直接就被问烂了…
下面再次捋一捋进程、线程、协程。
(以下是个人理解,欢迎指正。)
首先说说程序,程序是一些保存在磁盘上的指令的有序集合,程序是静态的。
而进程是程序执行的过程,一个程序运行起来就是一个进程,进程是资源分配的最小单位;
还是得从CPU的视角来看,在早期只能处理一个任务,而这个任务里面既有消耗CPU的操作(比如计算),又有不怎么需要CPU且很耗时的操作(比如IO);
那么这样单个进程执行单个任务的话对于CPU来说就非常浪费了,因为当执行到IO时,进程阻塞了,CPU没事干;
对此就提出了多任务、多进程的方式,这样当遇到类似IO等操作时,我们就可以转到其他进程上面,这样就能较好的利用CPU;
也就是说多进程的出现是为了更好的利用CPU的资源;
由于是多进程,而CPU又没那么多,为了照顾到所有的进程,因此必然涉及到调度,以及调度的方式;
那么进程在切换时必然就需要保存其上下文,进程在切换时需要进行下面的操作:
(1) 切换页目录(页表)以使用新的地址空间;(这里有快表TLB,切换后TLB失效,导致进程运行时效率降低)
(2) 切换内核栈和硬件上下文;
上面两点也是进程切换时的开销;
除此之外,多个进程之间并不是相互独立的,当进程间相互依赖时,就需要进行进程间的通信;
由于进程间的地址空间是相互独立的,因此进程间的通信也比较麻烦了;
那么针对进程在多任务时切换的开销以及通信问题,有没有什么解决方法?
有!那就是减小进程的粒度,此时就引入的线程的概念。
对比进程,线程可以理解成是进程中的一条执行流程,也就是一个进程中可以有多个线程;
并且让进程仍然是资源分配的最小单位,也就是说一个进程中的线程就能共享进程的资源了;
那么同一进程中的线程到底独占什么资源?又共享什么资源了?
线程本质上其实是进程的函数执行,而一个函数在进程的虚拟内存分区中就对应着一个栈帧;
因此线程其实就独占了这个栈帧,除此之外还有程序计数器、栈指针以及函数运行时用到的寄存器;
这些统称为线程的上下文,其余的虚拟内存都是贡献的(代码段,全局区,堆,自由储存区,部分的栈区);
如此一来同一进程下的线程就能方便进行通信,那么这样就解决了进程间通信麻烦的问题;
于此同时,我们让线程称为CPU调度的最小单位,再加上线程占有的私有东西少,这样线程的切换的开销也就没那么大了;
到此为止,线程能完美的解决所有问题。
这里先提一下,调度方式主要有两种(当然现在操作系统肯定是做了权衡的):
抢占式多任务(抢占式调度):被迫的放弃CPU资源,比如时间片用完了。
协作式多任务(非抢占式调度):自愿的放弃CPU资源,必须等一个进程主动让出执行权,其它进程才有机会执行。
而早期的调度方式其实是协作式多任务的,这样有一个很大的弊端:
如果当前进程不让(比如陷入死循环、比如调用非阻塞API循环死等总是不来的网络报文、比如用错误的接口循环死等读取硬件故障的磁盘),那么整个系统就会陷入瘫痪。
因此从Windows 95开始,协作式多任务换到了抢夺式多任务,现在操作系统大部分也是抢占式多任务方式(但是肯定有优化)
随着我们对高并发的追求,线程似乎并不满足我们的需求了,主要有下面这些原因:
首先是线程安全问题,由于线程共享资源,所以当多个线程访问同一资源时,需要通过加锁来实现线程同步,加锁的话也需要开销,同时还有死锁问题;
这里说说为什么会出现线程安全问题:其实本质上是我们和CPU的视角不同,我们看到的是高级语言的一条语句,
CPU看到的其实是机器指令,再网上讲可以是汇编指令;而一条高级语言的语句往往对应着好几条汇编指令;
那么当CPU在执行同一语句对应的几条汇编指令时,如果被中断了,切换到其他线程上去执行时就会出现数据错乱的问题;
接着,虽然线程的开销比进程小,但是在处理IO密集型高并发时,线程多起来切换的开销还是比较大的,而且一个进程中的线程个数也是有限的。
这里当然可以用异步io来解决,但是写起来就是太复杂了(epoll的话本质上还是同步的)
同时,由于是抢占式多任务,因此对于用户来说,我们并不知道程序会在什么时候被切换,这样的话有时就无法达到我们想要的目的(比如:无法更有效的规划数据访问)
个人认为本质上还是操作系统在控制调度,我们无法预知调度时期,导致无法实现我们想要的调度效果来实现高效率。
那么为了解决这些问题,就引入了协程的概念;
协程是比线程更小的概念,可以说协程和进程、线程根本不是在一个维度上的;
协程不能让操作系统知道,否则就会又变得不可控了。
协程让控制权重新回到了程序员的视线,使得可以由我们来控制协程的调度,也就是重新转变成协作式多任务的调度方式;
由于操作系统不知道协程,所以协程不是操作系统的调度单位,也就是说协程必须依赖于进程或者线程;
进一步的讲,一个线程中的协程共用一个CPU资源,因此一个线程中的协程一定是串行操作;
那协程怎么实现呢?协程通过函数来实现。
由于要实现我们想要的目的,也就是要在特定的时机调度协程,对应这协程的挂起和唤醒,那么也就是说我们要实现可以中断,唤醒的函数;
因此也可以说协程是一个可以挂起,可以唤醒的函数。这样就能达到可控。
那么协程对比线程有什么优势呢?
首先,协程的粒度更小,本质上就是一个函数,所以切换时保存栈帧信息就行,不用保存整个线程上下文;
由于协程占用的资源少了,因此一个进程、线程可以支持的协程数就多了。
然后,由于操作系统并不知道协程的存在,协程完全是由用户态自己控制的,这样有个好处就是在切换的时候并不用通过系统调用从用户态切换到内核态,而进程线程的切换是需要系统调用的,因此协程更快。
于此同时,正是因为协程不可被操作系统调度,即我们看到的和调度器看到的其实是一个维度(都是高级语言的语句),因此不会有数据错乱的问题(有的话也是我们程序员的问题)
接着,协程的调度是可控的,我们程序员可以设计调度方式来达到我们想要的效果;
还有一个重要的点,在应对IO密集型时,协程有很大的优势;
那么有人问,由于协程是依赖于线程的,那一个线程内的一个协程IO阻塞了,那不是整个线程都阻塞了吗?
没错,但是上面提到可以用异步的方式来解决,协程也雀氏是这样做的:
当一个线程中协程遇到IO时,我们可以通过异步IO的方式,立即返回后将协程挂起,切换到其他协程上工作,这样就能充分利用一个线程中的CPU资源;
当IO完成后,我们可以通过回调的方式去唤醒对应的被挂起的协程。
而为了不对代码进行大改动,以及方便的使用异步,可以说协程封装了异步操作,使得我们可以以同步调用的方式实现异步编程。
协程还有个特点:无法利用多核CPU 或者说 无法在CPU并行;
个人认为得从两个角度看这个特点:
一方面,它是优点:
正式由于CPU不能直接调度协程,这也使得协程在我们的调度下是可控的,是协作式多任务模式,能够达到我们想要的结果。
另一方面,它是缺点:
由于协程依赖于线程,所以同一线程中的协程无法利用多核CPU的特性,因此在进行计算密集型的任务时是不占优势的(不如多线程)。
C++ 协程
也来谈谈协程
干货 | 进程、线程、协程 10 张图讲明白了!
说说对协程的看法
有了线程,为什么还要有协程?
进程,线程和协程切换
程序员应如何理解高并发中的协程
线程间到底共享了哪些进程资源?