标题
协程
概念
yield
gevent
asnycio
分布式进程
分布式系统
managers
协程
概念
协程(Coroutine)又称微程,纤程。
子程序(又名“函数”)在所有语言中都是“层级调用”,换言之,就是A调用B,B调用C,那么就要等待C执行完成后返回到B,B完成后返回到A,然后A执行完成。正如我们所知的,这种运行模式是通过栈来实现的。
协程看上去也像是子程序,但是在执行过程中,协程在子程序的内部可以中断,然后转而执行别的子程序,在某个恰当的时间点又转回来执行这个子程序。
协程看起来更像是多线程模式的运行,但实际上它只用了一个线程,因此相比多线程,协程拥有极高的运行效率,因为协程不需要由CPU调度切换,而由程序自己控制;并且协程还不需要使用多线程的锁机制,因为只有一个线程就不存在变量冲突,在协程中控制共享资源只需要判断状态即可,因此也没有死锁的风险。
如果我们要利用多核CPU,就可以通过多进程+协程的方式来进行,既充分地了利用了多核,又充分地发挥了协程地高效率,可以获得极高地性能。
yield
Python用于generator的yield可以一定程度上实现协程的功效。我们来看下面这个例子:
假设我们需要同时执行两个任务,一个任务用于生成一个数据,一个任务用于接收这个数据并打印出来,那么如果用多线程模式进行编程的话,我们就需要两个线程,一个线程写数据一个线程拿数据,并且通过锁机制和队列控制等待。如果用yield的话,则可以这么做
这段代码与我们之前常用的写法似乎有些区别,因为我们看到子程序outputter压根没有跟在return后面的返回值,但是不论是在主程序中,还是在子程序producer中,都接收了outputter的返回值并且二者接收的内容显然还是不一样的。
要理解这种情况,我们首先要明白yield是如何返回一个生成器(generator)的。
我们知道,代码for ... in ...可以将in后面的对象的内容迭代出来,这个过程是可以重复的,简单的说,像是执行了代码for x in listA一次,后面我们依然可以用for...in...语句来迭代listA,listA依然能够被迭代,因为列表listA是一个可迭代对象。
但是,如果我们有大量的数据要迭代,并且不希望把它们存在一个类似于列表这样的可迭代对象里面,因为这样会很占内存,那么我们就可以考虑使用生成器来完成这个工作。生成器也是一种迭代器,但它只能被迭代一次,因为它实时地生成这些值,而这些值一旦被迭代后就会被销毁,不再是先都存放内存当中然后一个一个迭代了。我们可以参考下面这个例程
我们可以看到,尽管我们运行了两次for...in...语句,但成功的只有第一次,因为a就是一个简单的生成器,它只能被迭代一次。(如果打印a,我们就会发现a是一个生成器。)
yield就是这么一个生成generator的关键词,它类似于return,在一个子程序中调用yield的时候,这个子程序的第一次运行仅会运行到有yield的位置然后直接结束
我们可以看到,这就是为什么主程序中变量b接收到的内容是一个生成器。而每当我们调用一次函数next()(也可以这样调用next(b),这个函数在generator对象内是通过实例方法__next__来编写的)时,就会执行一次生成器函数test_func内的循环,又因为我们添加的条件语句,使得需要n内有值才会继续运行,否则会return,因此我们还需要通过send()方法往生成器里面传输内容。
到此,我们就简单地了解到了yield实现协程工作方式的原理。接下来为大家介绍Python实现协程的两个模块gevent和asyncio。
gevent
但是yield在Python中算式比较低级的对协程模式的支持,它还有很多不完善的内容。第三方模块gevent就是一个提供了比较完善的协程支持的模块。
gevent通过greenlet实现协程:
当一个greenlet遇到IO操作时,它会自动切换到其他的greenlet执行任务,等到IO操作完成时又在适当的时候切换回来运行。我们直到IO操作通常比较耗时,因此有这种自动切换机制就可以让我们的任务在等待IO操作时可以“顺便”做点儿其他的事。
gevent需要修改Python自带的一些标准库,以便于能够在IO操作时自动完成切换,这一过程通过monkey patch来完成:
如果这样子使用的话,3个greenlet会依次运行。
要让greenlet交替运行,可以通过gevent.sleep()交出控制权:
这样做就可以实现交替运行了。
不过实际上我们在使用gevent的时候是当然不会去用gevent.sleep()去切换协程的,因为greenlet在执行到IO操作时,是会自动切换的:
可以看到,greenlet“start”后在IO操作期间会切换到其它greenlet运行。另外要注意,这里monkey调用的方法是monkey.patch_all(),和上一个例程不一样。
asnycio
asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。
asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。以下是例子
@asyncio.coroutine装饰器把一个generator标记为coroutine类型,然后把coroutine放在事件循环loop中执行。在上面的程序中,程序执行到yeild from,由于asyncio.sleep()也是一个协程,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。因此你会看到程序先打印了两个holle world。等待了大约1秒后asyncio.sleep()(这里我们可以把asyncio.sleep(1)看作要耗时1秒的IO操作)返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。最后打印了了两个holle again,并且由打印信息我们可以发现以上协程均在同一个线程上并发执行。
在Python3.5后,引入了新的关键字asyns和await。asyns和await分别与@asyncio.coroutine和yield from等价,使得协程的代码更简洁易读。所以上面的协程holle()可以写为
分布式编程
分布式系统
我们知道,相比多线程,多进程的效果往往要更好,但是如果任务数量实在庞大,在一台机器上进行有限的多进程工作模式也往往很难吃的消。这个时候,我们就需要把多进程分配到多台机器上通过网络互相通信进行协同工作,相比最多只能分布到多个CPU的线程,这种工作模式的效率会有非常大的提升。
这种建立在网络之上的软件系统,就称为分布式系统(distributedsystem)
分布式系统有两大特点:内聚性和透明性
内聚性:每一个数据库节点高度自治,有本地的数据库管理系统。
透明性:每一个数据库分布节点对于用户的应用来说都是透明的,是无法区分本地还是远程的。
现在我们来了解一下,如何使用Python搭建分布式进程工作模式。
managers
Python中multiprocessing的子模块managers支持把多进程分布到多台机器上,一个服务进程可以作为调度者来将任务分布到其它的多个进程当中,并依靠网络进行互相通信。由于managers的模块封装好了,所以在Python中我们调用它时可以不需要了解网络通信的底层细节,就可以直接进行分布式多进程程序的编写:
我们现在假设我们有3个数据要传到另一台机器上,希望另一台机器将这3个数据进行加密(例如把它们都加上10)后返回给原来的这台机器,我们可以通过传输队列对象Queue来进行这个任务的实现:
这是作为第一台机器调度加密任务并接受加密后数据信息的例程manager.py
上面是子进程运行代码worker.py
我们先启动manager.py,可以看到
说明主进程开始运行,并且在等待结果。然后我们运行子进程代码worker.py
成功连接到主进程,并获取了数据进行处理,然后传输回主进程
主进程得到加密后的数据结果,并将其打印出来。
这便是一个简单的分布式运算,其中,Queue对象就存储在主进程manager.py中,worker.py没有创建Queue对象而是直接对manager.py中的Queue对象做出修改。
资料来源:
廖雪峰Python教程
Python初学分布式的疑问 CSDN
以及网上各位大佬的博文
AI遇见机器学习
mltoai
领取专属 10元无门槛券
私享最新 技术干货