GIL全局解释器锁 | 死锁现象

全局解释器锁GIL

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

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

  a、设置 GIL;

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

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

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

  e、解锁 GIL;

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

什么是GIL锁

点击查看详情
GIL (Global Interperter Lock) 称作全局解释器锁。
GIL并不是Python语言的特性,它是在实现Python解释器时引用的一个概念。GIL只在CPython解释器上存在。
不过,在Python的解释器中,使用最多的都是CPython解释器,所以我们不可避免的会遇到GIL。
在使用互斥锁解决代码中的资源竞争问题时,当一个线程执行时,会将全局共享的资源上锁,当线程执行完成后,将锁解开,释放资源,其他线程才能够使用。
GIL的作用与互斥锁的作用相似,是为了解决解释器中多个线程资源竞争的问题。

GIL锁:全局解释器锁,在解释器之上的一把大锁,线程必须获得这把锁,才能执行,只针对与cpython解释器

为什么开设全局解释锁GIL

1 全局解释器锁,GIL锁(cpython解释器的问题)
 同一个时刻,在一个进程中可以开多个线程,但只能有一条线程在执行
起初设置GIL仅仅是为了做垃圾回收,在当时还只有单核,并无多核。所以开了多条线程也不会被多个CPU调动执行,因为只有单核。线程拿到这把锁才能运行。在当时是没有问题,因为只有一个CPU。但是随着多核CPU的出现,假设电脑是四核,一个进程四条线程,理论一个线程一核,但是Python不行,在一个线程中开四条线程并不会被四个核运行,同一时刻只能有一个线程在一个核运行,就是因为GIL原因。
    -python需要做垃圾回收(gc)
    -垃圾回收线程,进行垃圾回收
    -设计了一个大锁(GIL锁),只有拿到这把锁的线程,才能执行
    -同一时刻,在一个进程中,可以开多个线程,但是只能有一条线程在执行
    -因此python不能利用多核优势
  

### 只针对与cpython解释器(其他解释器,包括其他语言不这样)
2 如果是计算密集型:要开进程
3 如果是io密集型:要开线程

验证GIL锁的存在

点击查看代码
from threading import Thread
import time
m = 100
def test():
    global m
    tmp = m
    tmp -= 1
    m = tmp
for i in range(100):
    t = Thread(target=test)
    t.start()
time.sleep(3)
print(m)


结果:
    0
    
"""
同一个进程下的多个线程虽然有GIL的存在不会出现并行的效果
但是如果线程内有IO操作还是会造成数据的错乱 这个时候需要我们额外的添加互斥锁
"""

GIL 解释器锁会在两种情况下释放

1.主动释放


遇到 IO 操作或者分配的 CPU 时间片到时间了。

注意,GIL存在的意义在于维护线程安全,x=10涉及到IO操作,如果也被当成普通的IO操作,主动交出GIL,那么一定会出现数据不安全问题,所以x=10一定是被区分对待了。

至于x=10如何实现的被区分对待,这其实很好理解,任何的io操作都是向操作系统发送系统调用,即调用操作系统的某一接口实现的,比如变量赋值操作肯定是调用了一种接口,文件读写操作肯定也是调用了一种接口,网络io也是调用了某一种接口,这就给区分对待提供了实现的依据,即变量赋值操作并不属于主动释放的范畴,这样GIL在线程安全方面才会有所作为

2.被动释放

python3.2之后定义了一个全局变量

/ Python/ceval.c /*

*static volatile int gil_drop_request = 0;

注意当只有一个线程时,该线程会一直运行,不会释放GIL,当有多个线程时

例如thead1,thread2

如果thread1一直没有主动释放掉GIL,那肯定不会让他一直运行下去啊

实际上在thread1运行的过程时,thread2就会执行一个cv_wait(gil,TIMEOUT)的函数

(默认TIMEOUT值为5milliseconds,但是可以修改),一旦到了时间,就会将全局变量

gil_drop_request = 1;,线程thread1就会被强制释放GIL,然后线程thread2开始运行并

返回一个ack给线程thread1,线程thread1开始调用cv_wait(gil,TIMEOUT

死锁

什么是死锁

所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁

点击查看死锁代码
from threading import Thread, Lock
import time

A = Lock()
B = Lock()


class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        A.acquire()
        print('%s 抢到了A锁' % self.name)  # current_thread().name  获取线程名称
        B.acquire()
        print('%s 抢到了B锁' % self.name)
        time.sleep(1)
        B.release()
        print('%s 释放了B锁' % self.name)
        A.release()
        print('%s 释放了A锁' % self.name)

    def func2(self):
        B.acquire()
        print('%s 抢到了B锁' % self.name)
        A.acquire()
        print('%s 抢到了A锁' % self.name)
        A.release()
        print('%s 释放了A锁' % self.name)
        B.release()
        print('%s 释放了B锁' % self.name)

for i in range(10):
    obj = MyThread()
    obj.start()
 
"""就算知道锁的特性及使用方式 也不要轻易的使用 因为容易产生死锁现象"""

线程1 先开始执行func1,分布拿到AB锁,然后释放
线程1 先执行func2,先拿到了B锁,开始sleep
线程2 先拿到了A锁
这时候形成了僵局,线程2想要线程1手里的B锁,线程1想要线程2里的A锁。

解决死锁问题

点击查看代码
### 解决死锁问题  RLock:可重入,可以重复acquire,获得几次,就要释放几次
from threading import Thread, Lock,Rlock
import time


A = RLock()   # 解决死锁问题
B = A


class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        A.acquire()
        print('%s 抢到了A锁' % self.name)  # current_thread().name  获取线程名称
        B.acquire()
        print('%s 抢到了B锁' % self.name)
        time.sleep(1)
        B.release()
        print('%s 释放了B锁' % self.name)
        A.release()
        print('%s 释放了A锁' % self.name)

    def func2(self):
        B.acquire()
        print('%s 抢到了B锁' % self.name)
        A.acquire()
        print('%s 抢到了A锁' % self.name)
        A.release()
        print('%s 释放了A锁' % self.name)
        B.release()
        print('%s 释放了B锁' % self.name)

for i in range(10):
    obj = MyThread()
    obj.start()
 

总结

  • 在Cpython解释器中才有GIL锁的存在(GIL与解释器有关)
  • GIL本质上其实也是一把互斥锁(并发变串行,牺牲效率保证安全)
  • GIL的存在是由于Cpython解释器创建时期只存在单核的计算机,Cpython中的内存管理不是线程安全的垃圾回收机制。
  • 同一时刻,在一个进程中,可以开多个线程,但是只能有一条线程在执行
  • 在python中同一进程下的多个线程无法实现并行(可以并发)
  • 不要轻易的使用锁 容易造成死锁现象

python多线程是否没用

# 是否有用需要看情况而定(程序的类型)
# IO密集型
	eg:四个任务  每个任务耗时10s
    	开设多进程没有太大的优势	42s+
        	遇到IO就需要切换 并且开设进程还需要申请内存空间和拷贝代码
        开设多线程有优势
			不需要消耗额外的资源   2s+
# 计算密集型
	eg:四个任务	 每个任务耗时10s
        计算密集型任务的特点是要进行大量的计算
        开设多进程可以利用多核优势	5s+
      	开设多线程无法利用多核优势   23s+
"""
多进程结合多线程
可以处理计算密集型与IO密集型
"""
"""IO密集型"""
# from multiprocessing import Process
# from threading import Thread
# import threading
# import os,time
# def work():
#     time.sleep(2)
#
#
# if __name__ == '__main__':
#     l=[]
#     print(os.cpu_count()) #本机为6核
#     start=time.time()
#     for i in range(400):
#         # p=Process(target=work) #耗时42.54s多,大部分时间耗费在创建进程上
#         p=Thread(target=work) #耗时2.08s多
#         l.append(p)
#         p.start()
#     for p in l:
#         p.join()
#     stop=time.time()
#     print('run time is %s' %(stop-start))


"""计算密集型"""
from multiprocessing import Process
from threading import Thread
import os,time
def work():
    res=0
    for i in range(100000000):
        res*=i
if __name__ == '__main__':
    l=[]
    print(os.cpu_count())  # 本机为6核
    start=time.time()
    for i in range(6):
        # p=Process(target=work) #耗时5.35s多
        p=Thread(target=work) #耗时23.37s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))

总结:多线程是否无用要看程序的类型,完全可以进程与线程结合使用带到最快效率

IO密集型 :如:socket,爬虫,web  用多线程
计算密集型 :如 :金融分析(需要大量计算的)  用多进程
上一篇:WinForm窗体常用属性


下一篇:C#6.0 的新特性