协程

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()

协程

上一篇:CentOS 7安装gevent


下一篇:Python抓取大型网站JS特效模板,想要的资源都能爬!