协程:
基于单线程来实现并发,是单线程下的开发,又称微线程,纤程。
协程并不是实际存在的实体,本质上是一个线程的多个部分。
协程比线程的单位更小 ,协程也叫纤程,在一个线程中可以开启很多协程。
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
在执行程序的过程中,遇到 IO 操作就冻结当前位置的状态,去执行其他任务,在执行其他任务的过程中,会不断地检测上一个冻结
的任务是否 IO 结束,如果 IO 结束了,就继续从冻结的位置开始执行。
单核CPU:
一个线程:如果没有遇到阻塞一直在使用CPU。
多个线程:只能有一个线程使用CPU。
协程相于线程:协程比线程之间的切换和线程的创建销毁所花费的时间,空间开销要小的多。
协程的特点:冻结当前程序/任务的执行状态,可以规避IO操作的时间。
单纯的切换,还是要耗费一些时间的,记住当前执行的状态。
用时间换了空间
**协程的引子**
import time
def producer():
res = []
for i in range(1000000):
res.append(i)
return res
def consumer(res):
for i in res:pass
start = time.time()
res = producer()
consumer(res)
print(time.time()-start) # 0.26484227180480957
def producer():
for i in range(1000000):
yield i
def consumer():
g = producer()
for i in g:pass
start = time.time()
consumer()
print(time.time() - start) # 0.09993767738342285
import time
def consumer():
while True:
x=yield
def producer():
g=consumer()
next(g)
for i in range(10000000):
g.send(i)
start = time.time()
producer()
print(time.time() - start) # 1.6259949207305908
需要强调的是:
1,python的线程是属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行。)
2,单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率。(!!!非io操作的切换与效率无关)
对比操作系统控制线程的切换,用户在线程内控制协程的切换:
优点如下:
1,协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级。
2,单线程内就可以实现并发的效果,最大限度地利用cpu.
缺点如下:
1,协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启多个协程。
2,协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程。
总结协程的特点:
1,必须在只有一个单线程里实现并发。
2,修改共享数据不需要加锁。
3,用户程序里自己保存多个控制流的上下文栈。
4,一个协程遇到 io 操作自动切换到其他协程
Greenlet模块:
安装:pip3 install greenlet
greenlet实现状态切换
import time
from greenlet import greenlet
def func1(name):
print('%s'%name,123)
g2.switch('小白')
time.sleep(1)
print('%s'%name,'abc')
def func2(name):
time.sleep(1)
print('%s'%name,456)
g1.switch()
g1 = greenlet(func1) # 实例化
g2 = greenlet(func2)
g1.switch('清秋') # 开始运行 可以在第一次switch时传入参数,以后就不用了。
'''
清秋 123
小白 456
清秋 abc
'''
效率对比
== 顺序执行 ==
import time
def f1():
res = 1
for i in range(10000000):
res += i
def f2():
res = 1
for i in range(10000000):
res *= i
start = time.time()
f1()
f2()
print(time.time()-start) # 1.5120854377746582
== 切换执行 ==
from greenlet import greenlet
import time
def f1():
res = 1
for i in range(10000000):
res += i
g2.switch()
def f2():
res = 1
for i in range(10000000):
res *= i
g1.switch()
start = time.time()
g1 = greenlet(f1)
g2 = greenlet(f2)
g1.switch()
print(time.time()-start) # 1.9758000373840332
== 由上可知,单纯的切换,反而会降低了程序的执行速度。==
greenlet 只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时,如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。
单线程里的这20个任务的代码通常会既有计算操作,又有阻塞操作,所以我们可以在这些时间去执行其他任务,这样就能提高效率,这就用到了gevent模块。
Gevent模块:
安装:pip3 install gevent
gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是greenlet,它是以C扩展模块形式介入Python的轻量级协程。greenlet全部运行在主程序操作系统进程的内部,但他们被协作式的调度。
用法介绍
g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,
可以是位置实参或关键字实参,都是传给函数eat的
g2=gevent.spawn(func2)
g1.join() #等待g1结束
g2.join() #等待g2结束
#或者上述两步合作一步:gevent.joinall([g1,g2])
g1.value#拿到func1的返回值
例子
from gevent import monkey;monkey.patch_all()
它会把下面导入的所有模块中的IO操作都打成一个包,gevent就能够识别这些IO操作了。
import time
import gevent
使用gevent模块来执行多个函数,表示在这些函数遇到IO操作的时候可以在同一个线程中进行切换。
利用其他任务的IO阻塞时间来切换到其他的任务继续执行。
spawn来发布协程任务
gevent本身并不认识其他模块中的IO操作,所以只有 from gevent import monkey;monkey.patch_all() 才能识别
gevent就能够认是在这句话后导入模块的IO操作。
from threading import currentThread
def eat():
print('eating1',currentThread())
time.sleep(1)
print('eating2')
def play():
print('playing1',currentThread())
time.sleep(1)
print('playing2')
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)
time.sleep(1) # 停一会等待执行完毕
遇到IO自动切换
from gevent import monkey;monkey.patch_all()
import time
import gevent
def eat(name):
print('%s eat1' % name)
time.sleep(1)
print('%s eat2' % name)
def play(name):
print('%s play1' % name)
time.sleep(1)
print('%s play2' % name)
g1 = gevent.spawn(eat,'egon')
g2 = gevent.spawn(play,'alex')
g1.join()
g2.join()
可以直接用 gevent.joinall([g1,g2])
print('主')
查看threading.current_thread().getName()
from gevent import monkey;monkey.patch_all()
import threading
import gevent
import time
def eat():
print(threading.current_thread().getName()) # DummyThread-1
print('eat1')
time.sleep(2)
print('eat2')
def play():
print(threading.current_thread().getName()) # DummyThread-2
print('play1')
time.sleep(2)
print('play2')
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)
gevent.joinall([g1,g2])
print('主')
gevent应用举例:
协程应用,爬虫
from gevent import monkey;monkey.patch_all()
import time
import gevent
from urllib.request import urlopen
def get_page(url):
res = urlopen(url)
print(len(res.read()))
url_lst = [
'http://www.baidu.com',
'http://www.sogou.com',
'http://www.sohu.com',
'http://www.qq.com',
'http://www.cnblogs.com',
]
start = time.time()
gevent.joinall([gevent.spawn(get_page,url) for url in url_lst])
print(time.time()-start) # 1.0084402561187744
网页读取有一个机制,第一次读取的时候时间都会普遍久
会将读取的网页缓存下来,以便下次的读取用。
start = time.time()
gevent.joinall([gevent.spawn(get_page,url) for url in url_lst])
print(time.time()-start) # 0.5516667366027832
start = time.time()
for url in url_lst:
get_page(url)
print(time.time()-start) # 1.533193588256836
所以我们可以通过时间看出,协程爬取是比普通遍历快很多。
通过协程实现单线程下的socket并发:
server
from gevent import monkey;monkey.patch_all()
import socket
import gevent
def async_talk(conn):
try:
while True:
conn.send(b'hello')
ret = conn.recv(1024)
print(ret)
# 为了实现能够一直和同一个客户端聊天。
finally:
conn.close() # 在程序报错的时候会关闭连接,节省空间
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()
while True:
conn,addr = sk.accept() # 因为循环,所以可重复接收多个多个客户端的连接
gevent.spawn(async_talk,conn) # 创建协程,将conn当参数传入函数
sk.close()
client
import socket
from threading import Thread
def chat():
sk = socket.socket() # 放在函数内部,则每次一个线程就会有一个新的sk。
sk.connect(('127.0.0.1',9000))
while True: # 循环对话。
print(sk.recv(1024))
sk.send(b'bye')
sk.close()
for i in range(500): # 创建500个线程客户端
Thread(target=chat).start()