一、铺垫:基于socket发送http请求

1、需求一:向百度发送请求搜索关键字“alex”,有如下两种方式:

  1. import requests
  2. ret = requests.get('https://www.baidu.com/s?wd=alex')

方式一(用requests模块):

  1. import socket
  2. sk = socket.socket()
  3. # 与百度创建连接: 阻塞
  4. sk.connect(('www.baidu.com',80))
  5. # 跟说百度我要什么?
  6. sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
  7. # 等着接收百度给我的回复
  8. chunk_list = []
  9. while 1:
  10. chunk = sk.recv(8096)
  11. if not chunk:
  12. break
  13. chunk_list.append(chunk)
  14. body = b''.join(chunk_list)
  15. print(body.decode('utf8'))

方式二(socket方式,也是requests的原理):

2、需求二:向百度发送请求搜索三个关键字

  1. import requests
  2. key_list = ['alex','db','sb']
  3. for item in key_list:
  4. ret = requests.get('https://www.baidu.com/s?wd=%s' %item)

方式一:

  1. import socket
  2. def get_data(key):
  3. client = socket.socket()
  4. # 跟百度创建连接: 阻塞
  5. client.connect(('www.baidu.com',80))
  6. # 跟百度说我要什么?
  7. client.sendall(b'GET /s?wd='+key.encode('utf-8')+b' HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
  8. # 我等着接收百度给我的回复
  9. chunk_list = []
  10. while True:
  11. chunk = client.recv(8096)
  12. if not chunk:
  13. break
  14. chunk_list.append(chunk)
  15.  
  16. body = b''.join(chunk_list)
  17. print(body.decode('utf-8'))
  18.  
  19. key_list = ['alex','db','sb']
  20. for item in key_list:
  21. get_data(item)

方式二:

  分析上述需求二的代码,我们发现,这两种方式去向浏览器发送请求的时候都是串行的,也就是等第一个请求得到相应之后再发送下一个请求,并没有实现并发。现在你可能会想:可以创建多线程来分别去发送请求,代码如下:

  1. # #################### 解决并发:多线程 ####################
  2. import threading
  3.  
  4. key_list = ['alex','db','sb']
  5. for item in key_list:
  6. t = threading.Thread(target=get_data,args=(item,))
  7. t.start()

  多线程虽然提高了效率,实现了并发,但是同时也浪费了资源,那我们想一下能不能用单线程实现并发,也就是这个线程去发送完第一个请求(IO请求)后不等待相应结果,而是直接去发送第二个请求,再继续发送第三个请求,等请求响应之后才去处理响应结果,这样就实现了单线程并发,即节省了资源又实现了并发,那具体怎么实现呢?首先需要解决两个问题:第一:如何判断是IO请求?第二:如何知道响应数据回来了?

二、基于IO多路复用+socket实现单线程并发

  1.   # ################ 解决并发:单线程+IO不等待 ################
  2.   import socket
  3.   import select
  4.  
  5.   client1 = socket.socket()
  6.   client1.setblocking(False) # 将原来阻塞的位置变成非阻塞(报错)
  7.   try:
  8.   client1.connect(('www.baidu.com',80))
  9.   except BlockingIOError as e:
  10.   pass
  11.  
  12.   client2 = socket.socket()
  13.   client2.setblocking(False) # 将原来阻塞的位置变成非阻塞(报错)
  14.   try:
  15.   client2.connect(('www.sogou.com',80))
  16.   except BlockingIOError as e:
  17.   pass
  18.  
  19.   client3 = socket.socket()
  20.   client3.setblocking(False) # 将原来阻塞的位置变成非阻塞(报错)
  21.   try:
  22.   client3.connect(('www.sina.com.cn',80))
  23.   except BlockingIOError as e:
  24.   pass
  25.  
  26.   socket_list = [client1,client2,client3]
  27.   conn_list = [client1,client2,client3]
  28.  
  29.   while True:
  30.   rlist,wlist,elist = select.select(socket_list,conn_list,[],0.005)
  31.   # rlist中表示已经接收到数据的socket对象
  32.   # wlist中表示已经连接成功的socket对象
  33.   for sk in wlist:
  34.   if sk == client1:
  35.   sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
  36.   elif sk == client2:
  37.   sk.sendall(b'GET /web?query=fdf HTTP/1.0\r\nhost:www.sogou.com\r\n\r\n')
  38.   else:
  39.   sk.sendall(b'GET /mid/search.shtml?q=alex HTTP/1.0\r\nhost:www.sina.com.cn\r\n\r\n')
  40.   conn_list.remove(sk)
  41.   for sk in rlist:
  42.   chunk_list = []
  43.   while True:
  44.   try:
  45.   chunk = sk.recv(8096)
  46.   if not chunk:
  47.   break
  48.   chunk_list.append(chunk)
  49.   except BlockingIOError as e:
  50.   break
  51.   body = b''.join(chunk_list)
  52.   print('------------>',body)
  53.   sk.close()
  54.   socket_list.remove(sk)
  55.   if not socket_list:
  56.   break

上面示例可以进行封装,但是封装前先来看这样两段代码:

  1. # 代码一:
  2. v = [
  3. [11,22], # 每个都有一个append方法
  4. [22,33], # 每个都有一个append方法
  5. [33,44], # 每个都有一个append方法
  6. ]
  7. for item in v:
  8. print(item.append)
  1. # 代码二(为了不改变for循环代码,可以进行如下封装)
  2. class Foo(object):
  3. def __init__(self,data):
  4. self.row = data
  5.  
  6. def append(self,item):
  7. self.row.append(item)
  8. v = [
  9. Foo([11,22]), # 每个都有一个append方法
  10. Foo([22,33]), # 每个都有一个append方法
  11. Foo([33,44]), # 每个都有一个append方法
  12. ]
  13.  
  14. for item in v:
  15. print(item.append)
  1. # ############## 单线程并发高级版:封装上面示例 ##############
  2. import socket
  3. import select
  4.  
  5. class Req(object):
  6. def __init__(self,sk,func):
  7. self.sock = sk
  8. self.func = func
  9.  
  10. def fileno(self):
  11. return self.sock.fileno()
  12.  
  13. class Nb(object):
  14. def __init__(self):
  15. self.conn_list = []
  16. self.socket_list = []
  17.  
  18. def add(self,url,func):
  19. client = socket.socket()
  20. client.setblocking(False) # 非阻塞
  21. try:
  22. client.connect((url, 80))
  23. except BlockingIOError as e:
  24. pass
  25. obj = Req(client,func)
  26. self.conn_list.append(obj)
  27. self.socket_list.append(obj)
  28.  
  29. def run(self):
  30. while True:
  31. rlist,wlist,elist = select.select(self.socket_list,self.conn_list,[],0.005)
  32. for sk in wlist:
  33. # 发生变换的req对象
  34. sk.sock.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
  35. self.conn_list.remove(sk)
  36. for sk in rlist:
  37. chunk_list = []
  38. while True:
  39. try:
  40. chunk = sk.sock.recv(8096)
  41. if not chunk:
  42. break
  43. chunk_list.append(chunk)
  44. except BlockingIOError as e:
  45. break
  46. body = b''.join(chunk_list)
  47. sk.func(body)
  48. sk.sock.close()
  49. self.socket_list.remove(sk)
  50. if not self.socket_list:
  51. break
  52.  
  53. def baidu_repsonse(body):
  54. print('百度下载结果:',body)
  55.  
  56. def sogou_repsonse(body):
  57. print('搜狗下载结果:', body)
  58.  
  59. def sina_repsonse(body):
  60. print('新浪下载结果:', body)
  61.  
  62. t1 = Nb()
  63. t1.add('www.baidu.com',baidu_repsonse)
  64. t1.add('www.sogou.com',sogou_repsonse)
  65. t1.add('www.sina.com.cn',sina_repsonse)
  66. t1.run()

封装版

总结:

1、socket默认是否是阻塞的?阻塞体现在哪里?

是,体现在等待连接和等待接收数据。

2、如何让socket编程非阻塞?

通过设置client.setblocking(False)

3、IO多路复用作用?

检测多个socket是否已经发生变化(是否已经连接成功/是否已经获取数据)(可写/可读)

操作系统检测socket是否发生变化,有三种模式:

select:最多1024个socket,循环去检测;

poll:不限制监听socket个数,循环去检测(水平触发);

epoll:不限制监听socket个数,回调方式(边缘触发);

Python模块:

select.select

select.epoll(windows不支持,linux中可以用)

4、提高并发方案:

- 多进程

- 多线程

- 异步非阻塞模块(Twisted), 爬虫中学的scrapy框架(内部是用单线程完成并发)

5、什么是异步非阻塞?

- 非阻塞,不等待。

比如创建socket对某个地址进行connect、获取接收数据recv时默认都会等待(连接成功或接收到数据),才执行后续操作。

如果设置setblocking(False),以上两个过程就不再等待,但是会报BlockingIOError的错误,只要捕获即可。

- 异步,通知,执行完成之后自动执行回调函数或自动执行某些操作(通知)。

比如做爬虫中向某个地址baidu.com发送请求,当请求执行完成之后自动执行回调函数。

6、什么是同步阻塞?

- 阻塞:等

- 同步:按照顺序逐步执行,例如:

  1. key_list = ['alex','db','sb']
  2. for item in key_list:
  3. ret = requests.get('https://www.baidu.com/s? wd=%s' %item)
  4. print(ret.text)

三、协程

进程和线程都是操作系统中存在的,而协程是由程序员创造出来的一个不是真实存在的东西。

协程:是微线程,对一个线程进行分片,使得线程在代码块之间进行来回切换执行,而不是原来的逐行执行。如下示例:

  1. import greenlet
  2. # 引入greenlet模块帮助我们实现协程,安装方式:pip3 install greenlet
  3.  
  4. def f1():
  5. print(11)
  6. gr2.switch()
  7. print(22)
  8. gr2.switch()
  9.  
  10. def f2():
  11. print(33)
  12. gr1.switch()
  13. print(44)
  14.  
  15. gr1 = greenlet.greenlet(f1) # 创建协程 gr1
  16. gr2 = greenlet.greenlet(f2) # 创建协程 gr2
  17.  
  18. gr1.switch() # 执行协程gr1

创建协程

  分析:单纯的协程没有意义,反而可能会让性能降低,那么协程的存在意义在哪里呢?结合上面单线程实现并发的示例,思考一下假如当我们执行了一段代码后遇到IO操作,此时我们不再等待,而是切换到另一段代码去执行,然后遇到IO操作的时候再去切换,这样是不是也能提高性能,实现并发,但是greenlet只能做协程,不能实现遇到IO就切换,所以协程如果再加上遇到IO就切换,那么便能实现单线程并发了。那么谁能做到遇到IO就切换呢?那就是另外一个模块geven,安装方法:pip3 install gevent。

  gevent内部要依赖greenlet,也就是greenlet + IO切换,所以gevent就牛逼了!写法如下:

  1.   from gevent import monkey
  2.   monkey.patch_all() # 以后代码中遇到IO都会自动执行greenlet的switch进行切换
  3.   import requests
  4.   import gevent
  5.  
  6.   def get_page1(url):
  7.   ret = requests.get(url)
  8.   print(url,ret.content)
  9.  
  10.   def get_page2(url):
  11.   ret = requests.get(url)
  12.   print(url,ret.content)
  13.  
  14.   def get_page3(url):
  15.   ret = requests.get(url)
  16.   print(url,ret.content)
  17.  
  18.   gevent.joinall([
  19.   gevent.spawn(get_page1, 'https://www.python.org/'), # 创建协程1
  20.   gevent.spawn(get_page2, 'https://www.yahoo.com/'), # 创建协程2
  21.   gevent.spawn(get_page3, 'https://github.com/') # 创建协程3
  22.   ])

  上面通过gevent实现了单线程并发,提高了效率,通过对比,我们发现,上面IO多路复用的示例中是一个线程在不停的执行,而是gevent是在代码间进行切换,虽然原理不行,但是都提高了效率,实现单线程并发。

总结:

1、协程可以提高并发吗?

协程自己本身无法实现并发,甚至性能会降低,而协程+IO切换性能就可以提高了。

2、单线程提高并发的方法有哪些?

a、协程+遇到就IO切换:gevent;   注意:不是异步,无回调函数,但本质也是基于事件循环

b、基于时间循环的异步非阻塞框架:Twisted;

3、线程、进程、协程的区别?

    进程cpu资源分配的最小单元,主要用来做数据隔离,那么线程是cpu工作的最小单元,一个应用程序可以有多个进程(默认有一个),一个进程可以有多个线程(默认有一个),这是它们的一个简单区别;基本上在其他语言中没有进程这个概念,大都用线程,而在python中由于有GIL锁,它保证了同一时刻一个进程中只能有一个线程被cpu调度,为了利用多核优势就要创建多个进程,多线程没有用,所以计算密集型的用多进程,IO密集型的用多线程就行,因为IO操作不占用CPU。而协程是程序员人为创造出来的不真实存在的,它可以让程序员控制代码执行顺序,在函数之间来回切换,本身协程存在没有意义,但是能跟IO切换放在一起就厉害了,相当于将线程切片,程序遇到IO就切换到其他代码,IO完成后再切回来,达到让线程不停去工作的效果,实现协程的模块是greenlet,实现协程+IO切换的模块是gevent,这就是三者的区别。

  4、手动实现协程:yield关键字生成器(没有意义,了解即可)

  1. def f1():
  2. print(11)
  3. yield
  4. print(22)
  5. yield
  6. print(33)
  7.  
  8. def f2():
  9. print(55)
  10. yield
  11. print(66)
  12. yield
  13. print(77)
  14.  
  15. v1 = f1()
  16. v2 = f2()
  17.  
  18. next(v1) # v1.send(None)
  19. next(v2) # v1.send(None)
  20. next(v1) # v1.send(None)
  21. next(v2) # v1.send(None)
  22. next(v1) # v1.send(None)
  23. next(v2) # v1.send(None)

手动实现协程

IO多路复用、协程的更多相关文章

  1. Python IO 多路复用 \协程

    IO 多路复用 作用:  检测多个socket是否已经发生变化(是否已经连接成功/是否已经获取数据) 即(可读/可写) IO请求时 解决并发  :  单线程 def get_data(key): cl ...

  2. IO多路复用,协程

    https://www.cnblogs.com/wangjun187197/p/9642429.html Python之路--协程/IO多路复用 I/O复用模型 此模型用到select和poll函数, ...

  3. Python异步IO之协程(一):从yield from到async的使用

    引言:协程(coroutine)是Python中一直较为难理解的知识,但其在多任务协作中体现的效率又极为的突出.众所周知,Python中执行多任务还可以通过多进程或一个进程中的多线程来执行,但两者之中 ...

  4. 进程&线程(三):外部子进程subprocess、异步IO、协程、分布式进程

    1.外部子进程subprocess python之subprocess模块详解--小白博客 - 夜风2019 - 博客园 python subprocess模块 - lincappu - 博客园 之前 ...

  5. day41 - 异步IO、协程

    目录 (见右侧目录栏导航) - 1. 前言- 2. IO的五种模型- 3. 协程    - 3.1 协程的概念- 4. Gevent 模块    - 4.1 gevent 基本使用    - 4.2 ...

  6. 异步IO(协程,消息循环队列)

    同步是CPU自己主动查看IO操作是否完成,异步是IO操作完成后发出信号通知CPU(CPU是被通知的) 阻塞与非阻塞的区别在于发起IO操作之后,CPU是等待IO操作完成再进行下一步操作,还是不等待去做其 ...

  7. python---异步IO(asyncio)协程

    简单了解 在py3中内置了asyncio模块.其编程模型就是一个消息循环. 模块查看: from .base_events import * from .coroutines import * #协程 ...

  8. Python异步IO之协程(二):使用asyncio的不同方法实现协程

    引言:在上一章中我们介绍了从yield from的来源到async的使用,并在最后以asyncio.wait()方法实现协程,下面我们通过不同控制结构来实现协程,让我们一起来看看他们的不同作用吧- 在 ...

  9. python-gevent模块(自动切换io的协程)

    2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import gevent     def foo() ...

  10. 异步IO和协程

    1-1.并行:真的多任务执行(CPU核数>=任务数):即在某个时刻点上,有多个程序同时运行在多个CPU上 1-2.并发:假的多任务执行(CPU核数<任务数):即一段时间内,有多个程序在同一 ...

随机推荐

  1. MySQL学习总结(三)索引

    补充一下,上一章节中约束的一点东西.我们在为约束设置名称的时候(标识符)推荐写法“约束缩写_字段名”,这样让人看起来就会很清晰.例如:FK_Deptno,我们通过索引的名字就可以大概知道这是一个设置的 ...

  2. JaunsGraph数据模型

    JanusGraph采用邻接表(adjacency list)的方式存储图,也即图以顶点(vertex)和其邻接表组成.邻接表中保存某个顶点的所有入射边(incident edges). 通过将图采用 ...

  3. Windows下MySQL備份與還原

    方法一 備份: C:\...\MySQL\MySQL Server 5.1\bin\>mysqldump aa -u root -p > d:\aaa.sql.bak 還原: C:\... ...

  4. Mysql 5.7.24 解压版安装步骤

    1.设置 MYSQL_HOME 变量(在mysql解压根目录下) 例如:C:\Program Files\mysql-5.7.24 2.系统path 变量最后面增加 %MYSQL_HOME%\bin ...

  5. IDEA 2017破解 license server激活

    确保电脑在联网状态,在激活窗口选择license server 填入下面的license server: http://intellij.mandroid.cn/ http://idea.imsxm. ...

  6. 虚拟机设置bios第一启动为u盘

    虚拟机可以用u盘启动吗?虚拟机如何设置u盘启动?今天u启动小编亲自为大家编写u启动制作的u盘启动盘在虚拟机中的进入u盘启动的教程: 总共三步骤:安装创建虚拟机和准备u启动u盘 - 虚拟机添加u盘设备  ...

  7. python django -4 模板

    模板介绍 作为Web框架,Django提供了模板,可以很便利的动态生成HTML 模版系统致力于表达外观,而不是程序逻辑 模板的设计实现了业务逻辑(view)与显示内容(template)的分离,一个视 ...

  8. SharePoint2013导入Excel到列表

    using Microsoft.SharePoint; using System; using System.Collections.Generic; using System.ComponentMo ...

  9. 基于ormlite创建数据库存储数据案例

    一直不知道安卓创建数据库存储数据,以前遇到过,但是没有深入研究,今天仔细的看了一下,学习到了一点知识 直接看代码了 public class DatabaseHelper extends OrmLit ...

  10. sublime text3 安装配置

    sublime text 3 语法检查插件(一直都是安装了但是却没有语法报错提示和苦恼) 第一种方法:有点卡 先去下载对应的开发环境,安装到本地,例如php. 从Pakage Control中安装su ...