今日所学内容:
GIL Global Interpreter Lock 全局解释器锁
线程池与进程池
同步、异步
异步回调
一、GIL Global Interpreter Lock 全局解释器锁
1、什么是GIL
定义:In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)
总结:在CPython中,GIL会把线程的并行变成串行,导致效率降低
需要知道的是,解释器并不只有CPython,还有PyPy,JPython等等。GIL也仅存在与CPython中,这并不是Python这门语言的问题,而是CPython解释器的问题
2、GIL带来的问题
首先必须明确执行一个py文件,分为三个步骤:
1)从硬盘加载Python解释器到内存
2)从硬盘加载py文件到内存
3)解释器解析py文件内容,交给CPU执行
其次需要明确的是每当执行一个py文件,就会立即启动一个python解释器
GIL加到了解释器上,并且是一把互斥锁,那么这把锁对应用程序到底有什么影响?这就需要知道解释器的作用,以及解释器与应用程序代码之间的关系
py文件中的内容本质都是字符串,只有在被解释器解释时,才具备语法意义,解释器会将py代码翻译为当前系统支持的指令交给系统执行。当进程中仅存在一条线程时,GIL锁的存在没有不会有任何影响,但是如果进程中有多个线程时,GIL锁就开始发挥作用了
开启子线程时,给子线程指定了一个target表示该子线程要处理的任务即要执行的代码。代码要执行则必须交由解释器,即多个线程之间就需要共享解释器,为了避免共享带来的数据竞争问题,于是就给解释器加上了互斥锁!由于互斥锁的特性,程序串行,保证了数据安全,但是却降低执行效率,GIL将使得程序整体效率降低!
3、为什么需要GIL
Eg:线程1正在定义变量a=10,而定义变量第一步会先到到内存中申请空间把10存进去,第二步将10的内存地址与变量名a进行绑定,如果在执行完第一步后,CPU切换到了GC线程,GC线程发现10的地址引用计数为0则将其当成垃圾进行了清理,等CPU再次切换到线程1时,刚刚保存的数据10已经被清理掉了,导致无法正常定义变量,有了GIL后,多个线程将不可能在同一时间使用解释器,从而保证了解释器的数据安全
4、DIL的加锁、解锁时机
加锁的时机:
在调用解释器时立即加锁
解锁时机:
当前线程执行时间超过设定值时释放
5、GIL的性能
GIL的优点:
保证了CPython中的内存管理是线程安全的
GIL的缺点:
互斥锁的特性使得多线程无法并行
总结:
1)单核下无论是IO密集还是计算密集GIL都不会产生任何影响
2)多核下对于IO密集任务,GIL会有细微的影响,基本可以忽略
3)Cpython中IO密集任务应该采用多线程,计算密集型应该采用多进程
另外:之所以广泛采用CPython解释器,就是因为大量的应用程序都是IO密集型的,还有另一个很重要的原因是CPython可以无缝对接各种C语言实现的库,这对于一些数学计算相关的应用程序而言非常的简便,直接就能使用各种现成的算法
6、自定义的线程锁与GIL的区别
GIL保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等,具体参考垃圾回收机制详解。
对于程序中自己定义的数据则没有任何的保护效果,这一点在没有介绍GIL前我们就已经知道了,所以当程序中出现了共享自定义的数据时就要自己加锁
二、线程池与进程池
1、什么是线程池/进程池
池表示一个容器,本质上就是一个存储进程或线程的列表
2、为什么需要进程/线程池
线程/进程池不仅帮我们控制线程/进程的数量,还帮我们完成了线程/进程的创建,销毁,以及任务的分配
3、如何判断池子中是存线程还是进程
IO密集型任务使用线程池
计算密集任务则使用进程池
4、如何使用:
创建线程池
1)创建线程池,指定最大线程数,此时不会创建线程,不指定数量时,默认为CPU和核数×5
pool = ThreadPoolExecutor(3) 指定最大线程数为 3
2)第一次提交任务时立即创建线程
pool.submit(task)
再有新任务时 直接使用之前已经创建好的线程来执行
pool.submit(task)
3)pool.shutdown() 等待所有任务全部完毕,销毁所有线程后关闭线程池,关闭后就不能提交新任务了
创建进程池
方法和创建线程池类似
三、同步、异步 ——任务提交(执行)方式
之前学过
阻塞、非阻塞 ——程序的运行状态
并行、并发 ——多任务的处理方式
同步:
指的是发起任务后,必须在原地等候,直到任务完成拿到结果,称之为同步
默认情况下,程序是同步的
异步:
发起任务,不需要等待任务结果,可以立即开启执行其他操作,一般在任务执行结束后会返回一个结果
异步的效率高于同步
四、异步回调
1、什么是异步回调:
异步回调指的是在发起一个异步任务的同时指定一个函数,在异步任务完成时会自动的调用这个函数,并且会传入Future对象
2、为什么需要异步回调:
之前在使用线程池或进程池提交任务时,如果想要处理任务的执行结果则必须调用result函数或是shutdown函数,而它们都是是阻塞的,会等到任务执行完毕后才能继续执行,这样一来在这个等待过程中就无法执行其他任务,降低了效率,所以需要一种方案,即保证解析结果的线程不用等待,又能保证数据能够及时被解析,返回,该方案就是异步回调
总结:异步回调使用方法就是在提交任务后,得到一个Futures对象,调用对象的add_done_callback(func)来指定一个回调函数
注意:
使用进程池时,回调函数都是父进程中执行执行,原因是任务是由父进程发起的,所以结果也应该交给父进程
使用线程池时,回调函数的执行线程是不确定的,哪个线程空闲就交给哪个线程,线程之间的数据是共享的,在哪里执行都可以
回调函数默认接收一个参数就是这个任务对象自己,再通过对象的result函数来获取任务的处理结果
如果你的任务结果需要交给父进程来处理,那建议回调函数,回调函数会自动将数据返回给父进程,不需要自己处理IPC