协程与并行(2/3):协程,管道和数据流

代码在这里

github 上查看代码和笔记



第二部分 : 协程,管道和数据流


协程可以用来构建管道,只用把协程串起来并通过send推送数据就行了

这种管道(由协程构成的)需要要一个初始的数据源(一个生产者),

源驱动了整个的管道链

像这样:

def source(target): 

while not done:

item = produce_an_item() ...

target.send(item)


               target.close()



数据源一般不是个协程。



管道结束点

管道必须要有一个结束点(下水道)


send()

->  coroutine -> 下水道(sink)


这个下水道的作用是收集所有发送给它的数据并处理

(即作为管道的尾端,不再向下一链条传递,必要时处理close异常)

 @coroutine

           def sink():

               try:

                   while True:

                        item = (yield)

...

               except GeneratorExit:

# Done ...


使用协程的改进版本的仿 ‘tail -f ‘命令

首先是协程的例行公事:


import functools


def coroutine(function):

    @functools.wraps(function)

    def wrapper(*args, **kwargs):

        generator = function(*args, **kwargs)

        next(generator) #prime the generator

        return generator

    return wrapper


数据生成源

import time

def follow(the file, target):

    thefile.seek(0,2)      # Go to the end of the file

    while True:

        line = thefile.readline()

        if not line:

          time.sleep(0.1)    # Sleep briefly

          continue

        target.send(line)


这个下水道仅仅打印收到的数据


@coroutine

def printer():

    while True:

        line = (yield)

        print(line, end=‘’)


关键点就在于follow函数读取数据并推送给printer()协程

f=open(‘a’)

follow(f, printer())



处于管道中间部分(既接收又推送)的协程一般是用来

完成变换,过滤,路由等功能。


@coroutine

def filter(target):

while True:

        item = (yield)

# Receive an item

# Transform/filter item
...
# Send it along to the next stage target.send(item)

grep filter coroutine

     @coroutine

     def grep(pattern,target):

while True:

             line = (yield)           # Receive a line

组装起来(很像lisp)

     f = open("access-log")

     follow(f,

            grep('python',

            printer()))



生成器与协程组成管道的不同之处在于,

生成器是基于迭代器的而协程使用send():


generator---(迭代器)->generator---(迭代器)->


coroutine--send()->coroutine--send()->coroutine--send()->


另外,可以使用协程将数据发送至多个方向

数据源只是发送数据,进一步的数据路由可以是任意的复杂


比如:将数据广播到多个目标:

 @coroutine

    def broadcast(targets):

        while True:

            item = (yield)

            for target in targets:

                target.send(item)







相对一般的迭代器而言,协程提供了更加强大的数据路由可能性

如果你构建了一堆数据处理组件,你可以通过一系列复杂的管道,

分支,合并等操作将它们粘合起来


不过协程也有一些限制(放在后边谈)


另外,yield并不是表达式(?)

想把它当作C语言一样来写的同学要悲剧了:

while(line=yield)是语法错误。。。



协程和对象:

协程在某些程度上和OO设计模式的

请求处理器差不多


class GrepHandler(object):

        def __init__(self,pattern, target):

            self.pattern = pattern

            self.target = target

        def send(self,line):

            if self.pattern in line:

                self.target.send(line)



协程版本的:


@coroutine

def grep(pattern,target):

    while True:

        line = (yield)           # Receive a line

        if pattern in line:

            target.send(line)



协程比类要简单且快(50%左右)

性能差距在于self指针的使用




第三部分:


协程与事件分派

协程可以用来写很多处理事件流的组件



现在展示一个处理xml组件:

一个记录当前巴士的xml:

<?xml version="1.0"?>

  <buses>

<bus>

<id>7574</id>

<route>147</route>

<color>#3300ff</color>

<revenue>true</revenue>

<direction>North Bound</direction>

<latitude>41.925682067871094</latitude>

<longitude>-87.63092803955078</longitude> <pattern>2499</pattern>

<patternDirection>North Bound</patternDirection>

<run>P675</run>

<finalStop><[CDATA[Paulina & Howard Terminal]]></finalStop> <operator>42493</operator>

</bus> 

</buses>


使用SAX库,一个事件驱动的xml解析库

SAX主要是在处理大xml文件时,使用增量的处理而不用占用大量内存

但事件驱动 的特性也使得SAX在处理时相当低级与笨拙


可以像这样的分派事件到协程:

class EventHandler(xml.sax.ContentHandler):

              def __init__(self,target):

                  self.target = target

              def startElement(self,name,attrs):

                  self.target.send(('start',(name,attrs._attrs)))

              def characters(self,text):

                  self.target.send(('text',text))

              def endElement(self,name):

                  self.target.send(('end',name))




随后 ,作者又展示了一个使用expat的样例性能提升83%

使用 c语言写的扩展,又提升55%

xml.etree.cElementTree是快速xml解析,实验表明,协程和它性能不相上下



小结:协程与生成器相似

你可以创建一堆处理组件并连接它们到一起

可以通过创建管道,数据流图等 方法处理数据

也可以使用协程来完成相当有技巧的执行(比如,事件驱动的系统)



并行:

你发送数据给协程

也可以发送数据给线程(通过队列)

也可以发送数据给进程(通过消息)

协程自然就与线程和分布式系统问题联系在一起了。




可以通过在线程内部或进程内部添加一个额外的层打包协程


示例在这里:

# cothread.py

#

# A thread object that runs a coroutine inside it.  Messages get sent

# via a queue object


from threading import Thread

import queue

from coroutine import *


@coroutine

def threaded(target):#这是用来将数据推送到下一个协程的代码(处于新的线程里)

    messages = queue.Queue()

    def run_target():

        while True:

            item = messages.get()

            if item is GeneratorExit:

                target.close()

                return

            else:

                target.send(item)

    Thread(target=run_target).start()#为后纯

    try:#主线程协程的代码

        while True:

            item = (yield)

            messages.put(item)

    except GeneratorExit:

        messages.put(GeneratorExit)


# Example use


if __name__ == '__main__':

    import xml.sax

    from cosax import EventHandler

    from buses import *


    xml.sax.parse("/Users/ly/ws/allroutes.xml", EventHandler(

                    buses_to_dicts(

                    threaded(

                         filter_on_field("route","22",

                         filter_on_field("direction","North Bound",

                         bus_locations()))

                    ))))

                 

整个图的关系可能看起来有些奇怪



添加线程会使性能降低50%左右。





也可以使用多进程的管道将两个协程

# coprocess.py

#

# An example of running a coroutine in a subprocess connected by a pipe


import pickle

from coroutine import *


@coroutine

def sendto(f):

    try:

        while True:

            item = (yield)

            pickle.dump(item,f)

            f.flush()

    except StopIteration:

        f.close()


def recvfrom(f,target):

    try:

        while True:

            item = pickle.load(f)

            target.send(item)

    except EOFError:

        target.close()



# Example use

if __name__ == '__main__':

    import xml.sax

    #from cosax import EventHandler

    from buses import *


    import subprocess

    p = subprocess.Popen(['python','busproc.py'],

                         stdin=subprocess.PIPE)


    xml.sax.parse("allroutes.xml",

                  EventHandler(

                          buses_to_dicts(

                          sendto(p.stdin))))



(貌似pickle包有问题,上边的代码不能通过)



需要注意的是,你必须有足够的其他东西补偿多进程通讯造成的损失。

(比如,足够的多CPU和进程)



你可以使用协程将任务的实现从它的执行环境分隔开来

协程就是实现

而环境是你选择任意(线程,多进程,网络等 )



警告

使用大量的协程,线程,及进程很容易造成一个不可维护的程序。

也有可能使程序缓慢

需要仔细地研究以知道这些是不是一个好主意


一些隐藏的危险:

对一个协程的send调用必须恰当地同步

如果你对一个正在正在执行中的协程调用send ,你的程序会当掉

比如:多线程对同一个目标发送数据



限制:

你不能创建一个协程循环或环(即yield 对自己调用send)

堆栈化的send组成了一种调用栈

如果你调用一个正在协程推送中的进程,会得到一个错误

send并不挂起协程的执行


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值