python协程入门

并发

并发编程是指在一台处理器上“同时”(同一个时间段)处理多个任务。 并发是在同一实体上的多个事件。 多个事件在同一时间间隔发生。

比如,多个线程任务在同一个CPU上快速地轮换执行,由于切换的速度非常快,给人的感觉就是这些线程任务是在同时进行的,但其实并发只是一种逻辑上的同时进行。

举个例子,你有一台单CPU的电脑,在上面打开了一个视频播放软件,这样就启动了一个进程,但是这个进程内部有2个线程,一个用于显示视频,另一个用于播放声音。在任意一个时刻,只有一个线程占用CPU,所以视频于音频并不是同时播放的,只是因为线程切换速度快到无法察觉,从宏观上看起来是“同时”的。

协程

协程,又称微线程,英文名Coroutine。实际上,协程是运行在单线程中的“并发”,程序员通过高超的代码能力,在代码执行流程中人为的实现多任务并发,是单个线程内的任务调度技巧。协程相比多线程的一大优势就是省去了多线程之间的切换开销,获得了更高的运行效率。

特点:

  1. 线程的切换由操作系统负责调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
  2. 协程切换需要的Stack远小于线程,因此可以在相同的内存中开启更多的协程。
  3. 由于在同一个线程上,因此可以避免出现竞争和死锁。
  4. 适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好实用线程去解决。

python协程

yield

python中协程的原始实现方式是通过yield关键字实现,而yield关键字在python中用于实现生成器。

yield的语法规则是这样的:代码执行时遇到yield则停止执行,返回yield右侧的值(没有则返回None),遇到next(或者send)命令则从yield处继续执行,并将send命令的参数传给yield的左值变量。

看个例子:

def example_coroutine():
    print('-> 启动协程')
    y = 10
    x = yield y
    print('-> 协程接收到了x的值:', x)

ex_coro = example_coroutine()
res = next(ex_coro)  # 或者ex_coro.send(None)
print(res)
ex_coro.send(10)

## 控制台输出:
-> 启动协程
10
-> 协程接收到了x的值: 10
Traceback (most recent call last):
  File "d:/exercise/algorithm_exercise/py_coroutine.py", line 15, in <module>
    ex_coro.send(10)
StopIteration

过程是这样的:next唤醒生成器,生成器返回变量y的值10,res接收到生成器的返回值,send(10)将10传入生成器并赋值给变量x,最后生成器结束,抛出StopIteration异常。

实际上到这里,已经实现了一个简易的协程,通过yield和send实现任务切换。

下面的例子表示如何通过协程实现主任务与副任务切换:

import time

def task1():
    while True:
        count = yield "<甲>累了,让<乙>工作一会儿"
        if count:
            print("<甲>开始工作.....")
            time.sleep(1)
        else:
            break

def task2(t):
    # next(t)
    a = t.send(None)  # a = "<甲>累了,让<乙>工作一会儿"
    count = 2
    while count:
        print("-----------------------------------")
        print("<乙>开始工作.....")
        time.sleep(2)
        print("<乙>累了,让<甲>工作一会儿....")
        ret = t.send(count)
        print(ret)
        count-=1
    t.close()

if __name__ == '__main__':
    t = task1()
    task2(t)
    
# 控制台输出
-----------------------------------
<乙>开始工作.....
<乙>累了,让<甲>工作一会儿....
<甲>开始工作.....
<甲>累了,让<乙>工作一会儿
-----------------------------------
<乙>开始工作.....
<乙>累了,让<甲>工作一会儿....
<甲>开始工作.....
<甲>累了,让<乙>工作一会儿

asyncio模块

asyncioPython 3.4版本引入的标准库,直接内置了对异步IO的支持。asyncio是一个基于事件循环的异步IO模块。

yield from可以返回另一个生成器生成的值。yield详细介绍点这里

asyncio可以分成三个过程:

  1. 创建事件循环
  2. 指定循环模式并运行
  3. 关闭循环

要注意的是,简单地调用一个协程并不会使其被调度执行。

通常我们使用asyncio.get_event_loop()方法创建一个循环。

循环创建后可以通过run_until_complete()方法或者run_forever()方法来运行循环

import asyncio
import threading

@asyncio.coroutine  # 声明为协程函数
def hello1():
    print('Hello 1 start! (%s)' % threading.currentThread())
    yield from asyncio.sleep(1)  # 遇到IO,出让线程
    print('Hello 1 end! (%s)' % threading.currentThread())

@asyncio.coroutine
def hello2():
    print('Hello 2 start! (%s)' % threading.currentThread())
    yield from asyncio.sleep(1)
    print('Hello 2 end! (%s)' % threading.currentThread())

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [hello1(), hello2()]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()
    
# 控制台输出
Hello 2 start! (<_MainThread(MainThread, started 21500)>)
Hello 1 start! (<_MainThread(MainThread, started 21500)>)
# 在这里阻塞1s
Hello 2 end! (<_MainThread(MainThread, started 21500)>)
Hello 1 end! (<_MainThread(MainThread, started 21500)>)

上面一段代码,使用asyncio模拟了同一个线程中的两个协程。不像上面的例子,分为主副协程,这个例子中的hello1和hello2属于平等,并不是由某个协程直接唤醒另外一个协程。虽然hello1再task列表中排在第一位,先执行的确是hello2 协程。

具体过程为@asyncio.coroutine装饰器声明协程,调用协程并不会使其被执行。asyncio.sleep(1)模拟1s的IO操作。hello2协程先执行,遇到IO操作时出让CPU,然后hello1协程被调度,同样遇到IO出让CPU,hello2的asyncio.sleep(1)结束后,由yield from返回hello2协程,获取CPU资源。因此虽然模拟了2s的IO操作,但是2次IO同时进行,程序总耗时大约1s。

async、await

Python3.5中对协程提供了更直接的支持,引入了async/await关键字。使用async代替@asyncio.coroutine,使用await代替yield from,代码变得更加简洁可读。从Python设计的角度来说,async/await让协程独立于生成器而存在,不需要再借助yield语法。asyncawait必须成双成对使用。

下面使用async和await重写上面的代码:

import asyncio
import threading

async def hello3():
    print('Hello 3 start! (%s)' % threading.currentThread())
    await asyncio.sleep(1)
    print('Hello 3 end! (%s)' % threading.currentThread())

async def hello4():
    print('Hello 4 start! (%s)' % threading.currentThread())
    await asyncio.sleep(1)
    print('Hello 4 end! (%s)' % threading.currentThread())

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [hello3(), hello4()]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()
    
# 控制台输出
Hello 3 start! (<_MainThread(MainThread, started 15288)>)
Hello 4 start! (<_MainThread(MainThread, started 15288)>)
Hello 3 end! (<_MainThread(MainThread, started 15288)>)
Hello 4 end! (<_MainThread(MainThread, started 15288)>)

怎么用协程

开头介绍了一些协程与线程的区别以及协程的优点,但是事实上大部分情况下,多线程都能很好的完成任务,而并不是花心思写协程。

目前,主流的代码结构设计理念需要先对要解决的问题进行抽象、分解、建模,强调的是高内聚低耦合的模块化思想。模块之间不应该相互影响,这对于互相基本不影响的多线程来说是很自然的,对于底层资源的竞争都交给操作系统来解决,上层的代码层面就可以做到模块化。而对于协程,却并不是这样,协程把资源的分配提到用户态,由程序员在代码中实现逻辑流,不可避免地会使得一些代码相互耦合。

但是多线程也不是万能的,抢断式线程调度下,操作系统对于不同线程的时间片分配使公平的,公平有时候就会导致效率低下。另外,涉及到线程非独立的时候会遇到更多的问题,比如A线程需要B线程产生的资源,那么操作系统即使切换到A线程,也会因为资源不足而必须再切回去;又比如多线程操作同一个资源,需要进行线程安全处理。这些情况下,主动出让式的协程效率则高得多。

在如今的python程序中,协程绝大多数情况下用来处理网络IO或者文件IO,但是很多情况下也是可以从协程的角度去设计代码的,很多问题从协程角度去思考可能会别有一番天地。


  上一篇
语言模型概述 语言模型概述
语言模型(Language Model),是对语句的概率分布的建模。对于语言模型,输入为字或者单词组成的序列,输出为这个序列的概率。
2021-05-03
下一篇 
Attention is all you need Attention is all you need
这是Google公司在2017年发表的一篇论文,论文提出了一种新的结构来“代替”RNN或者CNN的结构,确实是一种比较新颖的操作。
2021-03-10
   目录