3367 字
17 分钟
第16章 协程
2020-03-30

第16章 协程#

[toc]

进程 VS 线程#

进程和线程说明

  • 是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位

  • 线程是进程的一个执行实例,是程序执行的最小单位,它是比进程更小的能独立运行的基本单位

  • 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行

  • 一个程序至少有一个进程,一个进程至少有一个线程

可以联想 打开迅雷.exe 就是打开进程 多个任务 就是多个线程**

进程#

进程:一个运行的程序(代码)就是一个进程,没有运行的代码叫程序,进程是系统资源分配的最小单位,进程拥有自己独立的内存空间,所有进程间数据不共享,开销大。

进程: 1、操作系统进行资源分配和调度的基本单位,多个进程之间相互独立 2、稳定性好,如果一个进程崩溃,不影响其他进程,但是进程消耗资源大,开启的进程数量有限制

进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。

怎么得出来这结论的?

堆栈#

什么是堆栈?在计算机中堆栈的概念分为:数据结构的堆栈和内存分配中堆栈


#### 数据结构的堆栈

堆:堆可以被看成是一棵树,如:堆排序。在队列中,调度程序反复提取队列中第一个作业并运行,因为实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。

栈:一种先进后出的数据结构。

####内存分配中的堆和栈 内存分配中的堆和栈 栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

堆栈缓存方式 栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。 堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

线程#

线程: cpu调度执行的最小单位,也叫执行路径,不能独立存在,依赖进程存在,一个进程至少有一个线程,叫主线程,而多个线程共享内存(数据共享,共享全局变量),从而极大地提高了程序的运行效率。

线程: 1、CPU进行资源分配和调度的基本单位,线程是进程的一部分,是比进程更小的能独立运行的基本单位,一个进程下的多个线程可以共享该进程的所有资源 2、如果IO操作密集,则可以多线程运行效率高,缺点是如果一个线程崩溃,都会造成进程的崩溃

线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。

线程安全#

因为线程间共享进程中的全局变量,所以当其他线程改变了共享的变量时,可能会对本线程产生影响。

所谓线程安全的约束是指一个函数被多个并发线程反复调用时,要一直产生正确的结果。要保证线程安全,主要是通过加锁的方式保证共享变量的正确访问。

换句话说, 线程安全 是在多线程的环境下, 线程安全能够保证多个线程同时执行时程序依旧运行正确, 而且要保证对于共享的数据,可以由多个线程存取,但是同一时刻只能有一个线程进行存取.

既然,多线程环境下必须存在资源的竞争,那么如何才能保证同一时刻只有一个线程对共享资源进行存取?

加锁可以保证存取操作的唯一性, 从而保证同一时刻只有一个线程对共享数据存取.

通常加锁也有2种不同的粒度的锁:

  1. fine-grained(所谓的细粒度), 那么程序员需要自行地加,解锁来保证线程安全
  2. coarse-grained(所谓的粗粒度), 那么语言层面本身维护着一个全局的锁机制,用来保证线程安全

前一种方式比较典型的是 java, Jython 等, 后一种方式比较典型的是 CPython (即Python). 至于Python中的全局锁机制,也即 GIL (Global Interpreter Lock)

互斥锁#

每个对象都对应于一个可称为’互斥锁‘的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

同一进程中的多线程之间是共享系统资源的,

多个线程同时对一个对象进行操作,一个线程操作尚未结束,另一线程已经对其进行操作,导致最终结果出现错误,此时需要对被操作对象添加互斥锁,保证每个线程对该对象的操作都得到正确的结果。

阻塞VS非阻塞#

阻塞#

阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的。

常见的阻塞形式有:网络 I/O 阻塞、磁盘 I/O 阻塞、用户输入阻塞等。

阻塞是无处不在的,包括 CPU 切换上下文时,所有的进程都无法真正干事情,它们也会被阻塞。 如果是多核 CPU 则正在执行上下文切换操作的核不可被利用。

非阻塞#

程序在等待某操作过程中,自身不被阻塞,可以继续运行干别的事情,则称该程序在该操作上是非阻塞的。

非阻塞并不是在任何程序级别、任何情况下都可以存在的。

仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。

非阻塞的存在是因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。

同步VS异步#

同步#

多个任务之间有先后顺序执行,一个执行完下个才能执行

异步#

多个任务之间没有先后顺序,可以同时执行,有时候一个任务可能要在必要的时候获取另一个同时执行的任务的结果,这个就叫回调

区别#

同步异步相对于多任务而言,阻塞非阻塞相对于代码执行而言。

并发 VS并行#

1、多线程程序在单核上运行,就是并发
多线程,看起来一起执行,GIL在同一时刻限制了多个线程只能有一个线程被CPU执行

2、多线程程序在多核上运行,就是并行 多进程,多个进程在同一时刻可以占用多个CPU


举个例子: 1、并发: 因为是在一个cpu上,比如有十个线程,每个线程进行10毫秒(进行轮询操作) 从人的角度看,好像这10个线程都在运行,但是从微观上看,在某个时间点看, 其实只有一个线程在执行,这就是并发

2.并行:因为在多个CPU上(比如10个CPU),比如有10个线程,每个线程执行10毫秒(各自在不同CPU上执行), 从人的角度看,好像这10个线程都在运行,但是从微观上看,在某个时间点看, 也同时有10个线程在执行,这就是并行

协程#

协程: 是一种用户态的轻量级线程,协程的调度完全由用户控制。

协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

协程拥有自己的寄存器上下文和栈。

协程调度时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操中栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

简单点说协程是进程和线程的升级版,进程和线程都面临着内核态和用户态的切换问题而耗费许多切换时间,而协程就是用户自己控制切换的时机,不再需要陷入系统的内核态.

能够在一个线程中实现并发的概念 能够规避在一些任务中的IO操作 在任务的执行过程中,检测到IO就切换到其他任务 协程在一个线程上 提高CPU的利用率 协程相比与多线程优势 切换的效率更快

应用场景: 爬虫的例子,请求过程中的IO等待

from gevent import monkey
monkey.patch_all()
import gevent
from urllib.request import urlopen
def get_url(url):
response = urlopen(url)
content = response.read().decode('utf-8')
return len(content)
g1 = gevent.spawn(get_url, 'http://www.baidu.com')
g2 = gevent.spawn(get_url, 'http://www.taobao.com')
g3 = gevent.spawn(get_url, 'http://www.hao123.com')
gevent.joinall([g1, g2, g3])
print(g1.value)
print(g2.value)
print(g3.value)

运行结果:

156535
133524
298483

可以看到结果是同时显示出来的


Python里最常见的yield就是协程的思想

举个例子:

import threading
class Thread(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
def run(self):
for i in range(10):
print(self.name)
threadA = Thread("A")
threadB = Thread("B")
threadA.start()
threadB.start()

返回结果是:

A
A
A
B
B
B
B
B
B
B
B
B
BA
A
A
A
A
A
A

那么总共发生了 20 次切换:主线程 -> A -> B -> A -> B …

使用协程方式:

import greenlet
def run(name, nextGreenlets):
for i in range(10):
print(name)
if nextGreenlets:
nextGreenlets.pop(0).switch(chr(ord(name) + 1), nextGreenlets)
greenletA = greenlet.greenlet(run)
greenletB = greenlet.greenlet(run)
greenletA.switch('A', [greenletB])

返回结果:

A
B
B
B
B
B
B
B
B
B
B

此时发生了 2 次切换:主协程 -> A -> B

yield例子:

def consumer():
print(123)
while True:
x = yield
print("处理了数据:", x)
def producer():
c = consumer()
next(c)
for i in range(10):
print("生产了数据:", i)
c.send(i)
producer()

greenlet#

greenlet 是一个轻量级的协程实现,使用的方法简单而清晰。创建 greenlet 实例执行方法,在方法内部可通过 greenlet.switch() 切换至其他 greenlet 实例进行执行

例子:

from greenlet import greenlet
def eat():
print("eating start")
g2.switch()
print("eating end")
g2.switch()
def play():
print("playing start")
g1.switch()
print("playing end")
g1 = greenlet(eat)
g2 = greenlet(play)
g1.switch()

运行结果:

eating start
playing start
eating end
playing end

gevent#

gevent 基于 greenlet 库进行了封装,基于 libev 和 libuv 提供了高效的同步API。

对 greenlet 在业务开发中的不便之处,提供了很好的解决方案:

对于 greenlet 实例的管理,不使用树形关系进行组织,隐藏不必要的复杂性

因为协程还是运行在一个OS进程中,所以协程不能跑阻塞任务,否则就要将整个OS进程阻塞住了。

采用 monkey patching 与第三方库协作,将阻塞任务变成非阻塞,也不需要手工通过greenlet.switch() 切换;

例子:

import gevent
def foo():
print('Running in foo')
gevent.sleep(0)
print('Explicit context switch to foo again')
def bar():
print('Explicit context to bar')
gevent.sleep(0)
print('Implicit context switch back to bar')
gevent.joinall([
gevent.spawn(foo),
gevent.spawn(bar),
])

运行结果:

Running in foo
Explicit context to bar
Explicit context switch to foo again
Implicit context switch back to bar

通过这个例子可以看到 两个上下文通过调用 gevent.sleep()来互相切换。

再看一个例子:

import gevent
import random
def task(pid):
"""
Some non-deterministic task
"""
gevent.sleep(random.randint(0, 2) * 0.001)
print('Task', pid, 'done')
def synchronous():
for i in range(1, 10):
task(i)
def asynchronous():
threads = [gevent.spawn(task, i) for i in range(10)]
gevent.joinall(threads)
print('Synchronous:')
synchronous()
print('Asynchronous:')
asynchronous()

运行结果是:

Synchronous:
Task 1 done
Task 2 done
Task 3 done
Task 4 done
Task 5 done
Task 6 done
Task 7 done
Task 8 done
Task 9 done
Asynchronous:
Task 0 done
Task 4 done
Task 5 done
Task 1 done
Task 3 done
Task 7 done
Task 2 done
Task 6 done
Task 8 done
Task 9 done

在同步的情况下,任务是按顺序执行的,在执行各个任务的时候会阻塞主线程。

而gevent.spawn 的重要功能就是封装了greenlet里面的函数。 初始化的greenlet放在了threads这个list里面,被传递给了 gevent.joinall 这个函数,它会阻塞当前的程序来执行所有的greenlet。

在异步执行的情况下,所有任务的执行顺序是完全随机的。 每一个greenlet的都不会阻塞其他greenlet的执行。

协程VS子程序#

协程是为了实现单线程的i/o密集型的任务并发(通过event loop) 如果子程序的话,永远是串行的

什么是子程序?

协程比子程序的优势在哪里?

第16章 协程
https://blog.zhequtao.com/posts/python/流畅的python/第16章协程/
作者
阿俊
发布于
2020-03-30
许可协议
CC BY-NC-SA 4.0

相关阅读