爬虫的并发控制:
多进程、多线程、协程 yield
从硬件:
双核四线程(超线程技术):
有两个CPU核心,每个核心有两个逻辑处理器,相当于有四个CPU核心
四核四线程:
有一个CPU核心,每个核心有一个逻辑处理器,相当于有四个CPU核心
从操作系统:
进程和线程,都是CPU任务的执行单位。
进程:早期的操作系统是面向进程的:
表示一个程序的执行活动(打开、执行、保存、关闭)
线程:现在的操作系统都是面向线程:
表示一个进程处理任务时最小调度单位(执行功能a、执行功能b)
一个程序至少开启一个进程,一个进程至少有一个线程。
每个进程都有独立的内存空间,不同进程之间不共享任何状态。
进程之间的通信需要经过操作系统调度控制,通讯效率低、切换开销大。
同一个进程里的多个线程,是共享内存空间,切换开销小,通讯效率高。
线程的工作机制是"抢占式",出现竞争的状态,竞争意味着数据不安全。
引入了"互斥锁":让多个线程安全有序的访问内存空间的机制。
Python的多线程:
类似于 GIL(全局解释器锁):保证一个时间片里只有一个线程在运行。
好处:直接杜绝了多个线程的竞争问题:
坏处:Python的多线程不是真正的多线程。
Python解释器在处理IO阻塞类型的方法时,会释放GIL
如果没有IO操作,该线程会每隔 sys.getcheckinterval() 次释放GIL,让其他线程尝试执行。
并行:
同一CPU时间片内,CPU可以同时处理多个程序。如果有多个程序,同步执行。
程序1:----------------
程序2:----------------
程序3:----------------
程序4:----------------
并发:
同一CPU时间片,只能处理一个程序。如果有多个程序,交替执行。
程序1: ----- ------
程序2: -----
程序3: ----
程序4: -----
多进程:可以充分利用多核CPU的资源,适用于密集CPU任务(大量的并行运算)
Python的多进程模块:multiprocessing
进程之间通信成本高切换开销大,不适用于需要大量数据通信和切换的任务(爬虫)
设计模式:生产者消费者(并行模式)
多线程:适用于密集I/O任务(磁盘IO,内存IO,网络IO),切换开销小通信成本低。
Python的多线程:Thread、thraeding、multiprocessing.dummy
多线程:同一个时间片只能执行一个线程,无法充分利用CPU多核资源(只能做到并发,不能做到并行)
协程:操作系统和CPU不认识协程,是由程序员通过代码逻辑控制。
特点是在单线程下执行多个任务,且不需要通过操作系统切换(没有切换开销,也不需要处理锁),执行效率高。
Python: gevent ,猴子补丁(Python代码在执行网络IO阻塞时,会自动切换协程)
协程:适用于密集网络I/O任务
多进程爬虫:不合适
多线程爬虫:缺点 - 通过操作系统调度,有线程切换开销(海量URLs场景会增加CPU负载);优点 - 使用场景广泛(网络读写并发/数据库读写并发/磁盘读写并发)
协程爬虫:缺点 - gevent配合monkey.patch_all() 只能提高网络并发效率,不能处理其他并发场景;优点 - 通过程序员代码逻辑控制,不受操作系统调度,没有切换开销,降低CPU负载(处理海量URLs优势明显)
执行方式:
同步:执行一个任务,必须等待上一个任务完成(没有并发的爬虫)
异步:执行一个任务,不必等待上一个任务完成(并发的爬虫)
程序状态:
阻塞:程序执行时,必须等待该任务完成,否则保持等待状态。
非阻塞:程序执行时,不必等待该任务完成,可以继续执行下一个任务。
异步+非阻塞(效率最高):
发送请求后,不必等待响应返回,可以继续处理其他请求发送;当处理功能挂起时,能够立刻切换其他到功能继续执行。
异步网络框架 Twisted Tornada
Scrapy :请求处理模块+响应解析模块+twisted
scrapy-redis:Scrapy + Redis(在同一个数据库里处理请求去重、请求分配、数据存储)
单机爬虫:
分布式爬虫:
CPU -> 寄存器 -> CPU缓存L1/L2/L3 -> 内存 -> 硬盘/固态硬盘 -> 网络
协程:切换
三种方式实现: 1. yield 2.greenlet 3. gevent
1.yield
作用:
挂起当前函数 将后面表达式的值 返回到调用生成器的地方
接收数据 并唤醒当前函数 并且紧接着上次运行的地址继续执行
2.greenler
greenlet(函数) 创建协程
gr2.switch()切换到gr2执行
协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)
通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定
创建并执行协程
阻塞等待协程运行完成 .join()
阻塞等待所有协程退出 .join_all()
monkey.patch_all()作用是将rece, recefrom, time.sleep, accept进行破解, 不会阻塞等待, 在调用时可以切换到别的任务继续执行.
注意:join,join_all作用保持主进程存活
简单总结
- 进程是资源分配的单位
- 线程是操作系统调度的单位
- 进程切换需要的资源很最大,效率很低
- 线程切换需要的资源一般,效率一般
- 协程切换任务资源很小,效率高
- 多进程、多线程根据cpu核数不一样可能是并行的 也可能是并发的。协程的本质就是使用当前进程在不同的函数代码中切换执行,可以理解为并行。 协程是一个用户层面的概念,不同协程的模型实现可能是单线程 也可能是多线程。
来源:https://www.cnblogs.com/snailon/p/8945840.html