python中的协程(二)

协程

一、协程:python

单线程实现并发git

在应用程序里控制多个任务的切换+保存状态程序员

优势:github

应用程序级别速度要远远高于操做系统的切换编程

缺点:数组

多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地,该线程内的其余的任务都不能执行了多线程

一旦引入协程,就须要检测单线程下全部的IO行为, 实现遇到IO就切换,少一个都不行,觉得一旦一个任务阻塞了,整个线程就阻塞了, 其余的任务即使是能够计算,可是也没法运行了并发

二、协程序的目的:ssh

想要在单线程下实现并发异步

并发指的是多个任务看起来是同时运行的

并发=切换+保存状态

协程引入

cpu正在运行一个任务,会在两种状况下切走去执行其余的任务(切换由操做系统强制控制),一种状况是该任务发生了阻塞,另一种状况是该任务计算的时间过长或有一个优先级更高的程序替代了它

ps:在介绍进程理论时,说起进程的三种执行状态,而线程才是执行单位,因此也能够将上图理解为线程的三种状态 

        一:其中第二种状况并不能提高效率,只是为了让cpu可以雨露均沾,实现看起来全部任务都被“同时”执行的效果,若是多个任务都是纯计算的,这种切换反而会下降效率。为此咱们能够基于yield来验证。yield自己就是一种在单线程下能够保存任务运行状态的方法,咱们来简单复习一下:

#串行执行
import time

def func1():
    for i in range(10000000):
        i+1

def func2():
    for i in range(10000000):
        i+1

start = time.time()
func1()
func2()
stop = time.time()
print(stop - start)


#基于yield并发执行
import time
def func1():
    while True:
        yield

def func2():
    g=func1()
    for i in range(10000000):
        i+1
        next(g)

start=time.time()
func2()
stop=time.time()
print(stop-start)

  二:第一种状况的切换。在任务一遇到io状况下,切到任务二去执行,这样就能够利用任务一阻塞的时间完成任务二的计算,效率的提高就在于此。

import time
def func1():
    while True:
        print('func1')
        yield

def func2():
    g=func1()
    for i in range(10000000):
        i+1
        next(g)
        time.sleep(3)
        print('func2')
start=time.time()
func2()
stop=time.time()
print(stop-start)

yield不能检测IO,实现遇到IO自动切换

对于单线程下,咱们不可避免程序中出现io操做,但若是咱们能在本身的程序中(即用户程序级别,而非操做系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另一个任务去计算,这样就保证了该线程可以最大限度地处于就绪态,即随时均可以被cpu执行的状态,至关于咱们在用户程序级别将本身的io操做最大限度地隐藏起来,从而能够迷惑操做系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给咱们的线程。

协程的本质:

就是在单线程下,由用户本身控制一个任务遇到io阻塞了就切换另一个任务去执行,以此来提高效率。为了实现它,咱们须要找寻一种能够同时知足如下条件的解决方案:

#1. 能够控制多个任务之间的切换,切换以前将任务的状态保存下来,以便从新运行时,能够基于暂停的位置继续执行。

#2. 做为1的补充:能够检测io操做,在遇到io操做的状况下才发生切换

协程介绍

协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序本身控制调度的。、

须要强调的是:

#1. python的线程属于内核级别的,即由操做系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其余线程运行)
#2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操做系统)控制切换,以此来提高效率(!!!非io操做的切换与效率无关)

对比操做系统控制线程的切换,用户在单线程内控制协程的切换

优势以下:

#1. 协程的切换开销更小,属于程序级别的切换,操做系统彻底感知不到,于是更加轻量级
#2. 单线程内就能够实现并发的效果,最大限度地利用cpu

缺点以下:

#1. 协程的本质是单线程下,没法利用多核,能够是一个程序开启多个进程,每一个进程内开启多个线程,每一个线程内开启协程
#2. 协程指的是单个线程,于是一旦协程出现阻塞,将会阻塞整个线程

总结协程特色:

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里本身保存多个控制流的上下文栈
  4. 附加:一个协程遇到IO操做自动切换到其它协程(如何实现检测IO,yield、greenlet都没法实现,就用到了gevent模块(select机制))

Greenlet

若是咱们在单个线程内有20个任务,要想实如今多个任务之间切换,使用yield生成器的方式过于麻烦(须要先获得初始化一次的生成器,而后再调用send。。。很是麻烦),而使用greenlet模块能够很是简单地实现这20个任务直接的切换

#安装
pip3 install greenlet
复制代码
from greenlet import greenlet

def eat(name):
    print('%s eat 1' %name)
    g2.switch('egon')
    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('egon')#能够在第一次switch时传入参数,之后都不须要
复制代码

单纯的切换(在没有io的状况下或者没有重复开辟内存空间的操做),反而会下降程序的执行速度

greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时若是遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提高效率的问题。

单线程里的这20个任务的代码一般会既有计算操做又有阻塞操做,咱们彻底能够在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2。。。。如此,才能提升效率,这就用到了Gevent模块(后续)。

greenlet module与class

咱们首先看一下greenlet这个module里面的属性

其中,比较重要的是getcurrent(), 类greenlet、异常类GreenletExit。

getcurrent()返回当前的greenlet实例;

GreenletExit:是一个特殊的异常,当触发了这个异常的时候,即便不处理,也不会抛到其parent(后面会提到协程中对返回值或者异常的处理)

而后咱们再来看看greenlet.greenlet这个类:

比较重要的几个属性:

run:当greenlet启动的时候会调用到这个callable,若是咱们须要继承greenlet.greenlet时,须要重写该方法

switch:前面已经介绍过了,在greenlet之间切换

parent:可读写属性,后面介绍

dead:若是greenlet执行结束,那么该属性为true

throw:切换到指定greenlet后当即跑出异常

文章后面提到的greenlet大多都是指greenlet.greenlet这个class,请注意区别。

parent属性

每个Greenlet都有一个parent,一个新的greenlet在哪里创生,当前环境的greenlet就是这个新greenlet的parent。全部的greenlet构成一棵树,其跟节点就是尚未手动建立greenlet时候的”main” greenlet(事实上,在首次import greenlet的时候实例化)。当一个协程 正常结束,执行流程回到其对应的parent;或者在一个协程中抛出未被捕获的异常,该异常也是传递到其parent。

import greenlet
def test1(x, y):
    print id(greenlet.getcurrent()), id(greenlet.getcurrent().parent) # 40240272 40239952
    z = gr2.switch(x+y)
    print 'back z', z

def test2(u):
    print id(greenlet.getcurrent()), id(greenlet.getcurrent().parent) # 40240352 40239952
    return 'hehe'

gr1 = greenlet.greenlet(test1)
gr2 = greenlet.greenlet(test2)
print id(greenlet.getcurrent()), id(gr1), id(gr2)     # 40239952, 40240272, 40240352
print gr1.switch("hello", " world"), 'back to main'    # hehe back to main

上述例子能够看到,尽可能是从test1所在的协程gr1 切换到了gr2,但gr2的parent仍是’main’ greenlet,由于默认的parent取决于greenlet的创生环境。另外 在test2中return以后整个返回值返回到了其parent,而不是switch到该协程的地方(即不是test1),这个跟咱们平时的函数调用不同,记住“switch not call”。对于异常 也是展开至parent。

mport greenlet
def test1(x, y):
    try:
        z = gr2.switch(x+y)
    except Exception:
        print 'catch Exception in test1'

def test2(u):
    assert False

gr1 = greenlet.greenlet(test1)
gr2 = greenlet.greenlet(test2)
try:
    gr1.switch("hello", " world")
except:
    print 'catch Exception in main'

输出为:

catch Exception in main

from greenlet import greenlet, GreenletExit
huge = []
def show_leak():
    def test1():
        gr2.switch()
 
    def test2():
        huge.extend([x* x for x in range(100)])
        gr1.switch()
        print 'finish switch del huge'
        del huge[:]
    
    gr1 = greenlet(test1)
    gr2 = greenlet(test2)
    gr1.switch()
    gr1 = gr2 = None
    print 'length of huge is zero ? %s' % len(huge)
 
if __name__ == '__main__':
    show_leak() 
   # output: length of huge is zero ? 100

  在test2函数中 第11行,咱们将huge清空,而后再第16行将gr一、gr2的引用计数降到了0。但运行结果告诉咱们,第11行并无执行,因此若是一个协程没有正常结束是很危险的,每每不符合程序员的预期。greenlet提供了解决这个问题的办法,官网文档提到:若是一个greenlet实例的引用计数变成0,那么会在上次挂起的地方抛出GreenletExit异常,这就使得咱们能够经过try … finally 处理资源泄露的状况。以下面的代码:

from greenlet import greenlet, GreenletExit
huge = []
def show_leak():
    def test1():
        gr2.switch()
 
    def test2():
        huge.extend([x* x for x in range(100)])
        try:
            gr1.switch()
        finally:
            print 'finish switch del huge'
            del huge[:]
    
    gr1 = greenlet(test1)
    gr2 = greenlet(test2)
    gr1.switch()
    gr1 = gr2 = None
    print 'length of huge is zero ? %s' % len(huge)
 
if __name__ == '__main__':
    show_leak()
    # output :
    # finish switch del huge
   # length of huge is zero ? 0

  上述代码的switch流程:main greenlet –> gr1 –> gr2 –> gr1 –> main greenlet, 很明显gr2没有正常结束(在第10行刮起了)。第18行以后gr1,gr2的引用计数都变成0,那么会在第10行抛出GreenletExit异常,所以finally语句有机会执行。同时,在文章开始介绍Greenlet module的时候也提到了,GreenletExit这个异常并不会抛出到parent,因此main greenlet也不会出异常。

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的返回值
复制代码

遇到IO阻塞时会自动切换任务

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')
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('主')

上例gevent.sleep(2)模拟的是gevent能够识别的io阻塞,

而time.sleep(2)或其余的阻塞,gevent是不能直接识别的须要用下面一行代码,打补丁,就能够识别了

from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块以前

或者咱们干脆记忆成:要用gevent,须要将from gevent import monkey;monkey.patch_all()放到文件的开头

复制代码
from gevent import monkey;monkey.patch_all()

import gevent
import time
def eat():
    print('eat food 1')
    time.sleep(2)
    print('eat food 2')

def play():
    print('play 1')
    time.sleep(1)
    print('play 2')

g1=gevent.spawn(eat)
g2=gevent.spawn(play_phone)
gevent.joinall([g1,g2])
print('主')
复制代码

咱们能够用threading.current_thread().getName()来查看每一个g1和g2,查看的结果为DummyThread-n,即假线程

Gevent之同步与异步

from gevent import spawn,joinall,monkey;monkey.patch_all()

import time
def task(pid):
    """
    Some non-deterministic task
    """
    time.sleep(0.5)
    print('Task %s done' % pid)


def synchronous():
    for i in range(10):
        task(i)

def asynchronous():
    g_l=[spawn(task,i) for i in range(10)]
    joinall(g_l)

if __name__ == '__main__':
    print('Synchronous:')
    synchronous()

    print('Asynchronous:')
    asynchronous()
#上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn。 初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数,后者阻塞当前流程,并执行全部给定的greenlet。执行流程只会在 全部greenlet执行完后才会继续向下走。

Gevent之应用举例一

from gevent import monkey;monkey.patch_all()
import gevent
import requests
import time

def get_page(url):
    print('GET: %s' %url)
    response=requests.get(url)
    if response.status_code == 200:
        print('%d bytes received from %s' %(len(response.text),url))


start_time=time.time()
gevent.joinall([
    gevent.spawn(get_page,'https://www.python.org/'),
    gevent.spawn(get_page,'https://www.yahoo.com/'),
    gevent.spawn(get_page,'https://github.com/'),
])
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))

协程应用:爬虫

Gevent之应用举例二

经过gevent实现单线程下的socket并发(from gevent import monkey;monkey.patch_all()必定要放到导入socket模块以前,不然gevent没法识别socket的阻塞)

服务端

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()
        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 socket import *

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))


while True:
    msg=input('>>: ').strip()
    if not msg:continue

    client.send(msg.encode('utf-8'))
    msg=client.recv(1024)
    print(msg.decode('utf-8'))

多线程并发多个客户端

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()
相关文章
相关标签/搜索