11.python并发入门(part12 初识协程)

一、协程的简介。

协程,又被称为微线程,虽然是单进程,单线程,但是在某种情况下,在python中的协程执行效率会优于多线程。

这是因为协程之间的切换和线程的切换是完全不一样的!协程的切换是由程序自身控制的(程序的开发者使用yield去进行控制,协程和协程之间的切换是可控制的,想什么时候切换就什么时候切换)。

当使用多线程时,开的线程越多,协程的优势就越明显。

协程的另一个优点,就是无需锁机制,因为协程只有一个进程,和线程,不存在多线程或者多进程之间访问公共资源的冲突,所以说,在协程中无需加锁,如果多个协程之间要操作同一个公共资源,那么只需要做个对协程的状态做一个判断就可以了。

协程其实是可以利用cpu多核或者多个cpu的,想要实现并行效果,就要使用,多进程+协程,这样既可以保证并行,又可以保证协程的高效率。

(协程的优势:1,没有锁的概念。2,协程之间互相切换开销比多线程,多进程之间要小的多!)

刚刚在前面也说过了,协程是基于函数中的yield关键字去实现的!

如果对yield关键字,或者生成器的概念不了解,请单击下面的传送门,里面有详细的介绍。

http://suhaozhi.blog.51cto.com/7272298/1909032

二、快速回顾yield与生成器的快速回顾。

上面的连接有详细介绍,在这里再次快速回顾一下。

下面是例子:

例1:

#首先定义了一个函数func1。

def func1():

print "ok!"

yield

#然后去调用func1这个函数。

func1()

#此时print “ok!” 到底会不会被执行?

可以试着去运行一下,其实 print "ok!"这句话是不会被执行的!

因为python在检测到一个函数中有yield关键字的时候,这个函数就已经不再是个普通的函数了,这个函数就会变成一个生成器对象。

我们可以使用type函数看到这一现象。

print type(func1())

输出的结果:

<type ‘generator‘>

1.关于yield实现迭代器对象的回顾。

1.1 如果想要让生成器中的代码执行,必须只能用next函数,或者是先使用__iter__方法去获取一个   迭代器对象,然后执行这个迭代器的next方法或者send方法才可以触发生成器的执行。

(当第一次运行一个生成器的时候!必须只能用next函数,或者是先使用__iter__方法去获取一个迭代器对象,然后执行这个迭代器的next方法去触发运行!否则会报错!)

def func1():

print "ok!"

yield

next(func1())    #执行了next函数后,print "ok!"这句话才真正的被执行了!

#func1().__iter__().next()  #第二种调动方法,先获取一个迭代器对象,然后在执行迭代器对象下的next方法。

输出结果:

ok!

从上面的例子可以看出,使用yield关键字创建的生成器的第一个特性,就是想触发生成器的执行,只能用next函数或者send方法来触发生成器的执行。

1.2 yield和return一样,可以return一个返回值!

例:

#!/usr/local/bin/python2.7

# -*- coding:utf-8 -*-

def func1():

print "ok!"

yield [1,2,3]  #返回一个列表

l1 = next(func1())

print l1

输出结果:

ok!

[1, 2, 3]

1.3 yield的挂起特性。

def func1():

print "1"

yield

print "2"

yield

print "3"

yield

print "4"

g1 = func1()

next(g1) #第一次执行next,生成器会先执行到第一个yield关键字的位置挂起,并且保存当前运行的位置状态。

#输出结果:

1

next(g1) #第二次执行next,生成器会从刚刚挂起的位置继续执行后面的代码,如果再次执行到yield关键字后,生成器继续保存状态后挂起。

#输出结果:

2

next(g1) #第三次执行next,生成器会继续从刚刚挂起的位置开始运行,继续执行后面的代码,如果遇到yield,继续保存状态挂起。

#输出结果:

3

next(g1) #第四次执行next,生成器依旧会从刚刚挂起的位置继续执行后面的代码,但是不同的是,后面没有yield了!!生成器执行完print "4"这个代码后,就会抛出一个StopIteration的异常。

1.4 生成器的send方法,可以给yield前面的变量名赋值的特性。

#!/usr/local/bin/python2.7

# -*- coding:utf-8 -*-

def func1():

print "1"

name = yield 11111

print name

yield

g1 = func1()

v1 = g1.next() #如果想直接给yield左边的name变量传值,必须要先运行next然后再send!否则会报错!!

g1.send("test!!!") #通过yield给左边的name变量赋值

输出结果:

1

11111

test!!!

三、使用yield来实现最基本的协程。

个人理解,协程的底层就是基于yield去实现的,之前在说多线程的时候,有说过一个生产者消费者模型,我们可以通过yield实现协程,然后通过协程去实现这个模型。

在写基于协程的生产者消费者模型之前,先来补充几个概念:

传统的生产者消费者模型的实现,是通过一个线程去产生消息,另一个线程去取消息,如果要对公共数据进行操作,必须要加锁机制,但是一不小心可能会造成“死锁”现象。

通过协程去实现的生产者消费者模型,生产者产生消息后,直接可以通过yield跳转到消费者的函数开始执行,当消费者的函数运行完毕后,继续跳回生产者的函数继续生产,效率比多线程要高很多。

1.下面是一个使用yield关键字实现的一个简单的协程。

usr/local/bin/python2.7

# -*- coding:utf-8 -*-

#这个程序的思路是,生产者函数产生了数据之后,通过yield跳转到消费者的生成器函数,等待消费者执行完毕后

#继续切换回生产者函数进行生产.

import time

def consumer(name):  #这里的消费者函数是个生成器!

ret = ‘‘

while True:

bun_num = yield ret

if not bun_num:

return

print "%s eating bun %s " %(name,bun_num)

time.sleep(1)

ret = ‘consumer ----> go chi so u sa ma de shi ta ~!‘

def produce(con):

next(con) #第一次要先使用next函数去启动consumer生成器!(相当于通知)

num = 0

while num < 5:

num = num + 1

print  "producer --> producing %s" %(num)

consumer_return = con.send(num) #一旦产生了数据!切换到consumer(消费者),并且,把产生的数据,发送给consumer。

print  "producer ---> consumer return message: %s" %(consumer_return)

#当生产者拿到了消费者的返回结果,输出的一条消息。

con.close()  #当生产者停止生产,关闭consumer,这个close方法用于终止迭代。

if __name__ == ‘__main__‘:

con1 = consumer("suhaozhi") #创建消费者对象

produce(con1)

输出结果:

producer --> producing 1

suhaozhi eating bun 1

producer ---> consumer return message: consumer ----> go chi so u sa ma de shi ta ~!

producer --> producing 2

suhaozhi eating bun 2

producer ---> consumer return message: consumer ----> go chi so u sa ma de shi ta ~!

producer --> producing 3

suhaozhi eating bun 3

producer ---> consumer return message: consumer ----> go chi so u sa ma de shi ta ~!

producer --> producing 4

suhaozhi eating bun 4

producer ---> consumer return message: consumer ----> go chi so u sa ma de shi ta ~!

producer --> producing 5

suhaozhi eating bun 5

producer ---> consumer return message: consumer ----> go chi so u sa ma de shi ta ~!

#从上面输出结果的速度来看,生产者和消费者的执行是一种并发效果。

代码分析:

首先来说consumer生成器函数,consumer(消费者)通过yield拿到了producer(生产者发来的数据),

接着又通过这个yield把处理结果返回给producer(生产者),看了上述代码,我们可以发现yield还具有和return一样的功能,执行到了yield这个关键字后,函数的运行状态就被挂起,一直等待到函数调用者下一次执行next函数或者执行send方法或者for 循环的时候,这个函数才会从上次挂起的地方继续运行。

执行完这个代码你会发现,生产者和消费者之间是协作执行的,完全没有用到锁机制。(这是因为协程和协程之间完全不会像多线程一样会抢占资源。)

四、实现协程的另一种方式(greenlet)。

greenlet是python中自带的一个协程模块,它比yield实现的协程更加灵活,简单,而且它无需将一个函数声明为生成器(generator)。

下面来介绍并演示一下greenlet的基本使用。

常用方法:

greenlet.greenlet() 生成一个greenlet对象,在生成greenlet对象之前,可以传入两个参数,分别是run和parent,run用来传入一个可调用对象,parent用来传入一个父greenlet,默认就是当前的greenlet。

greenlet.switch() 这个方法用于协程和协程之间的切换,并且,switch是可以给greenlet对象传递参数的。

下面是个greenlet基本使用的示范:

import greenlet

def func1():

print "func1 start!"

print "1,2,3"

gr2.switch()  #切换到func2函数

print "7,8,9"

gr2.switch()

def func2():

print "func2 start!"

print "4,5,6"

gr1.switch() #切换到func1函数

print "10,11,12"

gr1 = greenlet.greenlet(run=func1)

gr2 = greenlet.greenlet(run=func2)

gr1.switch()  #启动greenlet对象gr1.

可能会用到的方法:

gr1 = greenlet.greenlet(run=func1)

gr1.switch(*args, **kwargs)  #切换到指定函数(在例子中是func1函数),并且可以给这个函数传参数。

gr1.dead 一旦当这个greenlet对象死了,返回一个True。

gr1.bool 判断这个greenlet对象是否处于活动状态,如果是活动状态返回一个True。

五、更简单的协程实现,gevent模块。

虽然我们在python内部可以通过yield去实现一个基本的协程,但是它的功能并不是很完全,gevent提供了更加完全的功能支持。

gevent和greenlit虽然都是实现协程的模块,但是它们还是有不同之处的。

greenlit:当遇到一个I/O操作的时候(访问网络,写文件之类的),需要切换greenlit对象,等到I/O操作结束后在切回来,这样反复的来回执行。(使用greenlit模块时,你需要手动找出函数中的所有执行I/O操作的位置,手动在程序中指定切换。)

gevent:gevent和greenlit最大的不同就是,它比greenlit操作起来更简单,它可以做到自动检测I/O操作,自动去切换协程。

下面是使用示例:(一个扒网页的例子)

from gevent import monkey

monkey.patch_all() #可以理解为阻塞的类型

import gevent

import urllib

import time

def get_web(url):

print "GET:%s" %(url)

resp = urllib.urlopen(url)

data = resp.geturl()

print "%d bytes recv from %s" %(len(data),url)

start_time = time.time()

gevent.joinall([gevent.spawn(get_web, ‘http://www.baidu.com/‘),gevent.spawn(get_web, ‘https://www.github.com/‘),gevent.spawn(get_web, ‘http://zhihu.com/‘)])

print "over! %s" %(time.time()-start_time)

monkey.patch_all():用过gevent就会知道,会在最开头的地方gevent.monkey.patch_all();把标准库中的thread/socket等给替换掉.这样我们在后面使用socket的时候可以跟平常一样使用,无需修改任何代码,但是它变成非阻塞的了.

注意!!Monkey patching能够使得gevent修改标准库里面大部分的阻塞式系统调用,包括socket,ssl,threading和select等模块,而变成协作式运行!!!

下面在补充一个官方文档提供的示例:

import gevent

from gevent import socket

urls = [‘www.google.com.hk’,’www.example.com’, ‘www.python.org’ ]

jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]

gevent.joinall(jobs, timeout=2)

[job.value for job in jobs]

gevent.spawn()方法创建一些任务,然后通过gevent.joinall将任务加入到微线程执行队列中等待其完成,设置超时为2秒。执行后的结果通过检查gevent.Greenlet.value值来收集。gevent.socket.gethostbyname()函数与标准的socket.gethotbyname()有相同的接口,但它不会阻塞整个解释器,因此会使得其他的greenlets跟随着无阻的请求而执行。

关于协程的东西远远不止这么多....这只是开始...

时间: 2024-11-08 01:46:14

11.python并发入门(part12 初识协程)的相关文章

Python并发编程-进程 线程 协程

一.进程 进程:就是一个程序在一个数据集上的一次动态执行过程. 进程由三部分组成: 1.程序:我们编写的程序用来描述进程要完成哪些功能以及如何完成 2.数据集:数据集则是程序在执行过程中所需要使用的资源 3.进程控制块:进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感 知进程存在的唯一标志. 二.线程                                                                        

15.python并发编程(线程--进程--协程)

一.进程:1.定义:进程最小的资源单位,本质就是一个程序在一个数据集上的一次动态执行(运行)的过程2.组成:进程一般由程序,数据集,进程控制三部分组成:(1)程序:用来描述进程要完成哪些功能以及如何完成(2)数据集:是程序在执行过程中所需要使用的一切资源(3)进程控制块:用来记录进程外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志.3.进程的作用:是想完成多任务并发,进程之间的内存地址是相互独立的二.线程:1.定义:最小的执行单位,线程的出现是为了

Python并发编程(线程队列,协程,Greenlet,Gevent)

线程队列 线程之间的通信我们列表行不行呢,当然行,那么队列和列表有什么区别呢? queue队列 :使用import queue,用法与进程Queue一样 queue is especially useful in threaded programming when information must be exchanged safely between multiple threads. class queue.Queue(maxsize=0) #先进先出 import queue #不需要通过

python并发编程(二):协程

'''协程: 1. 协程的定义: 1) 是一种用户态的轻量级线程, 即协程是由用户程序自己控制调度的 2) 是一种协作而非抢占式的处理并发方式, A --> B ---> A --> C 3) 协程的切换属于程序级别的, 操作系统不需要切换 2. 协程的特点: 1) 协程本身是一个线程, 是用户态的切换 2) 相比线程优点: 1> 切换没有消耗 2> 修改共享程序不需要加锁 3) 相比线程缺点: 一旦引入协程,就需要检测单线程下所有的IO行为, 实现遇到IO就切换,少一个都不

11.python并发入门(part13 了解事件驱动模型))

一.事件驱动模型的引入. 在引入事件驱动模型之前,首先来回顾一下传统的流水线式编程. 开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束 每一个代码块里是完成各种各样事情的代码,但编程者知道代码块A,B,C,D...的执行顺序,唯一能够改变这个流程的是数据.输入不同的数据,根据条件语句判断,流程或许就改为A--->C--->E...--->结束.每一次程序运行顺序或许都不同,但它的控制流程是由输入数据和

11.python并发入门(part1 初识进程与线程,并发,并行,同步,异步)

一.什么是进程? 在说什么是进程之前,需要先插入一个进程切换的概念! 进程,可以理解为一个正在运行的程序. 现在考虑一个场景,假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源.你是不是已经想到在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让程序B暂停.这当然没问题,但这里有一个关键词:切换. 既然是切换,那么这就涉及到了状态的保存,状态的恢复,加上程序A与

11.python并发入门(part2 threading模块的基本使用)

一.在使用python多线程之前,你需要知道的. python的多线程中,实现并发是没有问题的,但是!!是无法实现真正的并行的. 这是因为python内部有个GIL锁(全局解释器锁),这个锁限制了在同一时刻,同一个进程中,只能有一个线程被运行!!! 二.threading模块的基本使用方法. 可以使用它来创建线程.有两种方式来创建线程. 1.通过继承Thread类,重写它的run方法. 2.创建一个threading.Thread对象,在它的初始化函数__init__中将可调用对象作为参数传入.

11.python并发入门(part7 线程队列)

一.为什么要用队列? 队列是一种数据结构,数据结构是一种存放数据的容器,和列表,元祖,字典一样,这些都属于数据结构. 队列可以做的事情,列表都可以做,但是为什么我们还要去使用队列呢? 这是因为在多线程的情况下,列表是一种不安全的数据结构. 为什么不安全?可以看下面这个例子: #开启两个线程,这两个线程并发从列表中移除一个元素. import threading import time l1 = [1,2,3,4,5] def pri(): while l1: a = l1[-1] print a

11.python并发入门(part11 进程同步锁,以及进程池,以及callback的概念)

一.关于进程锁. 其实关于进程锁没啥好讲的了,作用跟线程的互斥锁(又叫全局锁也叫同步锁)作用几乎是一样的. 都是用来给公共资源上锁,进行数据保护的. 当一个进程想去操作一个公共资源,它就可以给公共资源进程"上锁"的操作,其他进程如果也想去访问或者操作这个公共资源,那么其他的进程只能阻塞,等待刚刚的进程把锁释放,下一个进程才可以对这个公共资源进行操作. 下面是个关于进程锁的使用示范: #!/usr/local/bin/python2.7 # -*- coding:utf-8 -*- im