~~并发编程(十六):协程理论~~
进击のpython
并发编程——协程理论
本节的主题的重点是基于单线程来实现并发,即只用一个主线程的我情况下实现并发
所以在说这节知识点之前,我们需要先回顾一下并发的本质:切换+保存状态
那可以肯定一点的就是CPU正在运行一个任务的时候,会在两种情况切走去执行其他的任务
但是这种切换机制,不是我们控制的,而是操作系统强制控制的
这两种情况是:1.发生了阻塞 2.该任务计算时间过长,或者来了优先级更高的程序
而很明显第二种情况并不能提升效率!只是为了让CPU能够雨露均沾,看起来是同时执行的假象
所以才会说并发是假的并行,因为他是看起来是同时,但是实际情况下并不是
如果在计算的时候,这种切换其实是降低效率的,我们可以验证一下,模拟切换机制yield
在串行的前提:
from time import time
def a():
n = 0
for i in range(10000):
n += 1
return n
def b(res):
pass
start_time = time()
res = a()
b(res)
stop_time = time()
print(stop_time - start_time) # 0.0025005340576171875
在yeild切换的状态下:
from time import time
def a():
g = b()
next(g)
n = 0
for i in range(10000):
n += 1
g.send(n)
return n
def b(res=None):
while True:
res = yield
pass
start_time = time()
a()
stop_time = time()
print(stop_time - start_time) # 0.006000518798828125
可以看到,来回切换的这种,速度确实慢!即使差得很不多,但是也三倍之多!
第一种情况的切换:在任务一遇到I/O的情况下,切换到任务二去执行
这样就可以利用任务一阻塞的时间完成任务二
第二种情况就不会执行I/O切换
而在单线程的情况下我们是不可避免的会遇到I/O阻塞的
但是如果我们能在自己的程序中控制单线程下的多个任务能够在一个任务遇到I/O阻塞时
就切换到另一个任务去计算,这样就能够保证这个线程最大程度的处于就绪态
这样就能“迷惑”操作系统,以为没有遇到I/O,让其感觉好像线程一直在工作,这样就可以一直“霸占”CPU
协程
协程记住一句话:他是可控的线程!
python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
对比线程,协程开销更小,更加的轻量级,而且可以在单线程里就达到并发的效果
但是缺点也就很明显了:无法利用多核,而且携程阻塞,也就阻塞了整个线程
那我们就简单的介绍一下可以实现协程的模块:
greenlet
其实上面的yeild就实现了协程,只是这么写,太麻烦了
于是就提供了一个模块来处理这种情况
from greenlet import greenlet
def eat(name):
print('%s eat 1' % name)
g2.switch('jevious')
print('%s eat 2' % name)
g2.switch()
def play(name):
print('%s play 1' % name)
g1.switch()
print('%s play 2' % name)
g1 = greenlet(eat)
g2 = greenlet(play)
g1.switch('ponny')
只需要第一次调用的时候传参
但是这只是优化了yield写法,本质上还是没有解决遇到阻塞就切换的问题
所以接下来这个方法就出现了
gevent
这也是一个第三方库,他是C扩展模块形式接入Python的轻量级协程
import gevent
def eat(name):
print('%s eat 1' % name)
gevent.sleep(1)
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, 'ponny')
g2 = gevent.spawn(play, 'ponny')
g1.join()
g2.join()
这种程度的模块不会很细的讲解
这种就很好的解决了遇到I/O主动切换的问题(否则上面的程序执行时间就应该是2s+)
但是有个弊端,就是他只认识自己造出来的阻塞gevent.sleep(1)
但是这样不行,系统产生的I/O可不是模块造出来的阻塞,也就意味着这样的阻塞不会被处理!
所以,gevent又内嵌一个方法:猴子(monkey)
from gevent import monkey;monkey.patch_all()
简称:打补丁,他就让所有的阻塞都可以被识别
那既然想让所有的阻塞都被识别,很明显这个语句就应该放在最前面才对!
代码优化如下:
from gevent import monkey;monkey.patch_all()
import gevent
import time
def eat(name):
print('%s eat 1' % name)
time.sleep(3)
print('%s eat 2' % name)
def play(name):
print('%s play 1' % name)
time.sleep(4)
print('%s play 2' % name)
start_time = time.time()
g1 = gevent.spawn(eat, 'ponny')
g2 = gevent.spawn(play, 'ponny')
g1.join()
g2.join()
print(f'执行时间为:{time.time()-start_time}') # 执行时间为:4.0118021965026855
这是在单线程实现了并发的效果!
ps:如果两个join写着麻烦,也可以gevent.joinall([g1, g2])
socket通信
我确定,这是最后一个版本了~哈哈
不断的优化,不断地修改,也终于该有个结束了
最后就用协程的方式来写socket通信:
# 服务端
from gevent import monkey;monkey.patch_all()
from socket import *
import gevent
def server(server_ip, port):
s = socket()
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 前面提过,用于解决端口复用
s.bind((server_ip, port))
s.listen()
while True:
conn, addr = s.accept()
gevent.spawn(talk, conn, addr)
def talk(conn, addr):
try:
while True:
res = conn.recv(1024)
print('client %s:%s msg: %s' % (addr[0], addr[1], res))
conn.send(res.upper())
except Exception as e:
print(e)
finally:
conn.close()
if __name__ == '__main__':
server('127.0.0.1', 8080)
# 服务端
from threading import Thread
from socket import *
import threading
def client(server_ip, port):
c = socket()
c.connect((server_ip, port))
count = 0
while True:
c.send(('%s say hello %s' % (threading.current_thread().getName(), count)).encode('utf-8'))
msg = c.recv(1024)
print(msg.decode('utf-8'))
count += 1
if __name__ == '__main__':
for i in range(500):
t = Thread(target=client, args=('127.0.0.1', 8080))
t.start()
协程的也写完了,至此,socket通信就结束了