本文共 7287 字,大约阅读时间需要 24 分钟。
我最近一直在做优化的问题,数据库操作优化还有加速爬取数据。 之前用过多进程、多线程、和协程池,因此考虑再次 用出来。 因为考虑过少,对于这两个问题我都使用了多进程+协程池,但是两个问题的效果差别很大, 爬取数据问题得到了解决。但是数据库优化还是没有很大进展。 归根结底还是底层的基础知识打的不牢,对多进程、多线程、协程池的原理和适用范围不清楚, 因此,在此对这些基础进行学习。
https://www.cnblogs.com/guolei2570/p/8810536.html
https://blog.csdn.net/daaikuaichuan/article/details/82951084
https://www.liaoxuefeng.com/wiki/897692888725344/923057403198272
https://zhuanlan.zhihu.com/p/70256971
https://mp.weixin.qq.com/s?__biz=MzIxMjY5NTE0MA==&mid=2247483720&idx=1&sn=f016c06ddd17765fd50b705fed64429c&scene=21#wechat_redirect
多进程形式,允许多个任务同时运行
多线程形式,允许单个任务分成不同的部分运行;
提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源
是资源分配的基本单位;
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动
一个进程最少包含一个线程,也可以包含多个线程
通信方式:管道(Pipe)、命名管道(FIFO)、消息队列(Message Queue) 、信号量(Semaphore) 、共享内存(Shared Memory);套接字(Socket)。
是操作系统调度(CPU调度)执行的最小单位。
线程有自己的栈,这个栈仍然是使用进程的地址空间,只是这块空间被线程标记为了栈。每个线程都会有自己私有的栈,这个栈是不可以被其他线程所访问的
调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。进程所维护的是程序所包含的资源(静态资源), 如:地址空间,打开的文件句柄集,文件系统状态,信号处理handler等;线程所维护的运行相关的资源(动态资源),如:运行栈,调度相关的控制信息,待处理的信号集等;系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;资源分配给进程,同一进程的所有线程共享该进程的所有资源;处理机分给线程,即真正在处理机上运行的是线程;线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
进程与线程的亲缘性
亲缘性的意思是进程/线程只在某个cpu上运行(多核系统)
BOOL WINAPI SetProcessAffinityMask( _In_ HANDLE hProcess, _In_ DWORD_PTR dwProcessAffinityMask);/*dwProcessAffinityMask 如果是 0 , 代表当前进程只在cpu0 上工作;如果是 0x03 , 转为2进制是 00000011 . 代表只在 cpu0 或 cpu1上工作;*/
使用CPU亲缘性的好处:设置CPU亲缘性是为了防止进程/线程在CPU的核上频繁切换,从而避免因切换带来的CPU的L1/L2 cache失效,cache失效会降低程序的性能。
这副图是一个双向多车道的道路图,假如我们把整条道路看成是一个“进程”的话,那么图中由白色虚线分隔开来的各个车道就是进程中的各个“线程”了。
①这些线程(车道)共享了进程(道路)的公共资源(土地资源)。 ②这些线程(车道)必须依赖于进程(道路),也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。 ③这些线程(车道)之间可以并发执行(各个车道你走你的,我走我的),也可以互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)。 ④这些线程(车道)之间依靠代码逻辑(交通灯)来控制运行,一旦代码逻辑控制有误(死锁,多个线程同时竞争唯一资源),那么线程将陷入混乱,无序之中。 ⑤这些线程(车道)之间谁先运行是未知的,只有在线程刚好被分配到CPU时间片(交通灯变化)的那一刻才能知道这个例子让我有了新的idea, 我现在的需求就是有8个子文件,一个总文件, 需要判断总文件中的每一个IP是否再8个子文件中出现。那我就先试试一个进程,8个线程?
用shell?? grep?
协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。
相比多线程的优势:
极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
协程的好处:
高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
协程的缺点
最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
python对协程的支持还非常有限,用在generator中的yield可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了。
来看例子:
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
import timedef consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER] Consuming %s...' % n) time.sleep(1) r = '200 OK'def produce(c): c.next() n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) c.close()if __name__=='__main__': c = consumer() produce(c)
执行结果:
[PRODUCER] Producing 1...[CONSUMER] Consuming 1...[PRODUCER] Consumer return: 200 OK[PRODUCER] Producing 2...[CONSUMER] Consuming 2...[PRODUCER] Consumer return: 200 OK[PRODUCER] Producing 3...[CONSUMER] Consuming 3...[PRODUCER] Consumer return: 200 OK[PRODUCER] Producing 4...[CONSUMER] Consuming 4...[PRODUCER] Consumer return: 200 OK[PRODUCER] Producing 5...[CONSUMER] Consuming 5...[PRODUCER] Consumer return: 200 OK
注意到consumer函数是一个generator(生成器),把一个consumer传入produce后:
整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
最后套用Donald Knuth的一句话总结协程的特点:
“子程序就是协程的一种特例。”
并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在CPU上运行。
当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是『同时』
并发提供了一种程序组织结构方式,让问题的解决方案可以并行执行,但并行执行不是必须的
并行是为了利用多核加速多任务完成的进度并发是为了让独立的子任务都有机会被尽快执行,但不一定能加速整体进度非阻塞是为了提高程序整体执行效率异步是高效地组织非阻塞任务的方式
单核CPU也可以运行多进程,只不过不是同时的,而是极快地在进程间来回切换实现的多进程
CPU密集型代码(各种循环处理、计算等等):使用多进程。IO密集型代码(文件处理、网络爬虫等):使用多线程
阻塞是指调用线程或者进程被操作系统挂起。
非阻塞是指调用线程或者进程不会被操作系统挂起。
非阻塞的存在是因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。
同步是阻塞模式,异步是非阻塞模式。
由调用方盲目主动问询的方式是同步调用,由被调用方主动通知调用方任务已完成的方式是异步调用。
以进程、线程、协程、函数/方法作为执行任务程序的基本单位,结合回调、事件循环、信号量等机制,以提高程序整体执行效率和并发能力的编程方式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PqVen2pZ-1618403888170)(640)]
哈哈哈哈哈 浪费“CPU”的时间等于谋财害命。而凶手就是程序猿。
提高效率,I/O是最大的瓶颈。
网络I/O是最大的I/O瓶颈
以python网络爬虫为例
最容易想到的解决方案就是依次下载,从建立socket连接到发送网络请求再到读取响应数据,顺序进行。
建网络连接需要1秒钟,那么sock.connect()就得阻塞1秒钟,等待网络连接成功。这1秒钟对一颗2.6GHz的CPU来讲,仿佛过去了83年,然而它不能干任何事情。
会有提升,比如开十个进程,但是总体耗时并没有缩减到原来的十分之一,因为存在进程切换开销。
当进程数量大于CPU核心数量时,进程切换是必然需要的。
缺点:
切换开销,多进程还有另外的缺点。一般的服务器在能够稳定运行的前提下,可以同时处理的进程数在数十个到数百个规模。如果进程数量规模更大,系统运行将不稳定,而且可用内存资源往往也会不足。
由于线程的数据结构比进程更轻量级,同一个进程可以容纳多个线程,从进程到线程的优化由此展开。后来的OS也把调度单位由进程转为线程,进程只作为线程的容器,用于管理进程所需的资源。而且OS级别的线程是可以被分配到不同的CPU核心同时运行的。
在做阻塞的系统调用时,例如sock.connect()
,sock.recv()
时,当前线程会释放GIL,让别的线程有执行机会。但是单个线程内,在阻塞调用上还是阻塞的。
除了GIL之外,所有的多线程还有通病。它们是被OS调度,调度策略是抢占式的,以保证同等优先级的线程都有均等的执行机会,那带来的问题是:并不知道下一时刻是哪个线程被运行,也不知道它正要执行的代码是什么。所以就可能存在竞态条件。这是多线程的主要问题。
非阻塞就是在做一件事的时候,不阻碍调用它的程序做别的事情。
OS将I/O状态的变化都封装成了事件,如可读事件、可写事件。并且提供了专门的系统模块让应用程序可以接收事件通知。这个模块就是select
。让应用程序可以通过select
注册文件描述符和回调函数。当文件描述符的状态发生变化时,select
就调用事先注册的回调函数。select因其算法效率比较低,后来改进成了
poll,再后来又有进一步改进,BSD内核改进成了
kqueue模块,而Linux内核改进成了
epoll`模块