1.协程介绍
协程:
1.是单线程下的并发
2.是用户代码自己控制的,遇到I/O就进行程序切换,即本来运行func1,遇到I/O,就去执行func2
(但是yeild,greenlet都无法实现遇到I/O进行程序切换,只有gevent可以实现)
3.修改共享数据不需要加锁
4.用户程序中保存多个控制流的上下文
"线程与协程对比"
线程:Python中的线程属于内核级别的,由操作系统控制调度
一个线程遇到I/O或执行时间过长就会*交出CPU,切换执行另一个线程
协程:单线程内的协程,一旦遇到I/O,就会从用户代码级别(不是由操作系统控制)控制切换,以此来提升效率
"非I/O操作的切换与效率无关"
"对比操作系统控制线程的切换《==》单线程内的协程切换"
协程切换的优点:
1.协程的切换开销更小,属于程序级别的切换,操作系统感知不到,因而更加轻量级
2.单线程内可以实现并发的效果,更大限度的利用CPU
协程切换的缺点:
1.协程的本质是在单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启多个协程
2.协程指的是单个线程,因而一旦协程阻塞,就会阻塞整个线程
1.1yeild实现协程
对于操作系统的多线程和多进程,操作系统能做到CPU在运行一个任务,会有两种情况CPU去执行其他任务
1.正在运行的任务发生I/O阻塞
2.正在运行的任务计算时间过长或有一个优先级更高的程序替代了它。
对于情况一,任务遇到I/O,切换到任务二去执行,这样可以利用任务一的阻塞时间文成任务二的计算,可以提升效率
对于情况二,如果任务是纯计算的,这种切换反而会降低效率
yeild实现协程有两个缺点:
1.演示任务一运行一段,就自动切换到任务二。
这种单纯的切换,反而会降低运行效率
2.演示遇到I/O,yeild是不能进行切换的,会阻塞在那里,等待程序的处理
1.1.1yeild单纯的切换,反而会降低运行效率
实际操作中,根据操作系统的不同,配置的不同,现象会有差异
"串行运行"
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
#串行执行
import time
def consumer(res):
'''任务1:接收数据,处理数据'''
pass
def producer():
'''任务2:生产数据'''
res=[]
for i in range(10000000):
res.append(i)
return res
start=time.time()
#串行执行
res=producer()
consumer(res) #写成consumer(producer())会降低执行效率
stop=time.time()
print(stop-start) #0.967444896697998
D:\software2\Python3\install\python.exe E:/PythonProject/new-python/python-test/BasicGrammer/test.py
0.967444896697998
Process finished with exit code 0
"基于yield保存状态,实现两个任务直接来回切换,即并发的效果"
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
#基于yield并发执行
import time
def consumer():
'''任务1:接收数据,处理数据'''
while True:
x=yield
#print("消费%s"%x)
def producer():
'''任务2:生产数据'''
g=consumer()
next(g)
for i in range(10000000):
#print("生产%s"%i)
g.send(i)
start=time.time()
#
#PS:如果每个任务中都加上打印,那么明显地看到两个任务的打印是你一次我一次,即并发执行的.
producer()
stop=time.time()
print(stop-start) #0.9723668098449707
D:\software2\Python3\install\python.exe E:/PythonProject/new-python/python-test/BasicGrammer/test.py
0.9723668098449707
Process finished with exit code 0
1.1.2yeild不能实现遇到I/O切换
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
import time
def consumer():
'''任务1:接收数据,处理数据'''
while True:
x=yield
def producer():
'''任务2:生产数据'''
g=consumer()
next(g)
for i in range(10000000):
g.send(i)
# producer遇到io就会阻塞住,并不会切到该线程内的其他任务去执行
time.sleep(2)
start=time.time()
producer()
stop=time.time()
print(stop-start)
1.2greenlet模块实现协程
greenlet模块实现协程,同样不能做到遇到I/O进行任务切换
greenlet只是提供了一种比generator更加便捷的切换方式
1.2.1greenlet模块演示
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
#安装:pip3 install greenlet
from greenlet import greenlet
def eat(name):
print('%s eat 1' %name)
# 可以在第一次switch时传入参数,以后都不需要
g2.switch('vita')
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('vita')#可以在第一次switch时传入参数,以后都不需要
D:\software2\Python3\install\python.exe E:/PythonProject/new-python/python-test/BasicGrammer/test.py
vita eat 1
vita play 1
vita eat 2
vita play 2
Process finished with exit code 0
1.2.2greenlet单纯的切换,反而耗费时间
"顺序执行"
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
#顺序执行
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)) #8.914215803146362
"greenlet切换"
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
#切换
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)) # 53.22377419471741
1.2.3greenlet遇到I/O不会进行任务切换
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
#切换
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
# 遇到sleep()就会停留等在这里,不会去执行别的任务
time.sleep(2)
g1.switch()
start=time.time()
g1=greenlet(f1)
g2=greenlet(f2)
g1.switch()
stop=time.time()
print('run time is %s' %(stop-start)) # 53.22377419471741
上面已经演示了yeild和greenlet模块的多线程模式,都不能做到遇到I/O进行任务的切换,下面的gevent可以做到,让我们来看奇迹吧!
1.3gevent实现协程
1.3.1gevent模块介绍
"安装"
pip3 install gevent
"用法"
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的返回值
1.3.2gevent是异步提交任务的
"由于是异步提交任务的,所以执行完spawn后,就继续主线程,主线程运行完了,就退出了
所以连函数都没执行"
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
import gevent
def eat(name):
print('%s eat 1' %name)
gevent.sleep(2)
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')
# 这里没有join
#或者gevent.joinall([g1,g2])
print('主')
D:\software2\Python3\install\python.exe E:/PythonProject/new-python/python-test/BasicGrammer/test.py
主
Process finished with exit code 0
1.3.2遇到I/O阻塞会自动切换任务
"需要join(),主线程等待,才会运行函数"
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
import gevent
def eat(name):
print('%s eat 1' %name)
# 遇到sleep,切换到play函数
gevent.sleep(2)
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])
print('主')
D:\software2\Python3\install\python.exe E:/PythonProject/new-python/python-test/BasicGrammer/test.py
egon eat 1
egon play 1
egon play 2
egon eat 2
主
Process finished with exit code 0
上面的gevent.sleep(2)模拟的是gevent可以识别的io阻塞
而time.sleep()或其他的阻塞,gevent要想识别,需要将from gevent import monkey;monkey.patch_all()放到文件的开头
可以使用threading.current_thread().getName()来查看线程名
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from gevent import monkey;monkey.patch_all()
from threading import current_thread
import gevent
import time
def eat():
print('%seat food 1'% current_thread().getName())
time.sleep(2)
print('%seat food 2'% current_thread().getName())
def play():
print('%splay 1'% current_thread().getName())
time.sleep(1)
print('%splay 2'% current_thread().getName())
g1=gevent.spawn(eat)
g2=gevent.spawn(play)
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('主')
D:\software2\Python3\install\python.exe E:/PythonProject/new-python/python-test/BasicGrammer/test.py
DummyThread-1eat food 1
DummyThread-2play 1
DummyThread-2play 2
DummyThread-1eat food 2
主
Process finished with exit code 0
1.3.3gevent实现socket并发
"server"
from gevent import monkey;monkey.patch_all()
from socket import *
import gevent
#如果不想用money.patch_all()打补丁,可以用gevent自带的socket
# from gevent import socket
# s=socket.socket()
def server(server_ip,port):
s=socket(AF_INET,SOCK_STREAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind((server_ip,port))
s.listen(5)
while True:
conn,addr=s.accept()
# 这里没有join,是因为spawn()之后,进入下一循环,主线程没有结束
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)
"client"
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from threading import Thread
from socket import *
import threading
def client(server_ip,port):
c=socket(AF_INET,SOCK_STREAM) #套接字对象一定要加到函数内,即局部名称空间内,放在函数外则被所有线程共享,则大家公用一个套接字对象,那么客户端端口永远一样了
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()