python GIL全局解释器锁

全局解释器锁简称:GIL(Global Interperter Lock)。首先需要明确一点就是,GIL并不是Python语言的特性,它是在现实Python解释器时引用的一个概念。GIL只在CPython解释器上存在。作用是保证同一时间内只有一个线程在执行。

线程互斥锁和GIL的区别

1.线程互斥锁是Python代码层面的锁,解决Python程序中多线程共享资源的问题(线程数据共共享,当各个线程访问数据资源时会出现竞争状态,造成数据混乱);

2.GIL是Python解释层面的锁,解决解释器中多个线程的竞争资源问题(多个子线程在系统资源竞争是,都在等待对象某个部分资源解除占用状态,结果谁也不愿意先解锁,然后互相等着,程序无法执行下去)。

GIL对程序有啥影响

1.Python中同一时刻有且只有一个线程会执行;

2.Python中的多个线程由于GIL锁的存在无法利用多核CPU;

3.Python中的多线程不适合计算机密集型的程序;

4.如果程序需要大量的计算,利用多核CPU资源,可以使用多进程来解决。

GIL 的存在使程序无法充分利用CPU进行运算,那么它真的一无是处么? 再进一步分析,程序分为两种,一种是上面提到的计算密集型程序,另一种叫作IO密集型程序。

大部分的程序在运行时,都需要大量IO操作,比如网络数据的收发,大文件的读写,这样的程序称为IO密集型程序。

IO密集型程序在运行时,需要大量的时间进行等待,那么这时如果IO操作不完成,程序就无法执行后面的操作,导致CPU空闲。

那么 GIL 对于这种IO密集型程序会有什么影响?

在解释器解释执行任何 Python 代码时,都需要先获得这把锁才行,在遇到 I/O 操作时会释放这把锁。如果是纯计算的程序,没有 I/O 操作,解释器会每隔 100 次操作就释放这把锁,让别的线程有机会执行,提高Python程序的执行效率。

如何改善GIL产生的问题

因为 GIL 锁是解释器层面的锁,无法去除 GIL 锁在执行程序时带来的问题。只能去改善。

1.更换更高版本的解释器,比如3.6,从3.2版本开始,据说Python对解释做了优化

2.更换解释器,比如JPython,但是由于比较小众,支持的模块较少,导致开发的效率降低

3.Python为了解决程序使用多核的问题,使用多进程代替多线程

开了多个进程,就相当于启动了多个Python解释器,而GIL是CPython解释器里面的一个东西,现在解释器都有多个了,GIL当然也有多个,每个进程一个,实际上并没解决什么问题,那个进程仍然相当于只有一个线程,并发仅仅是通过进程实现的,仍然不能突破GIL

————————————————
版权声明:本文为CSDN博主「weixin_39653764」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_39653764/article/details/114911079

 

程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种一个程序在一个数据集上的一次动态执行过程就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。进程一般由程序、数据集、进程控制块三部分组成。

有了进程为什么还要有线程呢?

因为进程还是有缺陷的:

进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了

进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行

那么什么是线程呢?

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

 

进程和线程的关系:

一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

资源分配给进程,同一进程的所有线程共享该进程的所有资源。

CPU分给线程,即真正在CPU上运行的是线程。

进程:资源管理单位 (容器),线程:最小执行单位

并行和并发
并行处理:是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。

并发处理:指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机(CPU)上运行,但任一个时刻点上只有一个程序在处理机(CPU)上运行

同步与异步
同步:指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去。

异步:指进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态,当有消息返回时系统会通知进程进行处理,这样可以提高执行效率

关于GIL(全局解释器锁)
Python中的线程是操作系统的原生线程,Python虚拟机使用一个全局解释器锁(Global Interpreter Lock)来互斥线程对Python虚拟机的使用。为了支持多线程机制,一个基本的要求就是需要实现不同线程对共享资源访问的互斥,所以引入了GIL。

GIL:在一个线程拥有了解释器的访问权之后,其他的所有线程都必须等待它释放解释器的访问权,即使这些线程的下一条指令并不会互相影响。

在调用任何Python C API之前,要先获得GIL

GIL缺点:多处理器退化为单处理器;优点:避免大量的加锁解锁操作 Python的多线程:由于GIL锁,导致同一时刻,同一进程只能有一个线程被执行

总结:

对于计算密集型任务(一直在使用CPU):python的多线程并没有用

对于IO密集型任务(存在大量IO操作):python的多线程是有意义的

要想使python使用多核:只能开进程, 弊端:开销大而且切换复杂

使用多核的着重点应该放在:协程+多进程

终极思路:换C模块实现多线程

 1 import threading
 2 import time
 3 
 4 def countNum(n): # 定义某个线程要运行的函数
 5 
 6     print("running on number:%s" %n)
 7 
 8     time.sleep(3)
 9 
10 if __name__ == '__main__':
11 
12     t1 = threading.Thread(target=countNum,args=(23,)) #生成一个线程实例
13     t2 = threading.Thread(target=countNum,args=(34,))
14 
15     t1.start() #启动线程
16     t2.start()
17 
18     print("ending!")
 1 继承Thread式创建:
 2 
 3 登录后复制 
 4 import threading
 5 import time
 6 
 7 class MyThread(threading.Thread):
 8 
 9     def __init__(self,num):
10         threading.Thread.__init__(self)
11         self.num=num
12 
13     def run(self):
14         print("running on number:%s" %self.num)
15         time.sleep(3)
16 
17 t1=MyThread(56)
18 t2=MyThread(78)
19 
20 t1.start()
21 t2.start()
22 print("ending")
 1 join()方法和setDaemon()方法
 2 
 3 登录后复制 
 4 # join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞。
 5 
 6 # setDaemon(True):
 7         '''
 8          将线程声明为守护线程,必须在start() 方法调用之前设置,如果不设置为守护线程程序会被无限挂起。
 9 
10          当我们在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成
11 
12          想退出时,会检验子线程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是只要主线程
13 
14          完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦'''
15 
16 
17 import threading
18 from time import ctime,sleep
19 import time
20 
21 def Music(name):
22 
23         print ("Begin listening to {name}. {time}".format(name=name,time=ctime()))
24         sleep(3)
25         print("end listening {time}".format(time=ctime()))
26 
27 def Blog(title):
28 
29         print ("Begin recording the {title}. {time}".format(title=title,time=ctime()))
30         sleep(5)
31         print('end recording {time}'.format(time=ctime()))
32 
33 
34 threads = []
35 
36 
37 t1 = threading.Thread(target=Music,args=('FILL ME',))
38 t2 = threading.Thread(target=Blog,args=('',))
39 
40 threads.append(t1)
41 threads.append(t2)
42 
43 if __name__ == '__main__':
44 
45     #t2.setDaemon(True)
46 
47     for t in threads:
48 
49         #t.setDaemon(True) #注意:一定在start之前设置
50         t.start()
51 
52         #t.join()
53 
54     #t1.join()
55     #t2.join()    #  考虑这三种join位置下的结果?
56 
57     print ("all over %s" %ctime())
其他方法

登录后复制 
Thread实例对象的方法
  # isAlive(): 返回线程是否活动的。
  # getName(): 返回线程名。
  # setName(): 设置线程名。

threading模块提供的一些方法:
  # threading.currentThread(): 返回当前的线程变量。
  # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

 

GIL是Global Interpreter Lock,即全局解释锁的缩写,保证了了同一时刻只有一个线程在一个CPU上执行字节码,无法将多个线程映射到多个CPU上。这是CPython解释器的缺陷,由于CPython是大部分环境下默认的Python执行环境,而很多库都是基于CPython编写的,因此很多人将GIL归结为Python的问题。

GIL被设计来保护线程安全,由于多线程共享变量,如果不能很好的进行线程同步,多线程非常容易将线程改乱。实际上即使有了GIL,这个问题也无法完全解决,因为GIL实际上也会释放,而且它并不是在某个线程执行完成后才释放,而是根据代码的字节码或者时间片进行释放,下面是一个例子:

 1 import threading
 2 
 3 total = 0
 4 def add():
 5     global total
 6     for i in range(1000000):
 7         total += 1
 8 
 9 def desc():
10     global total
11     for i in range(1000000):
12         total -= 1
13 
14 thread1 = threading.Thread(target=add)
15 thread2 = threading.Thread(target=desc)
16 thread1.start()
17 thread2.start()
18 thread1.join()
19 thread2.join()
20 
21 print(total)
这个程序直观来看,是将total加1000000减1000000,不管哪个线程先执行,最后的结果应该都是0才对,但是如果允许你该上面的代码多次,就会发现每次代码的结果都不一样,有正有负。这其中的原因就涉及到了GIL的释放。我们首先可以查看一下普通加法函数的字节码:

1 import dis
2 def add1(a):
3     a += 1
4     return a
5 print(dis.dis(add1))

结果如下:

1  2           0 LOAD_FAST                0 (a)
2               2 LOAD_CONST               1 (1)
3               4 INPLACE_ADD
4               6 STORE_FAST               0 (a)
5 
6   3           8 LOAD_FAST                0 (a)
7              10 RETURN_VALUE
8 None

可以看到a += 1的执行过程是先将变量a装载进CPU,再将常量1装载进CPU,然后执行相加操作,最后再将a存储在内存中。由于GIL不是根据Python代码段来释放,而是根据字节码或者时间片来释放的,在之前的例子中,如果add函数在进行加法后还未在内存中保存,GIL释放,desc函数获得执行权,此时它进行装载时装载的变量total是未进行加法操作的total,因此相当于之前的add函数失去了作用,在进行多次循环后,程序的运行结果自然不为0。这种情况称为竞态条件(race condition),即使没有GIL,也会出现这种问题。解决方法是使用锁机制,将会在后面的文章中提到。

还有一种条件会导致GIL释放,那就是当程序遇到IO操作和time.sleep将程序阻塞的时候,因此多线程对于处理IO操作的问题非常有效。



作者:NWKYEKJ
链接:https://www.jianshu.com/p/fb81d5570f05
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
https://blog.csdn.net/qq_50840738/article/details/123861602
https://blog.csdn.net/Runaway_pilot/article/details/125459913
posted @ 2022-09-29 17:44  寒风孤影,江湖故人  阅读(176)  评论(0编辑  收藏  举报