Python线程——GIL锁、线程锁(互斥锁)、递归锁(RLock)

GIL锁

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

全局解释器锁(GIL)

​ Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
  对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

  在多线程环境中,Python 虚拟机按以下方式执行:

  1、设置 GIL;

  2、切换到一个线程去运行;

  3、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));

  4、把线程设置为睡眠状态;

  5、解锁 GIL;

  6、再次重复以上所有步骤。
  在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。

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,JPython,PyPy,Psyco等不同的Python执行环境来执行。而JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

线程锁(互斥锁)

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
from threading import Thread
import time


def work():
# 在这里模拟一个底层做运算的过程,具体为什么这个我不知道,看某资料写的,
# 如果这里直接对num进行运算很难出现数据不安全的结果
global num # 把num变成全局变量
temp = num
time.sleep(1) # 注意了sleep的时候是不占有cpu的,这个时候cpu直接把这个线程挂起了,此时cpu去干别的事情去了
num = temp + 1 # 所有的线程都做+1操作


if __name__ == '__main__':

num = 0 # 初始化num为0
t_obj = list()
for i in range(100):
t = Thread(target=work)
t.start()
t_obj.append(t)

for t in t_obj:
t.join()
print("num:", num) # 输出最后的num值,可能是1

# 执行结果
num: 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
from threading import Lock,Thread
import time


def work(lock):
# 在这里模拟一个底层做运算的过程,具体为什么这个我不知道,看某资料写的,
# 如果这里直接对num进行运算很难出现数据不安全的结果
global num # 把num变成全局变量
lock.acquire()
temp = num
time.sleep(0.1) # 注意了sleep的时候是不占有cpu的,这个时候cpu直接把这个线程挂起了,此时cpu去干别的事情去了
num = temp + 1 # 所有的线程都做+1操作
lock.release()


if __name__ == '__main__':

num = 0 # 初始化num为0
t_obj = list()
lock = Lock()
for i in range(100):
t = Thread(target=work, args=(lock,))
t.start()
t_obj.append(t)

for t in t_obj:
t.join()
print("num:", num) # 输出最后的num值

# 执行结果
num: 100

注意:这里的Lock创建的锁和GIL没有关系 ,

递归锁(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
from threading import Lock, Thread
import time

noodle_lock = Lock()
fork_lock = Lock()


def eat1(name):
noodle_lock.acquire()
print('%s拿到面条啦' % name)
fork_lock.acquire()
print('%s拿到叉子了' % name)
print('%s吃面' % name)
fork_lock.release()
noodle_lock.release()


def eat2(name):
fork_lock.acquire()
print('%s拿到叉子了' % name)
time.sleep(1)
noodle_lock.acquire()
print('%s拿到面条啦' % name)
print('%s吃面' % name)
noodle_lock.release()
fork_lock.release()


if __name__ == '__main__':
Thread(target=eat1, args=('张三',)).start()
Thread(target=eat2, args=('李四',)).start()
Thread(target=eat1, args=('王五',)).start()
Thread(target=eat2, args=('赵六',)).start()

# 执行结果
张三拿到面条啦
张三拿到叉子了
张三吃面
李四拿到叉子了
王五拿到面条啦

上面例子中情况是在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。执行结果,是无限的进入死循环,所以不能这么加,这个时候就需要用到递归锁。

递归锁(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
from threading import Thread, RLock  # 递归锁
import time

fork_lock = noodle_lock = RLock() # 一个钥匙串上的两把钥匙


def eat1(name):
noodle_lock.acquire() # 一把钥匙
print('%s拿到面条啦' % name)
fork_lock.acquire()
print('%s拿到叉子了' % name)
print('%s吃面' % name)
fork_lock.release()
noodle_lock.release()


def eat2(name):
fork_lock.acquire()
print('%s拿到叉子了' % name)
time.sleep(1)
noodle_lock.acquire()
print('%s拿到面条啦' % name)
print('%s吃面' % name)
noodle_lock.release()
fork_lock.release()


if __name__ == '__main__':
Thread(target=eat1, args=('张三',)).start()
Thread(target=eat2, args=('李四',)).start()
Thread(target=eat1, args=('王五',)).start()
Thread(target=eat2, args=('赵六',)).start()

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

注意:递归锁用于多重锁的情况,如果只是一层锁,就用不上递归锁

-------------本文结束感谢您的阅读-------------

本文标题:Python线程——GIL锁、线程锁(互斥锁)、递归锁(RLock)

文章作者:GavinLiu

发布时间:2018年03月06日 - 23:03

最后更新:2018年03月06日 - 23:03

原始链接:http://gavinliu4011.github.io/post/5b18ac8.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

请博主吃个鸡腿吧
0%