协程
协程是线程的更小切分,又称为“微线程”,是一种用户态的轻量级线程。
与进程的区别:
相同点:
相同点存在于,当我们挂起一个执行流的时,我们要保存的东西:
栈, 其实在你切换前你的局部变量,以及要函数的调用都需要保存,否则都无法恢复
寄存器状态,这个其实用于当你的执行流恢复后要做什么
而寄存器和栈的结合就可以理解为上下文,上下文切换的理解:
CPU看上去像是在并发的执行多个进程,这是通过处理器在进程之间切换来实现的,操作系统实现这种交错执行的机制称为上下文切换
操作系统保持跟踪进程运行所需的所有状态信息。这种状态,就是上下文。
在任何一个时刻,操作系统都只能执行一个进程代码,当操作系统决定把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文,恢复新进程的上下文,然后将控制权传递到新进程,新进程就会从它上次停止的地方开始。
不同点:
执行流的调度者不同,进程是内核调度,而协程是在用户态调度,也就是说进程的上下文是在内核态保存恢复的,而协程是在用户态保存恢复的,很显然用户态的代价更低
进程会被强占,而协程不会,也就是说协程如果不主动让出CPU,那么其他的协程,就没有执行的机会。
对内存的占用不同,实际上协程可以只需要4K的栈就足够了,而进程占用的内存要大的多
从操作系统的角度讲,多协程的程序是单进程,单协程
与线程的区别:
既然我们上面也说了,协程也被称为微线程,下面对比一下协程和线程:
线程之间需要上下文切换成本相对协程来说是比较高的,尤其在开启线程较多时,但协程的切换成本非常低。
同样的线程的切换更多的是靠操作系统来控制,而协程的执行由我们自己控制。
协程只是在单一的线程里不同的协程之间切换,其实和线程很像,线程是在一个进程下,不同的线程之间做切换,这也可能是协程称为微线程的原因吧。
协程的优点:
(1)无需线程上下文切换的开销,协程避免了无意义的调度,由此可以提高性能(但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力)
(2)无需原子操作锁定及同步的开销
(3)方便切换控制流,简化编程模型
(4)高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理
协程的缺点:
(1)无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
(2)进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
- 协程,又称微线程,在不开辟多个线程的情况下,让多个任务交替执行 ,
- 由于协程不会开辟多个线程,所以当一个线程下有多个协程时,同一时间只会有一个协程正在执行。
通过yield来实现协程
代码执行到yield会暂停,然后把结果返回出去,下次启动时会在暂停的位置继续往下执行
- 每次启动都会返回一个值,多次启动可以返回多个值,也就是yield可以返回多个值
def work1():
count = 0
while True:
count += 1
print('work1',count)
yield
# 任务2
def work2():
count2 = 0
while True:
count2 += 1
print('任务2...', count2)
time.sleep(0.2)
yield
if __name__ == '__main__':
# 创建协程
g1 = work1()
g2 = work2()
# 启动协程
while True:
next(g1)
time.sleep(0.5)
next(g2)
可以看到work1和work2交替执行,每次执行到yield都会停止,调用next继续从yield开始执行
通过greenlet来实现协程
- 为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单,greenlet相当于是线程/进程内的一种合理安排后的串行,通过合理的代码执行流程切换,完全避免了死锁和阻塞等情况,但这些切换、返回,需要程序员手动进行编写安排。
# # 携程-greenlet
import greenlet
import time
def work1():
for i in range(10):
print('work1')
time.sleep(0.5)
# 切换任务
g2.switch()
def work2():
for i in range(8):
print('work2')
time.sleep(0.5)
# 任务切换
g1.switch()
if __name__ == '__main__':
g1 = greenlet.greenlet(work1)
g2 = greenlet.greenlet(work2)
# 可手动调整启动顺序
# 一个 “greenlet” 是一个小型的独立伪线程。可以把它想像成一些栈帧,
# 栈底是初始调用的函数,而栈顶是当前greenlet的暂停位置。
# 你使用greenlet创建一堆这样的堆栈,然后在他们之间跳转执行。
# 跳转必须显式声明的:一个greenlet必须选择要跳转到的另一个greenlet,这会让前一个挂起,而后一个在此前挂起处恢复执行。
# 不同greenlets之间的跳转称为切换(switching)
# 先执行1
g1.switch()
g2.switch()
使用gevent实现协程
在 Python 里,按照官方解释 greenlet 是轻量级的并行编程,gevent 就是利用 greenlet 实现的基于协程的 python 的网络 library,通过使用greenlet提供了一个在libev事件循环顶部的高级别并发API。即 gevent 是对 greenlet 的高级封装。
gevent内部封装的greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成。
# gevent
import greenlet
import gevent,time
from gevent import monkey
# 打补丁
monkey.patch_all()
# 使用gevent自带的方法
def work1():
for i in range(8):
print("看球")
# time.sleep(0.5)
gevent.sleep(0.5)
def work2():
for i in range(10):
print('吃饭')
# time.sleep(0.5)
gevent.sleep(0.5)
monkey.patch_all()
# 使用monkey 改写
def work3():
for i in range(5):
print("玩手机")
time.sleep(1)
def work4():
for i in range(5):
print("看书")
time.sleep(1)
爬虫异步IO阻塞切换:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2018/1/24 17:00
# @Author : Py.qi
# @File : gevent_urllib.py
# @Software: PyCharm
from urllib import request
import gevent,time
from gevent import monkey
monkey.patch_all() #将程序中所有IO操作做上标记使程序非阻塞状态
def url_request(url):
print('get:%s'%url)
resp = request.urlopen(url)
data = resp.read()
print('%s bytes received from %s'%(len(data),url))
async_time_start = time.time() #开始时间
gevent.joinall([
gevent.spawn(url_request,'https://www.python.org/'),
gevent.spawn(url_request,'https://www.nginx.org/'),
gevent.spawn(url_request,'https://www.ibm.com'),
])
print('haoshi:',time.time()-async_time_start) #总用时
协程实现多并发链接socket通信:
import socket,gevent
from gevent import monkey
monkey.patch_all()
def server_sock(port):
s = socket.socket()
s.bind(('',port))
s.listen(10)
while True:
conn,addr = s.accept()
gevent.spawn(handle_request,conn)
def handle_request(conn):
try:
while True:
data = conn.recv(1024)
if not data: conn.shutdown(socket.SHUT_WR)
print('recv:',data.decode())
conn.send(data)
except Exception as ex:
print(ex)
finally:
conn.close()
if __name__ == '__main__':
server_sock(8888)
import socket
HOST = 'localhost' # The remote host
PORT = 8888 # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
#msg = bytes(input(">>:"), encoding="utf8")
for i in range(50):
s.send('dddd'.encode())
data = s.recv(1024)
# print(data)
print('Received', repr(data))
s.close()