6.05—037—周三

今日所学内容:

    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的加锁、解锁时机

  加锁的时机:

    在调用解释器时立即加锁

  解锁时机:

    当前线程遇到了IO时释放

    当前线程执行时间超过设定值时释放

5、GIL的性能

  GIL的优点:

    保证了CPython中的内存管理是线程安全的

  GIL的缺点:

    互斥锁的特性使得多线程无法并行

  总结:

    1)单核下无论是IO密集还是计算密集GIL都不会产生任何影响

    2)多核下对于IO密集任务,GIL会有细微的影响,基本可以忽略

    3)Cpython中IO密集任务应该采用多线程,计算密集型应该采用多进程

    另外:之所以广泛采用CPython解释器,就是因为大量的应用程序都是IO密集型的,还有另一个很重要的原因是CPython可以无缝对接各种C语言实现的库,这对于一些数学计算相关的应用程序而言非常的简便,直接就能使用各种现成的算法

6、自定义的线程锁与GIL的区别

  GIL保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等,具体参考垃圾回收机制详解。

  对于程序中自己定义的数据则没有任何的保护效果,这一点在没有介绍GIL前我们就已经知道了,所以当程序中出现了共享自定义的数据时就要自己加锁

二、线程池与进程池

1、什么是线程池/进程池

  池表示一个容器,本质上就是一个存储进程或线程的列表

2、为什么需要进程/线程池  

  在很多情况下,需要控制进程或线程的数量在一个合理的范围,

  Eg:TCP程序中,一个客户端对应一个线程,虽然线程的开销小,但肯定不能无限的开,否则系统资源迟早被耗尽,解决的办法就是控制线程的数量。

  线程/进程池不仅帮我们控制线程/进程的数量,还帮我们完成了线程/进程的创建,销毁,以及任务的分配

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

  

转载于:https://www.cnblogs.com/Chinesehan/p/10978640.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值