zoukankan      html  css  js  c++  java
  • EffectivePython并发及并行

    第40条:考虑用协程来并发地运行多个函数

    线程有三个显著的缺点:

    为了确保数据安全,我们必须使用特殊的工具来协调这些线程。这使得多线程的代码,要比单线程的过程式代码更加难懂。这种复杂的多线程代码,会逐渐另程序变得难于扩展和维护。

    线程需要占用大量内存,每个正在执行的线程,大约占据8MB内存。

    线程启动时的开销比较大。如果程序不停地依靠创建新线程来同时执行多个函数,并等待这些线程结束,那么使用线程所引发的开销,就会拖慢整个程序的速度。

    Python的协程(coroutine)可以避免上述问题,它使得Python程序看上去好像是在同时运行多个函数。写成的实现方式,实际上是对生成器的一种扩展。启动生成器协程所需的开销,与调用函数的开销相仿。处于活跃状态的协程,在其耗尽之前,只会占用不到1KB的内存。

    协程的工作原理:每当生成器函数执行到yield表达式的时候,消耗生成器的那段代码,就通过send方法给生成器回传一个值。而生成器在收到了经由send函数所传进来的这个值之后,会将其视为yield表达式的执行结果。

     1 def my_coroutine():
     2     while True:
     3         received = yield
     4         print('Received:', received)
     5 it = my_coroutine()
     6 next(it)  # Prime the coroutine
     7 it.send('First')
     8 it.send('Second')
     9 
    10 >>>
    11 Received:First
    12 Received:Second

    评估完当前的yield表达式之后,生成器还会继续推进到下一个yield表达式那里,并把那个yield关键字右侧的内容,当成send方法的返回值,返回给外界。

    在生成器上面调用send方法之前,我们要先调用一次next函数,以便将生成器推进到第一条yield表达式那里。此后,我们可以把yield操作与send操作结合起来,令生成器能够根据外界所输入的数据,用一套标准的流程来产生对应的输出值。

    def minimize():
        current = yield
        while True:
            value = yield current
            current = min(value, current)
    
    it = minimize()
    next(it) # Prime the generator
    print(it.send(10))
    print(it.send(4))
    print(it.send(22))
    print(it.send(-1))

    与线程类似,协程也是独立的函数,它可以消耗由外部环境所传进来的输入数据,并产生相应的输出结果。但与线程不同的是,协程会在生成器函数中的每个yield表达式那里暂停,等到外界再次调用send方法之后,它才会继续执行到下一个yield表达式。

    这种奇妙的机制,使得消耗生成器的那段代码,可以在每执行完协程中的一条yield表达式之后,就进行相应的处理。例如,那段代码可以用生成器所产生的输出值,来调用其他函数,并更新程序的数据结构。更为重要的是,它可以通过这个输出值,来推进其他的生成器函数,使得那些生成器函数也执行到它们各自的下一条yield表达式处。接连推进多个独立的生成器,即可模拟出Python线程的并发行为,令程序看上去好像是在同时运行多个函数。

    Query-->向生成器协程提供一种手段,使得协程能够借此向外围环境查询相关的信息。

    下面这个协程,会针对本细胞的每一个相邻细胞,来产生与之对应的Query对象。每个yield表达式的结果,要么是ALIVE,要么是EMPTY。这就是协程与消费代码之间接口契约。其后,count_neighbors生成器会根据相邻细胞的生存状态,来返回本细胞周边的存活细胞个数。

     1 from collections import namedtuple
     2 def count_neighbors(y, x): # 获取相邻细胞的生存状态
     3     Query = namedtuple('Query', ('y', 'x'))
     4     n_ = yield Query(y + 1, x + 0)  # North
     5     ne = yield Query(y + 1, x + 1)  # Northeast
     6     e_ = yield Query(y + 0, x + 1)
     7     se = yield Query(y - 1, x + 1)
     8     s_ = yield Query(y - 1, x + 0)
     9     sw = yield Query(y - 1, x - 1)
    10     w_ = yield Query(y + 0, x - 1)
    11     nw = yield Query(y + 1, x - 1)
    12 
    13     neighbor_status = [n_, ne, e_, se, s_, sw, w_, nw]
    14     count = 0
    15     for state in neighbor_status:
    16         if state == 'ALIVE':
    17             count += 1
    18     return count
    19 
    20 it = count_neighbors(10, 5)
    21 q1 = next(it)
    22 print('First yield: ', q1)
    23 q2 = it.send('ALIVE')
    24 print('Second yield:', q2)
    25 q3 = it.send('ALIVE')
    26 print('Third yield: ', q3)
    27 q4 = it.send('ALIVE')
    28 print('4th yield:', q4)
    29 q5 = it.send('ALIVE')
    30 print('5th yield: ', q5)
    31 q6 = it.send('ALIVE')
    32 print('6th yield:', q6)
    33 q7 = it.send('ALIVE')
    34 print('7th yield: ', q7)
    35 q8 = it.send('ALIVE')
    36 print('8th yield:', q8)
    37 
    38 try:
    39     count = it.send('ALIVE')
    40 except StopIteration as e:
    41     print('Count: ', e.value)  # Value from return statement
    42 
    43 >>>
    44 First yield:  Query(y=11, x=5)
    45 Second yield: Query(y=11, x=6)
    46 Third yield:  Query(y=10, x=6)
    47 4th yield: Query(y=9, x=6)
    48 5th yield:  Query(y=9, x=5)
    49 6th yield: Query(y=9, x=4)
    50 7th yield:  Query(y=10, x=4)
    51 8th yield: Query(y=11, x=4)
    52 Count:  8

    用虚构的数据测试这个count_neighbors协程。针对本细胞的每个相邻细胞,向生成器索要一个Query对象,并根据该对象给出那个相邻细胞的存活状态。然后,通过send方法把状态发给协程,使count_neighbors协程可以收到上一个Query对象所对应的状态。最后,由于协程中的return语句会把生成器耗竭,所以程序届时将抛出StopIteration异常,而我们可以在处理该异常的过程中,得知本细胞周边的存活细胞数量。

    count_neighbors协程把相邻的存活细胞数量统计出来之后,我们必须根据这个数量来更新本细胞的状态,于是,就需要用一种方式来表示状态的迁移。-->step_cell协程,这个生成器会产生Transition对象,用以表示本细胞的状态迁移。

    step_cell协程会通过参数来接收当前细胞的网格坐标。它会根据此坐标产生Query对象,以查询本细胞的初始状态。接下来,它运行count_neighbors协程,以检视本细胞周边的其他细胞。此后,它运行game_logic函数,以判断本细胞在下一轮应该处于何种状态。最后,它生成Transition对象,把本细胞在下一轮所应有的状态,告诉外部代码。

    1 def game_logic(state, neighbors):
    2     pass
    3 
    4 def step_cell(y, x):
    5     state = yield Query(y, x)
    6     neighbors = yield from count_neighbors(y, x)
    7     next_state = game_logic(state, neighbors)
    8     yield Transition(y, x, next_state)

    请注意,step_cell协程用yield from表达式来调用count_neighbors。在Python程序中,这种表达式可以把生成器协程组合起来,使开发者能够更加方便地复用小段的功能代码,并通过简单的协程来构建复杂的协程。count_neighbors协程耗竭之后,其最终的返回值(也就是return语句的返回值)会作为yield from表达式的结果,传给step_cell。

     1 # 测试step_cell协程
     2 from collections import namedtuple
     3 Query = namedtuple('Query', ('y', 'x'))
     4 Transition = namedtuple('Transition', ('y', 'x', 'state'))
     5 def count_neighbors(y, x):
     6     n_ = yield Query(y + 1, x + 0)  # North
     7     ne = yield Query(y + 1, x + 1)  # Northeast
     8     e_ = yield Query(y + 0, x + 1)
     9     se = yield Query(y - 1, x + 1)
    10     s_ = yield Query(y - 1, x + 0)
    11     sw = yield Query(y - 1, x - 1)
    12     w_ = yield Query(y + 0, x - 1)
    13     nw = yield Query(y + 1, x - 1)
    14 
    15     neighbor_status = [n_, ne, e_, se, s_, sw, w_, nw]
    16     count = 0
    17     for state in neighbor_status:
    18         if state == 'ALIVE':
    19             count += 1
    20     return count
    21 
    22 def game_logic(state, neighbors):
    23     if state == 'ALIVE':
    24         if neighbors < 2:
    25             return 'EMPTY'
    26         elif neighbors > 3:
    27             return 'EMPTY'
    28     else:
    29         if neighbors == 3:
    30             return 'ALIVE'
    31     return state
    32 
    33 def step_cell(y, x):
    34     state = yield Query(y, x)
    35     neighbors = yield from count_neighbors(y, x)
    36     next_state = game_logic(state, neighbors)
    37     yield Transition(y, x, next_state)
    38 
    39 it = step_cell(10, 5)
    40 q0 = next(it)  # initial location query
    41 print('Me: ', q0)
    42 q1 = it.send('ALIVE')  #send my status, get neighbor query
    43 print('Q1:', q1)
    44 q2 = it.send('ALIVE')
    45 print('Q2: ', q2)
    46 q3 = it.send('ALIVE')
    47 print('Q3:', q3)
    48 q4 = it.send('ALIVE')
    49 print('Q4: ', q4)
    50 q5 = it.send('ALIVE')
    51 print('Q5: ', q5)
    52 q6 = it.send('ALIVE')
    53 print('Q6: ', q6)
    54 q7 = it.send('ALIVE')
    55 print('Q7:', q7)
    56 try:
    57     count = it.send('ALIVE')
    58 except StopIteration as e:
    59     print('Count: ', e.value)  # Value from return statement
    60 
    61 t1 = it.send('EMPTY')
    62 print('Outcome: ', t1)

    生命游戏的目标,是要同时在网格中的每个细胞上面,运行刚才编写的那套游戏逻辑。为此,我们把step_cell协程组合到新的simulate协程之中。新的协程,会多次通过yield from表达式,来推进网格中的每一个细胞。把每个坐标点中的细胞都处理完之后,simulate协程会产生TICK对象,用以表示当前这代的细胞已经全部迁移完毕。

  • 相关阅读:
    JSR 303
    Spring JSR-250注解
    java 内部类
    爬虫
    多线程异步编程示例和实践-Task
    多线程异步编程示例和实践-Thread和ThreadPool
    线程机制、CLR线程池以及应用程序域
    二维码的生成与识别
    Unicode 和 UTF-8 有何区别?
    json转换为自定义类型的集合
  • 原文地址:https://www.cnblogs.com/liushoudong/p/12356276.html
Copyright © 2011-2022 走看看