今天学习了并发编程中的最后一部分,协程,也是python中区别于java,c等语言中很大不一样的一部分html
1.协程产生的背景java
2.协程的概念python
3.yield模拟协程编程
4.协程中主要的俩个模块安全
5.协程的应用网络
开始今日份总结多线程
1.协程产生的背景并发
以前咱们学习了线程、进程的概念,了解了在操做系统中进程是资源分配的最小单位,线程是CPU调度的最小单位。按道理来讲咱们已经算是把cpu的利用率提升不少了。可是咱们知道不管是建立多进程仍是建立多线程来解决问题,都要消耗必定的时间来建立进程、建立线程、以及管理他们之间的切换。app
随着咱们对于效率的追求不断提升,基于单线程来实现并发又成为一个新的课题,即只用一个主线程(很明显可利用的cpu只有一个)状况下实现并发。这样就能够节省建立线进程所消耗的时间。异步
为此咱们须要先回顾下并发的本质:切换+保存状态
cpu正在运行一个任务,会在两种状况下切走去执行其余的任务(切换由操做系统强制控制),一种状况是该任务发生了阻塞,另一种状况是该任务计算的时间过长
ps:在介绍进程理论时,说起进程的三种执行状态,而线程才是执行单位,因此也能够将上图理解为线程的三种状态
一:其中第二种状况并不能提高效率,只是为了让cpu可以雨露均沾,实现看起来全部任务都被“同时”执行的效果,若是多个任务都是纯计算的,这种切换反而会下降效率。
二:第一种状况的切换。在任务一遇到io状况下,切到任务二去执行,这样就能够利用任务一阻塞的时间完成任务二的计算,效率的提高就在于此。
对于单线程下,咱们不可避免程序中出现io操做,但若是咱们能在本身的程序中(即用户程序级别,而非操做系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另一个任务去计算,这样就保证了该线程可以最大限度地处于就绪态,即随时均可以被cpu执行的状态,至关于咱们在用户程序级别将本身的io操做最大限度地隐藏起来,从而能够迷惑操做系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给咱们的线程。
协程的本质就是在单线程下,由用户本身控制一个任务遇到io阻塞了就切换另一个任务去执行,以此来提高效率。为了实现它,咱们须要找寻一种能够同时知足如下条件的解决方案:
#1. 能够控制多个任务之间的切换,切换以前将任务的状态保存下来,以便从新运行时,能够基于暂停的位置继续执行。 #2. 做为1的补充:能够检测io操做,在遇到io操做的状况下才发生切换
2.协程的概念
协程:在其余语言中不多去用,在python中很是重要的点,对于操做系统来讲,线程已是操做系统可以看到的最小单位,操做系统没法感知协程
协程利用切换来规避I/O操做带来的好处
在pthon中,协程是很是重要的。
3.yield模拟协程
那么如今就用yield来模拟协程,毕竟yield也是能够在代码级别记录状态
#代码以下,yield本质是保存如今的状态,send是调用其余函数 def pro(): print(1) n = yield 'a' print(n) yield 'b' def com(): g = pro() a = next(g) print(a) b = g.send(2) print(b) com()
代码执行顺序以下
相比于串行的去执行,单纯的用yield只会让时间更长
下面用yield测试一下以前用到的生产者消费者模型
#单纯的生产者,消费者模型 import time def consumer(res): '''单纯的处理数据''' pass def producer(): res =[] for i in range(10000000): pass return res start = time.time() res = producer() consumer(res) end = time.time() print(end-start) #结果 0.347031831741333 #用yield模式尝试 import time def consumer(): while True: x = yield def producer(): g = consumer() next(g) for i in range(10000000): g.send(i) start = time.time() #并发的执行任务 producer() end = time.time() print(end-start) #结果 1.8232519626617432
能够看出来,单纯线程之间俩个任务的切换时很可浪费时间的,若是数据量大存储数据也是很须要时间的,每一次切换都须要记住当前的状态,切换回去须要读取以前的状态。
若是咱们遇到I/0操做的时候能够自动切换,而且I/O阻塞时间能够和执行代码共享这段时间,才是真正的提升了程序的执行率,yield只是保存了状态。
能够用yield实现一个协程的操做。
4.协程中主要的俩个模块
协程中的主要有俩个模块,俩个模块都是第三方模块,既然是第三方模块那就先说明一下,第三方模块的导入方法
这个时候须要俩个第三方模块,一个是gevent,一个是greenlet,不过gevent是greenlet的上层模块,,gevent规避I/O操做,判断程序中的I/O操做,遇到I/O就切换到另外一个任务去执行。greenlet主要是俩个任务之间的切换,状态的保存以及读取
4.1 greenlet模块
安装 :pip3 install greenlet
查看代码
import greenlet def eat(): print('eat1') g2.switch() print('eat2') g2.switch() def sleep(): print('sleep1') g1.switch() print('sleep2') g1 = greenlet.greenlet(eat) g2 = greenlet.greenlet(sleep) g1.switch() #结果 eat1 sleep1 eat2 sleep2
greenlet 模块只是记录了状态而且在切换回去的是读取了状态,并无真正意思的自动规避I/O操做
4.2 gevent模块
这个时候就须要了gevent模块了
安装:pip3 install gevent
Gevent 是一个第三方库,能够轻松经过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet所有运行在主程序操做系统进程的内部,但它们被协做式地调度。
#gevent模块的使用方法 g1=gevent.spawn(func,1,,2,3,x=4,y=5)建立一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面能够有多个参数,能够是位置实参或关键字实参,都是传给函数eat的 g2=gevent.spawn(func2) g1.join() #等待g1结束 g2.join() #等待g2结束 #或者上述两步合做一步:gevent.joinall([g1,g2]) g1.value#拿到func1的返回值
先运用最基本的协程函数
import gevent def eat(): print('eat1') gevent.sleep(1) print('eat2') def sleep(): print('sleep1') gevent.sleep(1) print('sleep2') g1 = gevent.spawn(eat)#实例化一个gevent对象 g2 = gevent.spawn(sleep)#实例化一个gevent对象 gevent.joinall([g1,g2])#监测到有I/O就切换
上例gevent.sleep(2)模拟的是gevent能够识别的io阻塞,而time.sleep(2)或其余的阻塞,gevent是不能直接识别的须要用下面一行代码,打补丁,就能够识别了from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块以前,或者咱们干脆记忆成:要用gevent,须要将from gevent import monkey;monkey.patch_all()放到文件的开头。
查看更改后的代码
from gevent import monkey monkey.patch_all()#用来匹配全部的I/O操做 import gevent import time def eat(): print('eat1') time.sleep(1) print('eat2') def sleep(): print('sleep1') time.sleep(1) print('sleep2') g1 = gevent.spawn(eat)#实例化一个gevent对象 g2 = gevent.spawn(sleep)#实例化一个gevent对象 gevent.joinall([g1,g2])#监测到有I/O就切换
最后咱们来看一下协程的id号,代码以下
from gevent import monkey monkey.patch_all() import gevent import time from threading import currentThread def eat(): print('eat:',currentThread()) print('eat1') time.sleep(1) print('eat2') def sleep(): print('sleep:',currentThread()) print('sleep1') time.sleep(1) print('sleep2') g1 = gevent.spawn(eat) g2 = gevent.spawn(sleep) gevent.joinall([g1,g2]) #结果以下 eat: <_DummyThread(DummyThread-1, started daemon 53379528)> eat1 sleep: <_DummyThread(DummyThread-2, started daemon 53380480)> sleep1 eat2 sleep2
咱们能够用threading.current_thread().getName()来查看每一个g1和g2,查看的结果为DummyThread-n,即假线程
5.协程的应用
对于协程通常使用比较多的地方为网络I/O以及sleep操做,不过通常咱们程序代码基本是不会去使用sleep操做,因此平常用的比较多的就是网络爬虫以及socket.server
5.1 网络爬虫简易
看代码
#普通打开方式 import time from urllib import request def func(name,url): ret = request.urlopen(url)#获取网页 with open(name+'.html','wb') as f: f.write(ret.read()) url_lst = [ ('python','https://www.python.org/'), ('blog','http://www.cnblogs.com/Eva-J/articles/8324673.html'), ('pypi','https://pypi.org/project/pip/'), ('blog2','https://www.cnblogs.com/z-x-y/p/9237706.html'), ('douban','https://www.douban.com/') ] start = time.time() for url_item in url_lst: func(*url_item) end = time.time() print('普通打开方式',end-start) #协程打开方式 from gevent import monkey monkey.patch_all() import gevent from urllib import request import time def func(name,url): ret = request.urlopen(url) with open(name+'2.html','wb')as f: f.write(ret.read()) url_lst = [ ('python','https://www.python.org/'), ('blog','http://www.cnblogs.com/Eva-J/articles/8324673.html'), ('pypi','https://pypi.org/project/pip/'), ('blog2','https://www.cnblogs.com/z-x-y/p/9237706.html'), ('douban','https://www.douban.com/') ] start = time.time() g_list =[] for url_item in url_lst: g = gevent.spawn(func,*url_item) g_list.append(g) gevent.joinall(g_list) end = time.time() print('协程打开方式',end-start)
看结果
普通打开方式 6.35495924949646
协程打开方式 1.931349754333496
咱们会发现如今在少许的url情况下是这样,若是在大量的代码下,这个时间就会缩减的更多。
补充:这个是我在测试的时候发现的情况,在已有文件,打开文件并从新写入文件内容,耗费的时间会高不少!
在爬虫的时候仍是用协程,这样会更快的拿到咱们须要的数据并对其做出分析!
5.2 用协程实现socket.server
看代码
#服务端
#服务端 import socket from gevent import monkey monkey.patch_all() import gevent def talk(conn): while True: msg = conn.recv(1024).decode() conn.send(msg.upper().encode('utf-8')) sk =socket.socket() sk.bind(('127.0.0.1',8500)) sk.listen() while True: conn,addr = sk.accept() gevent.spawn(talk,conn)
#客户端
import socket sk = socket.socket() sk.connect(('127.0.0.1',8500)) while True: msg = input('--->').encode('utf-8') sk.send(msg) recv_msg = sk.recv(1024).decode('utf-8') print(recv_msg) sk.close()
任何基础知识都是看着简单,运用难,多练习就好啦!