热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【python】GIL锁、线程锁(互斥锁)、递归锁(RLock)

GIL锁计算机有4核,代表着同一时间,可以干4个任务。如果单核cpu的话,我启动10个线程,我看上去也是并发的,因为是执行了上下文的切换,让看上去是并发的。但是单核永远肯定时串行的


GIL锁

计算机有4核,代表着同一时间,可以干4个任务。如果单核cpu的话,我启动10个线程,我看上去也是并发的,因为是执行了上下文的切换,让看上去是并发的。但是单核永远肯定时串行的,它肯定是串行的,cpu真正执行的时候,因为一会执行1,一会执行2.。。。。正常的线程就是这个样子的。但是,在python中,无论有多少核,永远都是假象。无论是4核,8核,还是16核.......不好意思,同一时间执行的线程只有一个(线程),它就是这个样子的。这个是python的一个开发时候,设计的一个缺陷,所以说python中的线程是假线程。

1、全局解释器锁(GIL)

 无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行

2、GIL存在的意义?

 因为python的线程是调用操作系统的原生线程,这个原生线程就是C语言写的原生线程。因为python是用C写的,启动的时候就是调用的C语言的接口。因为启动的C语言的远程线程,那它要调这个线程去执行任务就必须知道上下文,所以python要去调C语言的接口的线程,必须要把这个上限问关系传给python,那就变成了一个我在加减的时候要让程序串行才能一次计算。就是先让线程1,再让线程2.......

 每个线程在执行的过程中,python解释器是控制不了的,因为是调的C语言的接口,超出了python的控制范围,python的控制范围是只在python解释器这一层,所以python控制不了C接口,它只能等结果。所以它不能控制让哪个线程先执行,因为是一块调用的,只要一执行,就是等结果,这个时候4个线程独自执行,所以结果就不一定正确了。有了GIL,就可以在同一时间只有一个线程能够工作。虽然这4个线程都启动了,但是同一时间我只能让一个线程拿到这个数据。其他的几个都干等。python启动的4个线程确确实实落到了这4个cpu上,但是为了避免出错。这也是Cpython的一个缺陷,其他语言没有,仅仅只是Cpython有。

3、GIL锁关系图

GIL(全局解释器锁)是加在python解释器里面的,效果如图:

如上图,为什么GIL锁要加在python解释器这一层,而却不加在其他地方?

因为你python调用的所有线程都是原生线程。原生线程是通过C语言提供原生接口,相当于C语言的一个函数。你一调它,你就控制不了了它了,就必须等它给你返回结果。只要已通过python虚拟机,再往下就不受python控制了,就是C语言自己控制了。你加在python虚拟机以下,你是加不上去的。同一时间,只有一个线程穿过这个锁去真正执行。其他的线程,只能在python虚拟机这边等待。

 总结:

需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。

 

 


 
线程锁(互斥锁)

 线程需要沟通,需要共享数据,但是我们之前并没有涉及到多线程情况共享数据的例子。下面就来看看,多线程共享数据会出现什么情况

 1、多进程共享数据(python2.7中实验)


?








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

import threading
import time
 
 
def run(n):
    global num   # 把num变成全局变量
    time.sleep(1# 注意了sleep的时候是不占有cpu的,这个时候cpu直接把这个线程挂起了,此时cpu去干别的事情去了
    num += 1   # 所有的线程都做+1操作
 
num = 0   # 初始化num为0
t_obj = list()
for i in range(100):
    t = threading.Thread(target=run, args=("t-{0}".format(i),))
    t.start()
    t_obj.append(t)
 
for t in t_obj:
    t.join()
 
 
print("--------all thread has finished")
print("num:", num)   # 输出最后的num值
 
#执行结果
--------all thead has finished
('num:', 97#输出的结果

最后输出的结果怎么会是 97 呢?应该是100才对啊,不是有GIL(全局解释器锁)已经控制了,为什么最后的输出结果还是错误?

其实这种情况只能在python2.x 中才会出现的,python3.x里面没有这种现象,下面我们就用一张图来解释一下这个原因。如图:

解释:



  1. 到第5步的时候,可能这个时候python正好切换了一次GIL(据说python2.7中,每100条指令会切换一次GIL),执行的时间到了,被要求释放GIL,这个时候thead 1的count=0并没有得到执行,而是挂起状态,count=0这个上下文关系被存到寄存器中.

  2. 然后到第6步,这个时候thead 2开始执行,然后就变成了count = 1,返回给count,这个时候count=1.

  3. 然后再回到thead 1,这个时候由于上下文关系,thead 1拿到的寄存器中的count = 0,经过计算,得到count = 1,经过第13步的操作就覆盖了原来的count = 1的值,所以这个时候count依然是count = 1,所以这个数据并没有保护起来。

 

2、添加线程锁

 通过上面的图我们知道,结果依然是不准确的。所以我还要加一把锁,这个是用户级别的锁。

 


?








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

import threading
import time
 
 
def run(n):
    lock.acquire()  # 添加线程锁
    global num   # 把num变成全局变量
    time.sleep(0.1# 注意了sleep的时候是不占有cpu的,这个时候cpu直接把这个线程挂起了,此时cpu去干别的事情去了
    num += 1   # 所有的线程都做+1操作
    lock.release()  # 释放线程锁
 
 
num = 0   # 初始化num为0
lock = threading.Lock()  # 生成线程锁实例
t_obj = list()
for i in range(10):
    t = threading.Thread(target=run, args=("t-{0}".format(i),))
    t.start()
    t_obj.append(t)
 
for t in t_obj:
    t.join()   # 为join是等子线程执行的结果,如果不加,主线程执行完,下面就获取不到子线程num的值了,共享数据num值就错误了
 
 
print("--------all thread has finished")
print("num:", num)   # 输出最后的num值

 小结:



  1. 用theading.Lock()创建一个lock的实例。

  2. 在线程启动之前通过lock.acquire()加加锁,在线程结束之后通过lock.release()释放锁。

  3. 这层锁是用户开的锁,就是我们用户程序的锁。跟我们这个GIL没有关系,但是它把这个数据相当于copy了两份,所以在这里加锁,以确保同一时间只有一个线程,真真正正的修改这个数据,所以这里的锁跟GIL没有关系,你理解就是自己的锁。

  4. 加锁,说明此时我来去修改这个数据,其他人都不能动。然后修改完了,要把这把锁释放。这样的话就把程序编程串行了。

 

3、使用场景

 在用户层面加锁,使程序变成串行了,那我们在什么情况下用呢?

  1、我们在程序中间不能有sleep,因为程序变成串行,这样你再sleep,程序执行的时间就会变长。

  2、我们使用的时候确保数据量不是特别大,如果数据量大,也会影响我们的执行效率。

  3、如果你程序结束时,不释放锁的话,而且程序又是串行的,则就是占着坑,那永远在那边等着,所以最后需要释放锁。

 

 


 
递归锁(RLock)

1、线程死锁

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都

正在使用,所有这两个线程在无外力作用下将一直等待下去


?








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

import threading
 
 
def run1():
    print("grab the first part data")
    lock.acquire()  # 修改num前加锁
    global num
    num += 1
    lock.release()   # 释放锁
    return num
 
 
def run2():
    print("grab the second part data")
    lock.acquire()   # 修改num2前加锁
    global num2
    num2 += 1
    lock.release()   # 释放锁
    return num2
 
 
def run3():
    lock.acquire()  # 加锁
    res = run1()   # 执行run1函数
    print('--------between run1 and run2-----')
    res2 = run2()  # 执行run2函数
    lock.release()  # 释放锁
    print(res, res2)
 
 
if __name__ == '__main__':
    num, num2 = 0, 0
    lock = threading.Lock()  # 设置锁的全局变量
    for i in range(10):
        t = threading.Thread(target=run3)
        t.start()
 
 
while threading.active_count() != 1# 判断是否只剩主线程了
    print(threading.active_count())
else:
    print('----all threads done---')
    print(num, num2)

上面的执行结果,是无限的进入死循环,所以不能这么加,这个时候就需要用到递归锁。

 

2、递归锁(RLock)


?








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

import threading
 
 
def run1():
    print("grab the first part data")
    lock.acquire()  # 修改num前加锁
    global num
    num += 1
    lock.release()   # 释放锁
    return num
 
 
def run2():
    print("grab the second part data")
    lock.acquire()   # 修改num2前加锁
    global num2
    num2 += 1
    lock.release()   # 释放锁
    return num2
 
 
def run3():
    lock.acquire()  # 加锁
    res = run1()   # 执行run1函数
    print('--------between run1 and run2-----')
    res2 = run2()  # 执行run2函数
    lock.release()  # 释放锁
    print(res, res2)
 
 
if __name__ == '__main__':
    num, num2 = 0, 0
    lock = threading.RLock()  # 只用修改这里,把线程锁lock()更改成递归锁RLock()的全局变量
    for i in range(10):
        t = threading.Thread(target=run3)
        t.start()
 
 
while threading.active_count() != 1# 判断是否只剩主线程了
    print(threading.active_count())
else:
    print('----all threads done---')
    print(num, num2)

递归锁原理其实很简单:就是每开一把门,在字典里面存一份数据,退出的时候去到door1或者door2里面找到这个钥匙退出,如图:










1
2
3
4

lock = {
door1:key1,
door2:key2
}

 注:递归锁用于多重锁的情况,如果只是一层锁,就用不上递归锁,在实际情况下,递归锁场景用的不是特别多。

转载:https://www.cnblogs.com/Keep-Ambition/p/7596098.html



推荐阅读
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • EPPlus绘制刻度线的方法及示例代码
    本文介绍了使用EPPlus绘制刻度线的方法,并提供了示例代码。通过ExcelPackage类和List对象,可以实现在Excel中绘制刻度线的功能。具体的方法和示例代码在文章中进行了详细的介绍和演示。 ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • Python教学练习二Python1-12练习二一、判断季节用户输入月份,判断这个月是哪个季节?3,4,5月----春 ... [详细]
  • 从批量eml文件中提取附件的Python代码实现方法
    本文介绍了使用Python代码从批量eml文件中提取附件的实现方法,包括获取eml附件信息、递归文件夹下所有文件、创建目的文件夹等步骤。通过该方法可以方便地提取eml文件中的附件,并保存到指定的文件夹中。 ... [详细]
  • 第七课主要内容:多进程多线程FIFO,LIFO,优先队列线程局部变量进程与线程的选择线程池异步IO概念及twisted案例股票数据抓取 ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • Python瓦片图下载、合并、绘图、标记的代码示例
    本文提供了Python瓦片图下载、合并、绘图、标记的代码示例,包括下载代码、多线程下载、图像处理等功能。通过参考geoserver,使用PIL、cv2、numpy、gdal、osr等库实现了瓦片图的下载、合并、绘图和标记功能。代码示例详细介绍了各个功能的实现方法,供读者参考使用。 ... [详细]
  • 006_Redis的List数据类型
    1.List类型是一个链表结构的集合,主要功能有push,pop,获取元素等。List类型是一个双端链表的结构,我们可以通过相关操作进行集合的头部或者尾部添加删除元素,List的设 ... [详细]
  • 十大经典排序算法动图演示+Python实现
    本文介绍了十大经典排序算法的原理、演示和Python实现。排序算法分为内部排序和外部排序,常见的内部排序算法有插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。文章还解释了时间复杂度和稳定性的概念,并提供了相关的名词解释。 ... [详细]
  • Visual C# TabControl中TabPage分离成若干个Form的小办法
    写Visual的同学们都会用到这个TabControl的控件,然后会分好几页的TabPage,每页都有很多控件和业务逻辑,但是每页的关系也 ... [详细]
  • 线程漫谈——线程基础
    本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。进程与线程理解线程是至关重要的,每个进程至少有一个线程,进程是线程的容器,线程才是真正的执行体,线程必 ... [详细]
author-avatar
shadow
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有