1、GIL
定义:
GIL:全局解释器锁(Global Interpreter Lock)
全局解释器锁是一种互斥锁,其锁住的代码是全局解释器中的代码
为什么需要全局解释器锁
在我们进行代码编写时,实际上我们只是编写了符合python语法的文本文件,如果我们的代码不交给解释器进行解释,那么我们的代码就是一堆字符串,只有在我们将代码交给解释器进行解释时,解释器把我们的代码进行一行一行的解释,解释成一堆二进制,此时再交给cpu进行执行,执行后电脑就会按照我们的代码执行相应的操作。
在python中,我们从来不用关心内存的创建与回收,这个与其他语言不同,其他的一些语言,像C语言,在创建变量时,必须手动的创建内存空间进行数据的存储,在这个数据使用完毕后,再手动的将内存进行回收,为什么我们不需要关心内存的创建与回收呢?
这是因为,我们现在使用的解释器时Cpython解释器,Cpython是由C语言编写的,在C语言中,有很多已经封装好的关于进程线程以及对于内存管理的模块,在解释器解释执行我们的代码时,已经默默的将开辟内存的操作执行了,所以我们不需要自己进行操作
在python中,还有一套垃圾回收机制,垃圾回收机制的工作原理是,在每个开辟的内存空间上添加引用计数,如果引用计数不为0,那么这块空间就会被编辑为不可用状态,当一个内存空间如果被引用多次时,其内存空间上的引用计数就会相应的增加,当所有的引用全部断开后,其引用计数就会就会变为0,同时垃圾回收机制的原理就是隔一段时间扫描一次内存空间,将内存空间中引用计数为0的内存区域重新标记为可用状态,那么下次再有数据要创建时就有可能使用这片空间
垃圾回收机制不是凭空产生的,他也是一段代码,也会产生一个线程,隔一段时间就会被执行一次,此时也需要被解释器进行解释,问题就出现在了这里,由于Cpython是线程不安全的,当我们创建内存空间时,如果内存空间创建到了一半,此时我们的代码执行时间到了,或者使用解释器时间过长就会被解释器挂起,开始执行另一端代码,此时如果恰好是垃圾回收机制进行解释,扫描内存空间,由于我们的内存空间还没有创建好,引用计数还没有进行加一,那么此块区域就有可能被回收,此时问题就产生了,当下次解释器再执行到我们的代码时开始执行引用计数加一的操作就会出错,因为此块区域已经被当做是垃圾回收了,那么我们应该怎样处理呢?
全局解释器锁应运而生,我们将解释器进行加锁,当年我们的代码拿到了解释器的使用权后就对解释器加锁,此时在同一个进程中,同一时间就只有一个线程在使用解释器执行,此时我们手持着这把锁,垃圾回收机制或者是其它的进程就不能使用解释器,从而达到线程安全的目的
全局解释器锁产生的问题
在添加了全局解释器锁以后,实际上多线程已经不能够实现并行的效果了,因为同一时间只有一个线程在执行,其它线程只能在锁释放后在使用锁,这个问题在单核处理器时代什么问题都没有,但是到了多核处理器时代,问题就出现了,由于多核时代可以真正的实现并行,那么多个线程就有可能同时执行,此时由于全局解释器锁的问题,python就不能实现真正的并行,只能实现并发
2、多进程以及多线程的选择
又有多进程又有多线程那么应该怎么选择呢?
当我们执行IO密集型的操作时,此时应该使用多线程
因为在进行IO密集型的操作时,由于IO操作相对于CPU的执行速度很慢很慢,此时如果使用多线程进行操作,也就是并发,是不会影响CPU的执行的,此时的短板在IO操作,如果换成多进程,也不会影响CPU的执行,在cpu切换过程中就能将这些数据处理完成,且不会等待,但是由于开启多进程所消耗的资源比较大,所以开启多线程会比较合适
当我们执行计算密集型的操作时,此时应该使用多进程
由于在执行数据密集型操作时,cpu一直在执行,此时只有时间片用完之后才会切换,此时数据的计算就需要等待CPU再次切换过来,此时如果开启多进程,由于进程之间使用不同的解释器,所以此时是在进行并行,执行的效率就会提高,所以在进行数据密集型作业时,推荐使用多进程
3、同步与异步
同步:同步的意思是在执行程序时,下一步的操作需要等待上一步执行完毕后才会执行
异步:程序在开启子进程或者子线程后不用等待程序的运行,可以直接执行后续的代码,不需要等待当子进程或字线程执行完毕
4、进程池、线程池
池:就是一个大池子,用术语来描述就是一个容器
进程池就是储存进程的容器
进程池与线程池基本相同
其使用为:
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor import time # 开启多进程 def test(): print("线程开启") time.sleep(5) print("线程结束") # 开启一个线程池,其中线程池种可以容纳最大线程数默认为 cpu数量 * 5 t_pool = ThreadPoolExecutor() # 将想要线程执行的代码交给线程池进行执行 t_pool.submit(test) print("over") # 开启多进程 def test(): print("线程开启") time.sleep(5) print("线程结束") # 开启一个线程池,其中线程池种可以容纳最大线程数默认为 cpu数量 * 5 if __name__ == '__main__': t_pool = ProcessPoolExecutor() # 将想要线程执行的代码交给线程池进行执行 t_pool.submit(test) # shutdown方法 pool.shutdown() # 在等到所有的线程都结束时,将线程池回收,如果还有线程在执行,则会进入等待状态 print("over")
在创建进程池以及创建线程池时不能直接将进程或者线程提前创建出来,只是为创建的进程以及线程增加一个最大值
使用进程线程池与自己开启进程线程的区别:
-
进程池封装了开启以销毁,我们不需要在开启线程
-
线程池中的线程不会被杀死,只有在系统重启时才会消失
-
控制线程开启的数量,保证系统的稳定性
4、回调函数
我们可以使用同步或者异步执行我们的程序,那么如果我们的父进程或者主线程需要子进程或者子线程的执行结果又该怎么办呢?
有两种方法:
一种是使用同步的方式,在子线程执行完毕后再执行后续的代码,但是此时失去了开启子进程或者子线程的意义了
另一种方法是使用异步的方式,开启子进程或者子线程后不再管子线程,当其执行结束后直接使用其执行结果,但是我们有应该怎么知道自己成什么时间结束呢?子线程什么时间结束只有子进程自己知道,那么有一种方法可以让子进程执行结束后将结果返回给父进程呢?
还真的有这种方法,这就是回调函数
回调函数在进程池以及线程池中可以使用,使用方法为:
import time from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor,_base from concurrent.futures._base import Future def task(): time.sleep(5) return "子线程结束" def future(args:Future): print(args.result()) pool = ThreadPoolExecutor() res = pool.submit(task) res.add_done_callback(future) print("over")
线程池中的线程使用回调函数,回调函数的执行还是在本线程执行,原因是 任务是由父进程发起的,所以结果也应该交给父进程
进程池中的回调函数,回调函数在执行时会将函数放到父进程中执行(会自动解决进程中通讯问题),原因是 线程之间数据本来是共享的