一、基础:协程

一、简介

​ 随着互联网的高速发展,用户量也发生了高速的增长。串行模型演变到并发模型,后台的服务框架也是从单体服务,演变成多机服务。后台服务也一直在解决一个问题,如何用最少的服务资源去处理更多的请求

​ 想要提高资源的利用率,单一提高CPU利用率,或者单一提高IO的利用率都没有用,只有同时提升两者的利用率才会提升资源利用率。

方案:多线程,多进程,异步IO,还有协程。

进程、线程是内核级别的。但是协程对于内核来说是无感知的,完全由用户自己控制,所以也称之为用户级线程,因为不被内核感知,无法做到CPU的强制切换,(例如:进程、线程间切换)​ ,只能自己主动交出控制权。

二、为什么使用协程?

2.1、线程

​ 线程是由操作系统内核控制的,如果使用多线程处理高并发场景,一个线程线程处理一个Socket,会导致大量的线程切换,线程的创建比较耗时,并且线程需要预先分配几M的栈空间,线程间上下文切换与调度消耗很多资源,例如从用户态切换到内核态,保存当前线程环境,加载目标线程的执行环境,在从内核态切换到用户态,性能可能反而降低,使用基于事件的异步编程可以解决这种问题。但是相应的提高开发上的复杂度。

2.2、协程

​ 协程相对于线程轻量很多,创建或者切换协程仅相当于一次函数调用,每个协程只需几十个字节就可以保存状态,相对线程栈低很多,协程是基于线程的,可以理解为在用户层模拟线程操作。每创建一个协程,都与一个内核态线程动态绑定,用户态下实现调度,切换,真正执行任务的还是内核线程,因为协程是在用户级别完成调度,切换的,对于内核来说是无感知的,所以内核的调度是基于用户进程的,也就避免了线程间的切换问题,大量节省资源。很多协程库都是这种模式。

2.3、小结

​ 这种在用户态模拟的线程模型并非真正的实现并发。如果因某些操作,导致阻塞,比如IO阻塞,那么整个进程就会被内核挂起。很多协程库会把阻塞操作,改成非阻塞操作。在阻塞的点上主动让出自己,通知其他协程执行。

三、协程实现

很多业务后台都是面向密集IO任务的。

1
2
3
4
5
6
处理并发模型:
- 监听线程收到请求。
- 监听线程将请求让如队列。
- 监听线程继续监听
- 工作线程从队列中取出请求,处理。
- 回包

对于协程来说是如何完场上面模型的呢?

3.1、协程执行

​ 每个线程拥有自己的线程函数,相应,每个协程都拥有一个自己的协程函数。线程函数每次从函数从第一句开始执行,参数,局部变量等都保留在线程栈上,函数返回,清空栈信息。但是协程函数所有信息保留在堆中,协程函数第一次执行,在堆中动态分配一个协程上下文,其中包含,局部变量,参数,以及交出控制权的位置,交出控制权后,堆中信息不会被删除,下一次啊执行会从堆中恢复上下文信息。协程函数结束,对应清空堆中信息。

​ 之前提过协程是基于线程的,在用户层面上维护一个数据结构与多个线程(线程池)。协程函数在一个队列中维护,多个线程(线程池)从队列中取出协程函数执行。

3.2、协程调度

​ 协程没有cpu权限, 无法使用cpu去完成调度。那么协程如何实现调度的呢?

3.2.1、有栈协程

​ 创建大量协程,这些协程绑定在一个线程上(主协程)。并且每一个协程保留一个私有栈。协程执行到异步IO处,交出控制权给主协程,由主协程进行调度分配。

  • 处于同一线程,协程间不存在竞态关系。
  • 带有协程栈,所以可以再任意节点交出控制权

有栈协程保存调用栈信息空间消耗太大,空间使用共享栈来解决这种问题,但是也会带来相应的copy问题。

3.2.2、无栈协程

​ 封装系统异步IO函数作为协程函数,协程一旦发起异步IO操作后,保留当前信息,控制权交付当前执行函数,线程去队列中拉取另一个协程函数执行。异步IO完成后重新获取控制权,恢复上下文环境继续执行之前协程函数。

  • 只有在异步IO操作是才能交出控制权。
  • 无需住协程调度
  • 线程A执行异步协程B的IO操作,协程B交出控制权,线程C执行,此时协程B恢复操作。因为B不在线程C中。所以可能存在竞态关系。

无栈协程需要标准语言与编译器支持。

四、总结

线程在调度上肯定是优胜于协程的,毕竟是内核调度的。但是目前在互联网高速发展的过程中,主要需要解决的问题就是,如何用最少的服务资源去处理更多的请求?

线程的开销基本上都是MB级别的,协程的开销只是KB级别的。所以在书需求上,协程更适合现在场景的应用。