Python-GIL 进程池 线程池

摘要:
GIL不知道什么代码会导致数据竞争。它不知道在哪里添加6C Python解释器。多线程是鸡肋吗?此时,系统处于死锁状态或系统已生成死锁。这些总是在等待的进程称为死锁进程。以下是死锁解决方案,递归锁。在Python中,为了支持同一线程中对同一资源的多个请求,Python提供了可重入锁RLock。手动限制进程的数量太麻烦了。此时,流程池可以发挥其作用。




5、GIL vs 互斥锁(*****)
1、什么是GIL(Global Interpreter Lock)
GIL是全局解释器锁,是加到解释器身上的,保护的就是解释器级别的数据 (比如垃圾回收的数据)
同一个进程内的所有线程都需要先抢到GIL锁,才能执行解释器代码
2 为什么需要GIL
python 中内存管理依赖于 GC(一段用于回收内存的代码) 也需要一个线程
除了你自己开的线程 系统还有一些内置线程 就算你的代码不会去竞争解释器 内置线程也可能会竞争
所以必须加上锁
3、GIL的影响
GIl会限制同一进程的内的多个线程同一时间只能有一个运行,也就是说python一个进程内的多线线程

无法实现并行的效果,即无法利用多核优势

然后多核提供的优势是同一时刻有多个cpu参与计算,意味着计算性能地提升,也就是说我们的任务是
计算密集型的情况下才需要考虑利用多核优势,此时应该开启python的多进程

在我们的任务是IO密集型的情况下,再多的cpu对性能的提升也用处不大,也就说多核优势在IO密集型程序面前
发挥的作用微乎其微,此时用python的多线程也是可以的

GIL的优缺点:
优点:
保证Cpython解释器内存管理的线程安全
缺点:
同一进程内所有的线程同一时刻只能有一个执行,无法利用多核CPU
也就说Cpython解释器的多线程无法实现并行

(问题: 一个py程序 要想运行 必须运行解释器 解释器的工作时翻译代码 并执行
当一个py进程中 有多个线程 线程的任务就是执行代码 意味者 多个线程都要使用解释器
简单的说 多线程会争抢解释器的执行权
如果是自己开的线程 多线程要访问相同数据 加锁就能解决
但是有一写代码不是程序员写的 也确实需要共享使用 就是解释器
GC:垃圾回收器 负责清理内存中的无用数据 清理垃圾也需要执行代码 但是GC不应该卡住用户的代码执行
只能开线程
GC 看到 x = 10 x = 1 准备删除10 这时候突然CPU切到用户线程 a = 10 此此时还没有问题
紧接着 CPU 又切到GC GC上来就删除10 在切到用户线程 a 所指向的地址被清理了 产生错误
解决方案: 给解释器加上锁 保证GC执行期间 用户线程不能执行)


4、GIL vs 自定义锁
保护不同的数据就应该加不同的锁。
相同点:都是互斥锁
不同点:
GIL解释器级别锁 锁的是解释器代码
自定义锁 锁的是自己写的代码
GIL 在当一个线程调用解释器时 自动加锁 在IO阻塞时或线程代码执行完毕/执行时间过长3ms时 自动解锁

本质就是一个互斥锁,然后保护不同的数据就应该用不同的互斥锁,保护我们应用程序级别的数据必须自定义互斥锁
有了GIL 为什么还需要自定义锁?
GIL 不清楚什么代码会造成数据竞争问题 不知道什么地方该加

6 Cpython的解释器下,多线程是鸡肋?*****
多个任务是IO密集型:多线程 (IO的速度 明显要比CPU执行速度慢)
多个任务是计算密集型:多进程
7、死锁现象与递归锁(可重入锁),信号量(**)
死锁?
进程也有死锁与递归锁,在进程那里忘记说了,放到这里一切说了额
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,
若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,
这些永远在互相等待的进程称为死锁进程,如下就是死锁

解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,
从而使得资源可以被多次require。直到一个线程所有的acquire都被release,
其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,
则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止

死锁造成的问题.程序卡死
一个锁不会产生死锁
当有多个锁多个线程时会产生死锁
a b 锁
p k 线程
当p 和 k 都需要a和b锁时才可能产生死锁

递归锁(可重入锁)RLock
同一个线程可以多次执行acquire 执行一次acquire 计数加1
执行一次release 次数减一 执行acquire的次数需要与release的次数对应
在执行被锁的代码时 同一个线程 不会判断次数 其他线程需要判断 计数为0才可以执行

不是用来解决死锁的

Semaphore信号量(了解) 常用在线程中
信号量作用:限制同时执行被锁代码的线程数量
案列:
sem = semaphore(2)
acquire
code.....
release
开了十个线程 只能有两个同时执行

8、队列queue(***)
queue 这个queue和进程里的Queue不同 就是一个简单的容器
队列是一种数据的容器
特点:先进先出
queue先进先出
lifoqueue先进后出
priorityqueue 优先级队列 整型表示优先级 数字越大优先级越低

import queue
q = queue.Queue()# 普通队列 先进先出
q.put("a")
q2 = queue.LifoQueue()# 堆栈队列 先进后出 后进先出 函数调用就是进栈 函数结束就出栈 递归造成栈溢出
q3 = queue.PriorityQueue() # 优先级队列

9、Event事件(**)了解
python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法 set、wait、clear。

事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,
那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。

是什么?
线程间通讯的方式
为什么用?
简化代码
set()设置为True
wati()阻塞 直到为True
clear:将“Flag”设置为False

4、池(*****)
就是一个装进程/线程的容器
为何要用池:
操作系统无法无限开启进程或线程
池作用是将进程或线程控制操作系统可承受的范围内
什么时候用进程池: (比如 双十一)
当程序中有多个进程时 管理变得非常麻烦
进程池可以帮我们管理进程
1.进程的创建
2.进程的销毁
3.任务的分配
4.限制最大的进程数 保证系统正常运行

池内装的东西有两种:
装进程:进程池
装线程:线程池

进程池
在利用Python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,
并行操作可以节约大量的时间。多进程是实现并发的手段之一,需要注意的问题是:
1 很明显需要并发执行的任务通常要远大于核数
2 一个操作系统不可能无限开启进程,通常有几个核就开几个进程
3 进程开启过多,效率反而会下降(开启进程是需要占用系统资源的,而且开启多余核数目的进程也无法做到并行)
例如当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程,
十几个还好,但如果是上百个,上千个。。。手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。

我们就可以通过维护一个进程池来控制进程数目,比如httpd的进程模式,规定最小进程数和最大进程数...
创建进程池的类:如果指定numprocess为3,则进程池会从无到有创建三个进程,
然后自始至终使用这三个进程去执行所有任务,不会开启其他进程


使用方式?
ThreadPoolExecutor 线程池
实例化 时指定最大线程数
ProcessPoolExecutor 进程池
实例化 时指定最大进程数
执行submit来提交任务
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
p=ThreadPoolExecutor(4) # 默认开启的线程数是cpu的核数*5
p.submit(task,i)

总结一下:
进程池可以自动创建进程
进程限制最大进程数
自动选择一个空闲的进程帮你处理任务

进程什么时候算是空闲?
代码执行完算是空闲

进程池,池子内什么时候装进程:并发的任务属于计算密集型
线程池,池子内什么时候装线程:并发的任务属于IO密集型


回调函数:
需要回调函数的场景:进程池中任何一个任务一旦处理完了,就立即告知主进程:
我好了额,你可以处理我的结果了。主进程则调用一个函数去处理该结果,该函数即回调函数

我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),
这样主进程在执行回调函数时就省去了I/O的过程,直接拿到的是任务的结果。

如果在主进程中等待进程池中所有任务都执行完毕后,再统一处理结果,则无需回调函数

免责声明:文章转载自《Python-GIL 进程池 线程池》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇ABP 如何创建 BackgroundWorker (后台作业)SQL分类之DML:增删改表中的数据下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

基于无锁的C#并发队列实现

最近开始学习无锁编程,和传统的基于Lock的算法相比,无锁编程具有其独特的优点,Angel Lucifer的关于无锁编程一文对此有详细的描述。 无锁编程的目标是在不使用Lock的前提下保证并发过程中共享数据的一致性,其主要的实现基础是CAS操作,也就是compare_and_swap,通过处理器提供的指令,可以原子地更新共享数据,并同时监测其他线程的干...

JAVA GC 常见错误处理方法

java.lang.OutOfMemoryError: Java heap space 原因:Heap内存溢出,意味着Young和Old generation的内存不够。 解决:调整java启动参数-Xms -Xmx 来增加Heap内存。 java.lang.OutOfMemoryError: unable to create new native t...

C# SerialPort运行方式

SerialPort中串口数据的读取与写入有较大的不同。由于串口不知道数据何时到达,因此有两种方法可以实现串口数据的读取。一、线程实时读串口;二、事件触发方式实现。由于线程实时读串口的效率不是十分高效,因此比较好的方法是事件触发的方式。在SerialPort类中有DataReceived事件,当串口的读缓存有数据到达时则触发DataReceived事件,其...

C#基础系列——多线程 信号量 异步 编程 Task Thread async和await

 多线程: ThreadStart 是一个委托函数 static void Main(string[] args) { Thread oGetArgThread = new Thread(new ThreadStart(() => {...

jvm主内存与工作内存

一、jvm主内存与工作内存     首先,JVM将内存组织为主内存和工作内存两个部分。     主内存主要包括本地方法区和堆。每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)。   1.所有的变量都存储在主内存中(虚拟机内存的一部分),对于所有线程都是共...

Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现

     声明:本文为原创博文,转载请注明出处。      Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们。在网络编程中,一般都是基于Reactor线程模型的变种,无论其怎么演化,其核心组件都包含了Reactor实例(提供事件注册、注销、通知功能)、多路复用器(由操作系统提供,比如kque...