Python中的GIL和深浅拷贝

文章目录

  • Python中的GIL和深浅拷贝
    • 一、GIL全局解释器锁
      • 1.引入
      • 2、GIL
      • 3、Python GIL底层实现原理
      • 4、计算密集型和IO密集型
      • 5、解决GIL问题的方案:
      • 6、线程释放GIL锁的情况:
      • 7、GIL对python多线程的影响?阐明多线程程序是否可比单线程性能有提升,并解释原因。
    • 二、深浅拷贝
      • 1、 浅拷贝
      • 2. 深拷贝
      • 3、拷贝的其他方式
      • 4. 注意点
      • 5、copy.copy和copy.deepcopy的区别
      • 6、总结

一、GIL全局解释器锁

1.引入

GIL,中文译为全局解释器锁。在讲解 GIL 之前,首先通过一个例子来直观感受一下 GIL 在 Python 多线程程序运行的影响。

首先运行如下程序:

import time
start = time.clock()
def CountDown(n):while n > 0:n -= 1
CountDown(100000)
print("Time used:",(time.clock() - start))
运行结果为:
Time used: 0.0039529000000000005

在我们的印象中,使用多个(适量)线程是可以加快程序运行效率的,因此可以尝试将上面程序改成如下方式:

import time
from threading import Thread
start = time.clock()
def CountDown(n):while n > 0:n -= 1
t1 = Thread(target=CountDown, args=[100000 // 2])
t2 = Thread(target=CountDown, args=[100000 // 2])
t1.start()
t2.start()
t1.join()
t2.join()
print("Time used:",(time.clock() - start))运行结果为:
Time used: 0.006673
  • 可以看到,此程序中使用了 2 个线程来执行和上面代码相同的工作,但从输出结果中可以看到,运行效率非但没有提高,反而降低了
  • 如果使用更多线程进行尝试,会发现其运行效率和 2 个线程效率几乎一样(本机器测试使用 4 个线程,其执行效率约为 0.005)
  • 是不是和你猜想的结果不一样?事实上,得到这样的结果是肯定的,因为 GIL 限制了 Python 多线程的性能不会像我们预期的那样。

2、GIL

GIL 是最流程的 CPython 解释器(平常称为 Python)中的一个技术术语,中文译为全局解释器锁,其本质上类似操作系统的 Mutex。GIL 的功能是:在 CPython 解释器中执行的每一个 Python 线程,都会先锁住自己,以阻止别的线程执行。

当然,CPython 不可能容忍一个线程一直独占解释器,它会轮流执行 Python 线程。这样一来,用户看到的就是“伪”并行,即 Python 线程在交替执行,来模拟真正并行的线程

有读者可能会问,既然 CPython 能控制线程伪并行,为什么还需要 GIL 呢?其实,这和 CPython 的底层内存管理有关。

CPython 使用引用计数来管理内容,所有 Python 脚本中创建的实例,都会配备一个引用计数,来记录有多少个指针来指向它当实例的引用计数的值为 0 时,会自动释放其所占的内存。

举个例子,看如下代码:

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3
  • 可以看到,a 的引用计数值为 3,因为有 a、b 和作为参数传递的 getrefcount 都引用了一个空列表。
  • 假设有两个 Python 线程同时引用 a,那么双方就都会尝试操作该数据,很有可能造成引用计数的条件竞争,导致引用计数只增加1(实际应增加 2)
  • 这造成的后果是,当第一个线程结束时,会把引用计数减少 1,此时可能已经达到释放内存的条件(引用计数为 0),当第 2 个线程再次视图访问 a 时,就无法找到有效的内存了。
  • 所以,CPython 引进 GIL,可以最大程度上规避类似内存管理这样复杂的竞争风险问题。

3、Python GIL底层实现原理

  • 上面这张图,就是 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3
    轮流执行
  • 每一个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;
  • 同样的,每一个线程执行完一段后,会释放GIL,以允许别的线程开始利用资源。
  • 读者可能会问,为什么 Python 线程会去主动释放 GIL 呢?毕竟,如果仅仅要求 Python 线程在开始执行时锁住 GIL,且永远不去释放 GIL,那别的线程就都没有运行的机会。
  • 其实,CPython 中还有另一个机制,叫做间隔式检查(check_interval),意思是 CPython 解释器会去轮询检查线程 GIL 的锁住情况,每隔一段时间,Python 解释器就会强制当前线程去释放 GIL,这样别的线程才能有执行的机会
  • 注意,不同版本的 Python,其间隔式检查的实现方式并不一样。早期的 Python 是 100 个刻度(大致对应了 1000 个字节码);
  • 而 Python 3 以后,间隔时间大致为 15 毫秒。当然,我们不必细究具体多久会强制释放 GIL,读者只需要明白,CPython 解释器会在一个“合理”的时间范围内释放 GIL 就可以了。

整体来说,每一个 Python 线程都是类似这样循环的封装,来看下面这段代码:

for (;;) {if (--ticker < 0) {ticker = check_interval;   /* Give another thread a chance */PyThread_release_lock(interpreter_lock);/* Other threads may run now */   PyThread_acquire_lock(interpreter_lock, 1);}bytecode = *next_instr++;switch (bytecode) {/* execute the next instruction ... */}
}
  • 从这段代码中可以看出,每个 Python 线程都会先检查 ticker 计数。只有在 ticker 大于 0的情况下,线程才会去执行自己的代码。
  • Python GIL不能绝对保证线程安全
  • 注意,有了 GIL,并不意味着 Python 程序员就不用去考虑线程安全了,因为即便 GIL 仅允许一个 Python 线程执行,但别忘了 Python 还有 check interval 这样的抢占机制。

比如,运行如下代码:

import threading
n = 0
def foo():global nn += 1
threads = []
for i in range(100):t = threading.Thread(target=foo)threads.append(t)
for t in threads:t.start()
for t in threads:t.join()
print(n)

执行此代码会发现,其大部分时候会打印 100,但有时也会打印 99 或者 98,原因在于 n+=1 这一句代码让线程并不安全。如果去翻译 foo 这个函数的字节码就会发现,它实际上是由下面四行字节码组成:

>>> import dis
>>> dis.dis(foo)
LOAD_GLOBAL              0 (n)
LOAD_CONST               1 (1)
INPLACE_ADD
STORE_GLOBAL             0 (n)

而这四行字节码中间都是有可能被打断的!所以,千万别以为有了 GIL 程序就不会产生线程问题,我们仍然需要注意线程安全。

4、计算密集型和IO密集型

遇到IO阻塞时,正在执行的线程会暂时释放GIL锁,这时其它线程会利用这个空隙时间,执行自己的代码,因此多线程抓取比单线程抓取性能要好。

  • 计算密集型:要进行大量的数值计算,例如进行上亿的数字计算、计算圆周率、对视频进行高清解码等等。这种计算密集型任务虽然也可以用多任务完成,但是花费的主要时间在任务切换的时间,此时CPU执行任务的效率比较低。
  • IO密集型:涉及到网络请求(time.sleep())、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。

5、解决GIL问题的方案:

1.使用其它语言,例如C,Java

2.使用其它解释器,如java的解释器jython

3.使用多进程

6、线程释放GIL锁的情况:

1.在IO操作等可能会引起阻塞的system call之前,可以暂时释放GIL,但在执行完毕后,必须重新获取GIL。

2.Python 3.x使用计时器(执行时间达到阈值后,当前线程释放GIL)或Python 2.x,tickets计数达到100。

7、GIL对python多线程的影响?阐明多线程程序是否可比单线程性能有提升,并解释原因。

  1. Python语言和GIL没有半毛钱关系。仅仅是由于历史原因在Cpython虚拟机(解释器),难以移除GIL。
  2. GIL:全局解释器锁。每个线程在执行的过程都需要先获取GIL,保证同一时刻只有一个线程可以执行代码。
  3. 线程释放GIL锁的情况: 在IO操作等可能会引起阻塞的system call之前,可以暂时释放GIL,但在执行完毕后,必须重新获取GIL Python 3.x使用计时器(执行时间达到阈值后,当前线程释放GIL)或Python 2.x,tickets计数达到100
  4. Python使用多进程是可以利用多核的CPU资源的。
  5. 多线程爬取比单线程性能有提升,因为遇到IO阻塞会自动释放GIL锁

二、深浅拷贝

1、 浅拷贝

浅拷贝是对于一个对象的顶层拷贝
通俗的理解是:拷贝了引用,并没有拷贝内容


2. 深拷贝

深拷贝是对于一个对象所有层次的拷贝(递归)


3、拷贝的其他方式

  • 分片表达式可以赋值一个序列

  • 字典的copy方法可以拷贝一个字典

4. 注意点

浅拷贝对不可变类型和可变类型的copy不同
copy.copy对于可变类型,会进行浅拷贝
copy.copy对于不可变类型,不会拷贝,仅仅是指向

In [88]: a = [11,22,33]
In [89]: b = copy.copy(a)
In [90]: id(a)
Out[90]: 59275144
In [91]: id(b)
Out[91]: 59525600
In [92]: a.append(44)
In [93]: a
Out[93]: [11, 22, 33, 44]
In [94]: b
Out[94]: [11, 22, 33]In [95]: a = (11,22,33)
In [96]: b = copy.copy(a)
In [97]: id(a)
Out[97]: 58890680
In [98]: id(b)
Out[98]: 58890680

5、copy.copy和copy.deepcopy的区别

  • copy.copy

  • copy.deepcopy


6、总结

  • 深浅拷贝都是对源对象的复制,占用不同的内存空间
  • 如果源对象只有一级目录的话,源做任何改动,不影响深浅拷贝对象
  • 如果源对象不止一级目录的话,源做任何改动,都要影响浅拷贝,但不影响深拷贝
  • 序列对象的切片其实是浅拷贝,即只拷贝顶级的对象

Python中的GIL和深浅拷贝相关推荐

  1. python中的赋值和深浅拷贝

    赋值 在python中,赋值仅仅是复制了对象的引用,并没有开辟内存空间 a = 1 b = a 上述代码只是把a的引用复制给了b,结果是a和b同时指向1 对于可变对象 a = [1, 2, 3] b ...

  2. python中复制、浅层拷贝、深层拷贝的区别

    python中复制.浅层拷贝.深层拷贝的区别 一.学习要点: 1.python中的复制与拷贝的区别 2.python中浅层拷贝与深层拷贝的区别 二.代码: import copy a=[1,2,3,4 ...

  3. Python中的GIL锁

    Python中的GIL锁 在Python中,可以通过多进程.多线程和多协程来实现多任务. 在多线程的实现过程中,为了避免出现资源竞争问题,可以使用互斥锁来使线程同步(按顺序)执行. 但是,其实Pyth ...

  4. Python中的GIL和异步Asyncio、Futures

    一 .基本概念 以下概念都是在 Python 环境下 Sync 同步编程 Async 异步 ,是指在外观上看来程序不会等待,而是找出可执行的操作/任务/线程 继续执行 Asyncio 单个主线程 多个 ...

  5. python中对GIL的理解

    深入理解Python中的GIL(全局解释器锁) 1. GIL是什么? 首先来看看GIL究竟是什么.我们需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所 ...

  6. python gil 解除_详解Python中的GIL(全局解释器锁)详解及解决GIL的几种方案

    先看一道GIL面试题: 描述Python GIL的概念, 以及它对python多线程的影响?编写一个多线程抓取网页的程序,并阐明多线程抓取程序是否可比单线程性能有提升,并解释原因. GIL:又叫全局解 ...

  7. 深入理解Python中的GIL(全局解释器锁)

    前言:本博文主要讲解Python中的GIL(全局解释器锁),本人经过查阅多个资料,摒弃一些较官方的描述,用自己的语言整理并加以补充,如果有描述有误的地方,还望读者在评论区指出,谢谢! 文章目录 一.G ...

  8. 深入理解Python中的GIL(全局解释器锁)。

    Python是门古老的语言,要想了解这门语言的多线程和多进程以及协程,以及明白什么时候应该用多线程,什么时候应该使用多进程或协程,我们不得不谈到的一个东西是Python中的GIL(全局解释器锁).这篇 ...

  9. python中的GIL详解

    python中的GIL详解 参考Python-- GIL 锁简述 GIL是什么 首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念.就 ...

最新文章

  1. 数据库分析函数 MySQL_MySql数据库索引分析explain函数的使用
  2. Linux~Sh脚本一点自己的总结
  3. java中抽象类的匿名子类和匿名对象
  4. 纵向表格_Excel如何把横向数据变纵向?教你一键快速实现
  5. mysql视频教程siki_siki老师MySQL数据库从零到精通,资源教程下载
  6. (转)公钥,私钥和数字签名这样最好理解
  7. 课时28.假链接(掌握)
  8. IE8给你选择的理由
  9. php获取post全部数据,PHP获取POST数据的几种方法汇总_PHP教程
  10. 从任意网页上摘取酷炫Jquery效果为自己使用的方法
  11. java jml_JML 入门
  12. 用模糊查询like语句时如果要查是否包含%字符串该如何写
  13. Lake Counting(信息学奥赛一本通-T1249)
  14. 服务器存储满了进不去系统,解决PC常见问题 篇四十五:建议收藏!手贱升级进不去系统?两步简单恢复黑群晖!...
  15. 北京思科CCNP和思科 CCIE考试常见问题GRE虚拟专用网络详解
  16. UVALive 4490 Help Bubu
  17. 四子棋 java_JAVA写的四子棋
  18. notion 纪念日公式
  19. 不背公式快速计算IP地址掩码---游码法
  20. 【源码】程序员优质资源汇总

热门文章

  1. 210305设计共享内存
  2. J-LINK不能烧写(错误:JLink Warning: RESET (pin 15) high, but should be low. Please check target)
  3. OP07高级电路图-摘自:Reza Moghim
  4. Dapper使用技巧
  5. Polly 重试策略
  6. linux/unix编程手册-61_64
  7. Java Servlet(三):Servlet中ServletConfig对象和ServletContext对象
  8. js调用php和php调用js的方法举例
  9. CodeForces - 1359E Modular Stability(组合数学)
  10. java 网络驱动器_删除多余的网络驱动器