Python - - 并发编程 - - 协成
目录
- 协程介绍
- greenlet模块
- gevent模块
1,协程介绍
-
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
-
并发的本质:切换+保存状态
-
在操作系统中
- 进程:是资源分配的最小单位
- 线程:是CPU调度的最小单位
- 协程:是单线程内实现并发切换执行任务的
-
使用yield也可以实现在一个主线程中切换执行
# 生成器 yield
def func1():
print(1)
yield
print(3)
yield
def func2():
g = func1() # 生成器函数在被调用时不会立即执行,除非next(g)触发才可以
next(g) # 开始执行func1()函数,但是遇到yield就会停止
print(2)
next(g)
print(4)
func2()
# 结果呈现
1
2
3
4
def consumer(): # 消费者模型
while True:
n = yield # yield接收g.send()的结果,然后赋值给n
print("消费了包子 %s" % n)
def producer(): # 生产者模型
g = consumer() # 调用生成器函数,并不会理解执行生成器函数内部的代码(除非next()进行触发)
next(g) # 开始执行生成器函数
for i in range(10):
print("生产了包子 %s" % i)
g.send(i) # send()给生成器函数yield处接收
producer()
# 结果呈现
生产了包子 0
消费了包子 0
生产了包子 1
消费了包子 1
生产了包子 2
消费了包子 2
生产了包子 3
消费了包子 3
生产了包子 4
消费了包子 4
生产了包子 5
消费了包子 5
生产了包子 6
消费了包子 6
生产了包子 7
消费了包子 7
生产了包子 8
消费了包子 8
生产了包子 9
消费了包子 9
- 在yield切换中,在任务一遇到io情况下,切到任务二去执行,这样就可以利用任务一阻塞的时间完成任务二的计算,效率的提升就在于此。
- 需要强调的是:
- 1 python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
- 2 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
- 对比操作系统控制线程的切换,用户在单线程内控制协程的切换
- 优点:
- 1 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
- 2 单线程内就可以实现并发的效果,最大限度地利用CPU
- 缺点:
- 1 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程开启多个线程,每个线程内开启协程
- 2 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
- 优点:
- 总结协程特点
- 1 必须在只有要给单线程里实现并发
- 2 修改共享数据不需要加锁
- 3 用户程序里自己保持多个控制的上下文栈
- 4 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
2,greenlet模块
2.1 greenlet 实现同一线程内切换
import time
from greenlet import greenlet # 在单线程中切换状态的模块
def eat1():
print("吃鸡腿")
g2.switch() # 切换执行eat2
time.sleep(5) # greenlet进行切换时,并不会规避掉IO时间(也就是切换回来时还是需要等待2秒在执行)
print("吃鸡翅")
g2.switch() # 切换执行eat2
def eat2():
print("吃饺子")
g1.switch() # 切换执行eat1
time.sleep(3) # greenlet进行切换时,并不会规避掉IO时间(也就是切换回来时还是需要等待2秒在执行)
print("白切鸡")
g1 = greenlet(eat1)
g2 = greenlet(eat2)
g1.switch() # 切换执行eat1
# 结果呈现
吃鸡腿
吃饺子
吃鸡翅
白切鸡
- 如果在同一个程序有IO的情况下,才切换会让效率提高很多,但是yield greenlet均不会在切换时规避掉IO时间
2.2 greenlet 实现 效率对比
#顺序执行
import time
def f1():
res=1
for i in range(100000000):
res+=i
def f2():
res=1
for i in range(100000000):
res*=i
start=time.time()
f1()
f2()
stop=time.time()
print('run time is %s' %(stop-start)) #10.985628366470337
#切换
from greenlet import greenlet
import time
def f1():
res=1
for i in range(100000000):
res+=i
g2.switch()
def f2():
res=1
for i in range(100000000):
res*=i
g1.switch()
start=time.time()
g1=greenlet(f1)
g2=greenlet(f2)
g1.switch()
stop=time.time()
print('run time is %s' %(stop-start)) # 52.763017892837524
3,gevent模块
- gevent 就是当遇到gevent.sleep() IO 时会自动切换;
# gevent 内部封装了greenlet模块
import gevent
def eat(name):
print('%s eat 1' %name)
gevent.sleep(2) # gevent可以在gevevt.sleep()自己认识的IO操作切换
print('%s eat 2' %name)
def play(name):
print('%s play 1' %name)
gevent.sleep(1)
print('%s play 2' %name)
g1=gevent.spawn(eat,'egon')
g2=gevent.spawn(play,name='egon')
# g1.join()
# g2.join()
gevent.joinall([g1,g2]) # 相当于上面的g1.join() g2.join()
# 结果呈现
egon eat 1
egon play 1
egon play 2
egon eat 2
- gevent()对普通的IO (比如time模块的sleep,socket 以及urllib request等网络请求)是无法切换的:
import gevent,time
def eat(name):
print('%s eat 1' %name)
time.sleep(2)
print('%s eat 2' %name)
def play(name):
print('%s play 1' %name)
time.sleep(1)
print('%s play 2' %name)
g1=gevent.spawn(eat,'egon')
g2=gevent.spawn(play,name='egon')
gevent.joinall([g1,g2])
# 结果呈现
egon eat 1
egon eat 2
egon play 1
egon play 2
- 上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了
from gevent import monkey;monkey.patch_all()
必须放到被打补丁者的前面,如time,socket模块之前- 我们可以用
threading.current_thread().getName()
来查看每个g1和g2,查看的结果为DummyThread-n,即假线程
from gevent import monkey;monkey.patch_all() # 加上这句话,gevent遇到其他模块(time,socket等IO操作)的IO 需要等待时 就会切换协程
from threading import current_thread
import gevent,time
def func1():
print(current_thread().name) # 打印当前线程名(其实协程并不是线程,多个协程是在同一个线程内完成的)
print(123)
time.sleep(1)
print(456)
def func2():
print(current_thread().name)
print(789)
time.sleep(1)
print(101112)
g1 = gevent.spawn(func1) # 遇见它认识的io会自动切换的模块
g2 = gevent.spawn(func2)
# g1.join()
# g2.join()
gevent.joinall([g1,g2])
# 结果呈现
DummyThread-1
123
DummyThread-2
789
456
101112
3.1 Gevent之同步与异步
- 测试有IO操作时,使用多个协程与开单线程单步执行多个任务执行效率的对比
from gevent import monkey;monkey.patch_all()
import gevent,time
def task(args):
time.sleep(1)
print(args)
def sync_fucn(): # 同步
for i in range(10):
task(i)
def async_func(): # 异步
g_lst = []
for i in range(10):
g_lst.append(gevent.spawn(task,i)) # 发起协程任务,传参数
gevent.joinall(g_lst)
start = time.time()
sync_fucn()
print(time.time() - start)
start = time.time()
async_func()
print(time.time() - start)
# 结果呈现
0
1
2
3
4
5
6
7
8
9
10.011728048324585
0
1
2
3
4
5
6
7
8
9
1.0025279521942139
3.2 开多个协程去爬取多个网页与单线程单步执行爬取网页的效率对比
# 爬去网页信息的例子
from gevent import monkey;monkey.patch_all()
import gevent,requests,time
# 协程函数发起10个网页的爬取任务
def get_url(url):
res = requests.get(url)
print(url,res.status_code,len(res.text)) # 返回爬取网页的信息(requests.get(url).text----获取网页源代码; requests.get(url).status_code----获取网页状态码)
url_lst = [
"http://www.sohu.com",
"http://www.baidu.com",
"http://www.qq.com",
"http://www.python.org",
"http://www.cnblogs.com",
"http://www.mi.com",
"http://www.apache.org"
]
g_lst = []
start = time.time()
for url in url_lst:
g = gevent.spawn(get_url,url)
g_lst.append(g)
gevent.joinall(g_lst)
print(time.time() - start)
start = time.time()
for url in url_lst:
get_url(url)
print(time.time() - start)
# 结果呈现
http://www.baidu.com 200 2381
http://www.sohu.com 200 178923
http://www.qq.com 200 205793
http://www.mi.com 200 312788
http://www.cnblogs.com 200 41063
http://www.apache.org 200 62019
http://www.python.org 200 49235
1.198430061340332
http://www.sohu.com 200 178923
http://www.baidu.com 200 2381
http://www.qq.com 200 205793
http://www.python.org 200 49235
http://www.cnblogs.com 200 41043
http://www.mi.com 200 312788
http://www.apache.org 200 62019
2.1779263019561768
- 协程在响应一个网页时,有网络延时,它就可能利用这个时间去打开其他网页了,也就是时间复用,有可能利用第一个网页等待时间,把剩下所有网页的请求都发出去了;
- 同步单步执行时,每执行一个网页就会等待网络延时,串行的;而协程就是在发送一个网页时,不等,因为它直到有网络延时,所以直接执行下一个任务;
3.3 使用协程完成server端和client端的通信
- 测试连通性
# server
import socket
sk = socket.socket()
sk.bind(("127.0.0.1",8080))
sk.listen()
conn,addr = sk.accept()
ret = conn.recv(1024).decode("utf-8")
print(ret)
conn.send(ret.upper().encode("utf-8"))
conn.close()
sk.close()
# client
import socket
sk = socket.socket()
sk.connect(("127.0.0.1",8080))
sk.send(b"hi")
ret = sk.recv(1024).decode("utf-8")
print(ret)
sk.close()
- 客户端并发连接服务端
# server
from gevent import monkey;monkey.patch_all()
import socket,gevent
def talk(conn):
while True:
ret = conn.recv(1024).decode("utf-8")
print(ret)
conn.send(ret.upper().encode("utf-8"))
conn.close()
sk = socket.socket()
sk.bind(("127.0.0.1",8080))
sk.listen()
while True:
conn,addr = sk.accept()
gevent.spawn(talk,conn)
sk.close()
# client
from gevent import monkey;monkey.patch_all()
import socket,gevent,time,threading
def my_client():
sk = socket.socket()
sk.connect(("127.0.0.1",8080))
while True:
sk.send(b"hi")
ret = sk.recv(1024).decode("utf-8")
print(ret)
time.sleep(1)
sk.close()
for i in range(500):
threading.Thread(target=my_client).start()
-
(cpucount + 1 ) * cpucount * 5 * 500并发 == 50000