协程1 --- 发展历史

文章目录

  • 一个编译器问题
    • 背景
    • 解决
  • 协程为什么一开始没发展成一等公民?
    • 自顶向下、逐步求精(Top-down, stepwise refinement)
    • 线程的出现
  • 协程的雄起
    • IO密集型
    • 同步语义实现异步
    • 发展史
  • 线程和协程的关系
    • 并发性
    • 调度方式
    • 资源占用

一个编译器问题

协程概念的出现比线程更早,可以追溯到20世纪50年代的一个问题:COBOL编译器无法做到读一次磁带就可以完成整个编译过程

背景

编译器的基本步骤包括:读取字符流、词法分析、语法分析、语义分析、代码生成器、代码优化器等,上一步的输出作为下一步的输入。

  • COBOL程序被写在一个磁带上,而磁带不支持随机读写(fopen指针不能fseek)。
  • 计算机内存小又不可能把整个磁带的内容都装进去,所以一次读取没编译完就要再从头读。

解决

将词法分析和语法分析合作运行,而不再像其他编译器那样相互独立,两个模块交织运行,编译器的控制流在词法分析和语法分析之间来回切换:

  • 当词法分析模块基于词素产生足够多的词法单元Token时就控制流转给语法分析
  • 当语法分析模块处理完所有的词法单元Token时将控制流转给词法分析模块
  • 词法分析和语法分析各自维护自身的运行状态,并且具备主动让出和恢复的能力

在这里插入图片描述

协程为什么一开始没发展成一等公民?

自顶向下、逐步求精(Top-down, stepwise refinement)

自顶向下的思想:对要完成的任务进行分解,先对最高层次中的问题进行定义、设计、编程和测试,而将其中未解决的问题作为一个子任务放到下一层次中去解决。这样逐层、逐个地进行定义、设计、编程和测试,直到所有层次上的问题均由实用程序来解决,就能设计出具有层次结构的程序。

然而协程这种相互协作调度的思想和自顶向下是不合的,在协程中各个模块之间存在很大的耦合关系,不符合高内聚低耦合的编程思想,相比之下自顶向下的设计思想使程序结构清晰、层次调度明确,代码可读性和维护性都很不错。

线程的出现

抢占式的线程可以解决大部分的问题,也就是说协程能干的线程干得也不错,线程干的不好的地方,使用者暂时也可以接受。(抢占式任务系统依赖于CPU硬件的支持,对硬件要求比较高,对于一些嵌入式设备来说,协同调度再合适不过了,所以协程在另外一个领域也施展了拳脚。)

协程的雄起

技术、思想等发展很大程度受时代和场景的影响

IO密集型

对于CPU来说,任务分为两大类:计算密集型和IO密集型。
IO密集型提高CPU有效利用率一直是个难点。
在抢占式调度中也有对应的解决方案:异步+回调
整个过程相比同步IO来说,原来整体的逻辑被拆分为好几个部分,各个子部分有状态的迁移,逻辑复杂,bug肯定多。

同步语义实现异步

前端经典的回调地狱:
在这里插入图片描述
例子:

<script>
    function doSomething1() {
        return 'doSomething1'
    }
    function doSomething2() {
        return 'doSomething2'
    }
    function doSomething3() {
        return 'doSomething3'
    }

    setTimeout(() => {
        console.log(doSomething1())
        setTimeout(() => {
            console.log(doSomething2())
            setTimeout(() => {
                console.log(doSomething3())
            }, 1000)
        }, 1000)
    }, 1000)
</script>

用同步的写法实现异步效果:

<script>
    function doSomething1() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('doSomething1')
            }, 1000)
        })
    }
    function doSomething2() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('doSomething2')
            }, 1000)
        })
    }
    function doSomething3() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('doSomething3')
            }, 1000)
        })
    }

    (async () => {
        console.log(await doSomething1())
        console.log(await doSomething2())
        console.log(await doSomething3())
    })();
</script>

发展史

在这里插入图片描述

  • 1966 年,线程(thread)的概念被提出。
  • 1968 年,Dijkstra 发表论文《GOTO 语句是有害的》,结构化编程的理念深入人心,自顶向下的程序设计思想成为主流,协程“跳来跳去”的执行行为类似 goto 语句,违背自顶向下的设计思想。
  • 1979 年,Marlin 提交博士论文 Coroutines : A Programming Methodology, A Language Design, and An Implementation,是协程理论的集大成之作。
  • 1980 年及之后的 20 余年,多线程成为并发编程的代名词,抢占式击败协作式成为主流的调度方式,协程逐渐淡出主流编程语言舞台。
  • 2003 年,Lua v5.0 版本开始支持协程。
  • 2005 年,Python 开始支持生成器和 yield/send 关键字,之后数年一直在演化。
  • 2009 年,Go 语言问世,以 Goroutine 的方式支持并发编程,一代传奇拉开序幕。
  • 2012 年,C# 开始支持 async 函数和 await 表达式,标志着协程王者归来。
  • 2015 年,Python 支持 async/await 语法。
  • 2017 年,async/await 纳入 ES2017 标准。
  • 2017 年,Kotlin 另辟蹊径,以 suspend 关键字的形式实现了协程。
  • 2019 年,Dart 支持 Future、async/await 语法。
  • 2020 年,C++ 20 支持 co_async/co_await。
  • 2022 年 3 月,JDK 19 预览版(Early-Access)中引入了一种新的并发编程模型(织布机计划)——虚拟线程,非最终版,可能随时被删除。

线程和协程的关系

协程和线程并非矛盾,协程的威力在于IO的处理,恰好这部分是线程的软肋,由对立转换为合作才能开辟新局面。

从进程到线程再到协程,我们对于CPU的压榨从未停止
linux2.6之后的线程切换耗时大概是在几微秒,协程大概是在100纳秒左右。

并发性

线程在多核的环境下是能做到真正意义上的并行执行的,而协程是为并发而生的。

打个简单的比方,射雕英雄传中周伯通教郭靖一手画圆,一手画方,两只手同时操作,这个就是并行。普通人大概率不能并行,却可以并发,你先左手画一笔,然后右手画一笔,同一时候只有一只手在操作,来回交替,直到完成两个图案,这就是并发,协程主要的功能。

在这里插入图片描述

调度方式

  • 线程 – 抢占式调度,操作系统内核调度,切换涉及到内核态和用户态的转换,开销较大。
  • 协程 – 协作式调度,由程序自身控制,切换只需要保存和恢复协程的上下文,开销较小。

协程的控制可能造成某些协程的饥饿,抢占式更加公平
协程的控制权由用户态决定可能转移给某些恶意的代码,抢占式由操作系统来调度更加安全

资源占用

  • 线程 – 有自己独立的栈空间(linux默认8MB),TCB等
  • 协程 – 栈空间可以根据需要动态调整,共享线程的内核资源,一般设置的128K
上一篇:灰度梯度的表示形式、非极大值抑制、Canny算子、otsu


下一篇:线性数据结构之栈结构