一、上集回顾

在上一篇中我们主要研究了python的多线程困境,发现多核情况下由于GIL的存在,python的多线程程序无法发挥多线程该有的并行威力。在文章的结尾,我们提出如下需求: 既然python的多线程只是实现了并发功能,那么我们是否能够进一步的提升并发的能力,减小多线程的切换开销以及避免应对多线程复杂的同步问题?那么一个较好的解决方案就是我们本篇要介绍的协程技术。本篇仍然主要注重理论知识介绍,不着重讲python的协程代码实现。

大龙:Python线程、协程探究(1)——Python的多线程困境​zhuanlan.zhihu.com

首先我们放个图在这里,表示进程、线程、协程的关系,图片涉及的内容会后续介绍。

进程、线程、协程的关系

二、前景知识

协程并不是一个新的概念,事实上,协程的概念比线程提出来的还要早,协程涉及到的知识也不是新的知识,所以介绍协程之前,我们首先明确一些基础知识,包括并发和并行的概念以及了解线程调度的相关概念。

并发和并行,虚线和实线代表两个不同的任务

2.1 并发

计算机中每一个线程都是一个执行任务,假设我们现在有一个单核的CPU,CPU每时每刻只能调度执行一个线程,我们第一种做法就是让所有的线程排好队,一个任务一个任务的依次执行,执行完一个执行下一个。采用这种方式的调度带来的问题就是,如果当前执行的任务陷入了死循环,那么CPU会一直卡在这个任务上,导致后续的任务无法执行。所以,操作系统采用的方案是,每个任务分一个时间片来执行,时间片结束之后便切换任务,换另一个执行,做到雨露均沾。假设我们有4个任务,每个任务都分250ms进行计算,那么1s后,每个任务的拥有者都发现自己的任务往前进行了一点,这就是我们提到的并发(concurrency)。在POSIX中,并发的定义要求“延迟调用线程的函数不应该导致其他线程的无限期延迟”。我们上面的四个任务中,并发操作之间可能任意交错,对任务的拥有者来说,1s后四个任务都往前推进了一部分,好像四个任务是并行执行的,但是实际CPU执行任务的时候还是一个一个执行的,所以并发不代表操作同时进行。那么如果我有四个核心的CPU会怎么样呢,4个CPU核心会各自拿一个任务执行,这种情况才是我们常说的并行。

2.2 并行

并行只在多处理器的情况下才存在,因为每个处理器可以各自执行一个任务,这时四个任务便是并行执行的。单处理器的情况下是没办法做到并行的。所以我们回顾中会说,即使在多核的CPU计算资源情况下,python的多线程没有达到并行而只能达到并发,因为多个线程无法同时被执行,只能击鼓传花似的被依次的执行。

2.3 线程调度——上下文切换

线程上下文切换

前文提到,为了实现并发,我们需要让CPU交替切换的执行不同的任务,但当操作系统从thread1切换到thread2的时候,操作系统实际上打断了thread1的执行流程,那么下一次thread1重新被执行的时候,怎么能保证是继续上一次被打断的时候的位置继续执行的呢?所以切换的时候要保存任务的执行环境信息比如代码运行到哪一行了,哪些变量被赋值了,当时寄存器都是那些值等等。保存当前线程的执行环境信息,加载下一个线程的执行环境的操作就称为上下文切换。有了上下文切换,我们就不用担心任务被打断后会丢失一些执行信息导致下一次接着执行的时候出错。

2.4 线程调度——阻塞调用

当运行中的线程调用sleep操作时,被阻塞,操作系统调度其他程序,直到该线程获得唤醒信号

CPU是非常稀缺的计算资源,每一纳秒都是珍贵的,所以我们调度任务的目标就是让CPU不停的去计算,别让它空闲着。当线程A中的代码调用了文件读取操作时,会发生什么呢?

def 

由于存储的访问速度非常慢,CPU就会原地空转一直等着DMA把数据准备好,准备好了之后再往下执行。那么CPU等待的这段时间就完全被空闲浪费了,因为CPU等待的时候还有其他的任务迫切的需要任务计算。所以操作系统选择当线程A调用文件读取这样的阻塞操作的时候,就把线程A阻塞挂起,停止执行线程A,然后调度另一个线程继续执行,当线程A需要的数据准备好了之后,操作系统便会在未来的某个时刻调度线程A继续执行,如果线程A的数据始终都准备不好,那么线程A就永远不会被调度执行。

三、协程理解

协程是用户级的线程,是线程之上的轻量级线程

有了前面的基础知识,我们理解协程就会简单很多,事实上,协程本质就是用户态下的线程,进程里的线程的切换调度是由操作系统来负责的。但是线程内的协程的调度执行,是由线程来负责的。如果我们把协程对应到原生线程,那么协程所在的原生线程就是操作系统的角色。即原生线程需要负责什么时候切换协程,什么时候挂起协程。协程切换的时候,线程需要把协程A的执行环境进行保存,在下一次执行A的时候,线程需要恢复执行环境,这样就可以从A之前的位置继续执行。

用户线程即为协程,操作系统感知不到协程的存在,只调度内核线程

在这里我们需要提醒的是,多线程的使用是可以让一个程序获得更多的计算时间的,但是协程的使用不会, 多线程的使用在多核的情况下,可以达到并行的效果,但是协程的使用不会达到并行的效果。因为操作系统感知不到协程的存在,只会把时间片和CPU核心分给线程。至于分给线程的时间,线程又会分配给哪个协程来运行,那是线程自己决定的内容。比如分配2ms给一个拥有两个协程的线程A,线程被操作系统调度指派给了CPU核心C1, A会决定在C1运行哪个线程,,可以雨露均沾,让两个协程各自运行1ms, 也可以是把2ms全部分配给一个协程,自始至终,所有的协程都运行在CPU核心C1上,所以无法实现协程并行。

线程内部自主进行协程调度

那使用协程的好处是什么呢?提高线程的并发度,减小切换的开销,限于篇幅,这里就不展开讲,其结论就是,协程的切换只是线程栈内的切换操作,不涉及内核操作,其切换速度远快于线程。

如果我们要实现协程调度,我们该实现哪些功能呢。比如有一个线程底下有两个协程A,B,根据用户输入的文件名,A协程进行文件读取,并返回文件内容,B协程根据文件名计算哈希值并返回。

# 以下代码并非真实的python协程代码,只是为了说明例子

线程首先调度执行A,执行到文件读取部分发现需要等待,于是挂起协程A并切换到协程B执行。所以要实现调度协程,那么至少需要实现协程挂起操作协程恢复运行两个操作, 如果不想手动进行调度,那么可以实现一个中央的调度器来帮助进行调度。

四、协程的实现

协程主要有如下两个特点:

  • 协程可以保留运行时的状态数据
  • 协程可以出让自己的执行权,当重新获得执行权时从上一次暂停的位置继续执行

保留运行时状态数据就是上下文切换时做的工作,便于下一次执行时能继续上一次暂停的位置执行。协程出让执行权,指的是如果线程指定一个协程运行,除非该协程主动放弃执行权,不然线程无法将协程挂起切换。

Lua很早就有了语言级别对协程的实现,我个人觉得其协程API还是比较清晰的, 在这里简单介绍说明一下。

Lua中关于协程的API

五、Talk is cheap, show me the code

python的协程实现历史较为悠久,很多介绍协程的文章会从很早的协程库开始介绍,因为本篇博客更多专注于协程的概念理解,并不专注于python的协程技术实现,我们就直接从最新的协程代码编写方式开始介绍。

python3.4之后引入了asyncio模块,使得协程的使用更加的方便,其中关键词async表明这一块函数是一个协程块,而不是普通的函数模块(函数模块从中间退出之后,是不会保留运行环境的,但是协程会保留), await关键字表明协程主动出让执行权。我们定义三个协程模块,并让调度器进行调度执行A和B。首先调度运行协程B, 运行到sleep函数的时候遇到await关键字并出让执行权,这时调度器切换执行协程A,协程A执行又遇到await,再一次出让执行权。这时两个协程都在等待唤醒的信号。等待到了信号之后,两个协程被唤醒进而调度执行,然后运行结束。结果如下

import 

程序结果1:

协程B开始执行
协程B出让执行权
协程A开始执行
协程A出让执行权
协程B重新获得执行权,并执行结束
协程A重新获得执行权,并执行结束
程序运行时间: 2.002208709716797

此时我们加上第三个协程进行调度,这样当A、B等待时钟信号的时候我们在等待的期间,让调度器执行调度协程C,虽然协程C也调用sleep函数,但是由于睡眠时间短,所以很快又会被唤醒进行调度执行。当然了,由于协程C是死循环,所以协程A、B结束之后,会一直执行协程C。

import 

程序运行部分结果:

协程B开始执行
协程B出让执行权
协程A开始执行
协程A出让执行权
由于协程A,B始终等待时钟信号,协程C执行
由于协程A,B始终等待时钟信号,协程C执行
由于协程A,B始终等待时钟信号,协程C执行
由于协程A,B始终等待时钟信号,协程C执行
协程A重新获得执行权,并执行结束
协程B重新获得执行权,并执行结束

我们前面提到过,协程的两大特点,一是可以保存运行时环境,另一个便是可以主动出让执行权。那么假如有一个协程C始终不出让执行权,即在代码中,不用await关键字,那么其他协程是不是就没办法被执行了呢,很不幸的是,的确是这样的。我们看下代码

import 

程序运行结果

协程B开始执行
协程B出让执行权
协程A开始执行
协程A出让执行权
协程C不使用await关键字,故不选择出让执行权,所以继续执行C
协程C不使用await关键字,故不选择出让执行权,所以继续执行C
协程C不使用await关键字,故不选择出让执行权,所以继续执行C
协程C不使用await关键字,故不选择出让执行权,所以继续执行C
协程C不使用await关键字,故不选择出让执行权,所以继续执行C
协程C不使用await关键字,故不选择出让执行权,所以继续执行C
协程C不使用await关键字,故不选择出让执行权,所以继续执行C
协程C不使用await关键字,故不选择出让执行权,所以继续执行C
...

从结果中我们可以看到,B和A都主动出让了执行权,但由于C中虽然同样调用了sleep()函数,但是没有使用await关键字来出让执行权,所以始终C就被执行,永远轮不到A和B执行了。

六、总结

很多讲协程的博客都是从异步/同步的角度出发,但我始终觉得异步实际上无处不在,并不是只有协程才有的概念,协程说到底就是用户态下的线程,如果我们了解清楚线程,包括线程的上下文切换、线程的调度我们就能很好的理解协程。

七、后记

终于写完了这篇博客,为了写这篇,花了好久的时间去查资料,还顺便把本科的操作系统课的课件翻出来看了一遍。最大的感受就是想要把这个内容在一篇博客中尽可能的说清楚,真的有点难,因为涉及到的内容太多了,上文中还有许多的概念和结论没有展开说,但是限于篇幅,只能日后有需要再进行展开介绍了。不管怎么说,这个flag,算是拔掉了。

python 协程可以嵌套协程吗_Python线程、协程探究(2)——揭开协程的神秘面纱...相关推荐

  1. python 中主线程结束 子线程还在运行么_python 线程之一:线程的创建、启动及运行方式

    threading:这个模块在较低级的模块 _thread 基础上建立较高级的线程接口 以后我们就用 threading 模块来管理线程就可以了. Tread 类:控制线程创建.启动及运行方式 一.线 ...

  2. python 协程可以嵌套协程吗_Python | 详解Python中的协程,为什么说它的底层是生成器?...

    今天是Python专题的第26篇文章,我们来聊聊Python当中的协程. 我们曾经在golang关于goroutine的文章当中简单介绍过协程的概念,我们再来简单review一下.协程又称为是微线程, ...

  3. python 协程 多线程_python进阶之多线程(简单介绍协程)

    多线程 线程:实现多任务的另一种方式 一个进程中,也经常需要同时做多件事,就需要同时运行多个'子任务',这些子任务,就是线程 线程又被称为轻量级进程(lightweight process),是更小的 ...

  4. python线程协程进程的区别_进程和线程、协程的区别

    现在多进程多线程已经是老生常谈了,协程也在最近几年流行起来.python中有协程库gevent,py web框架tornado中也用了gevent封装好的协程.本文主要介绍进程.线程和协程三者之间的区 ...

  5. 进程池、线程池、回调函数、协程

    阅读目录 摘要: 进程池与线程池 同步调用和异步调用 回调函数 协程 一.进程池与线程池: 1.池的概念: 不管是线程还是进程,都不能无限制的开下去,总会消耗和占用资源. 也就是说,硬件的承载能力是有 ...

  6. 线程queue、事件event及协程

    线程queue.事件event及协程 线程queue 多线程抢占资源,让其保持串行的两种方式: ​ 1.互斥锁 ​ 2.队列 线程队列分为以下三种: 1.Queue(先进先出) import queu ...

  7. day10-Python学习笔记(二十三)线程池,unittest参数化,协程

    线程池,unittest参数化,协程 python的多线程只能利用cpu的一个核心,一个核心同时只能运行一个任务那么为什么你使用多线程的时候,它的确是比单线程快答:如果是一个计算为主的程序(专业一点称 ...

  8. linux 线程切换开销,协程 用户级(内核级)线程 切换开销 协程与异步回调的差异...

    今天先是看到多线程级别的内容,然后又看到协程的内容. 基本的领会是,协程是对异步回调方式的一种变换,同样是在一个线程内,协程通过主动放弃时间片交由其他协程执行来协作,故名协程. 而协程很早就有了,那时 ...

  9. 并发编程之进程池,线程池 和 异步回调,协程

    1.进程池和线程池 2.异步回调 3.协程 4.基于TCP使用多线程实现高并发 一.进程池和线程池 什么是进程池和线程池: ''' 池 Pool 指的是一个容器 线程池就是用来存储线程对象的 容器创建 ...

最新文章

  1. 我的Android进阶之旅------gt;Java全角半角的转换方法
  2. redis相比memcached有哪些优势?
  3. 生态篇-HBase 生态介绍
  4. 计算机专业技术人员工作总结,计算机教师专业技术年终工作总结及计划范文模板.docx...
  5. 正经炼丹师如何完美安排国庆长假?| 假期专属论文清单
  6. 38行代码AC——UVA-167The Sultan‘s Successors(八皇后问题,附视频讲解)
  7. python之_init_函数的简介
  8. jQuery 鼠标滚轮插件应用 mousewheel
  9. mysql先删后增并发时出现死锁_MySQL死锁案例分析一(先delete,再insert,导致死锁)...
  10. NHibernate3.0剖析:Query篇之NHibernate.Linq标准查询
  11. java nvarchar max_sql server中使用nvarchar(MAX)代替ntext
  12. java日志系统简介: 从tomcat大量打印debug日志说起
  13. DFA敏感词过滤算法详解
  14. oracle实时异地同步,异地Oracle数据库数据同步
  15. python demo.py_pythonDemo.py
  16. 区块链挑战传统支付体系
  17. FPGA学习笔记——计数器
  18. uboot移植——启动内核
  19. 偏微分方程简明教程第三章部分答案
  20. 强化学习通俗导论(一):什么是强化学习

热门文章

  1. java api集合,javaAPI_集合基础_集合中常见操作示例
  2. c语言 生成大素数,C语言实现寻找大素数
  3. web服务器的文档根目录,web服务器根目录中
  4. 9中继器添加一列序号自增_三个动态自动更新EXCEL序号的小技巧,解决重复编号困扰...
  5. linux 指定库名 登录mysql_数据库学习笔记之MySQL(01)
  6. Python关于装饰器的练习题
  7. Python基础教程:repr()与str() 的区别
  8. python 中文件输入输出及os模块对文件系统的操作
  9. RS(纠删码)技术浅析及Python实现
  10. Python强大的格式化format