python之路---并发编程之线程&GIL

什么是GIL

'''
定义:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple 
native threads from executing Python bytecodes at once. This lock is necessary mainly 
because CPython’s memory management is not thread-safe. (However, since the GIL 
exists, other features have grown to depend on the guarantees that it enforces.)
'''

可以从上面一段关于GIL的定义中总结出三个方面

1.对象是Cpython

GIL并不是python的特性,他只是Cpython解释器为解决某一问题而引入的,像Jpython就没有GIL

2.为什么要有GIL

因为Cpython的内存管理不是线程安全的

3.GIL带来的影响

GIL(全局解释器锁)本质是一个互斥锁,它阻止了同一进程下多个线程同一时刻执行python字节码

Cpython的内存管理为什么不是线程安全的

知识储备

1.python的内存管理说的是,python的垃圾回收机制。对于一个进程来说,每个进程下都有一个专门负责垃圾回收的线程,该线程是解释器级别的,并与其它线程并发运行

2.每个对象上都有两个头部信息:类型标志符与引用计数,而python会自动回收那些引用计数为0的对象

3.执行x=1,实际上是开辟一个内存空间将1这个对象放入,然后让x这个变量名指向这个内存空间(变量的引用)

4.一个.py文件仅仅是一个文本文件,代码的执行是需要python解释器的

假设没有GIL的存在,对于垃圾回收线程和其它线程来说可以是并行的,当线程1执行x=(1,2,3)时,先开辟了内存空间,然后将(1,2,3)这个对象放入,这时候垃圾回收线程有可能恰巧检测到这个对象的引用为0,它就会回收,这就造成了数据的不安全

而GIL的存在就是给解释器加了锁,谁被分配到这把锁谁就能执行(谁就能将所要执行的代码作为参数传给python解释器)

GIL带来的影响

有了GIL的存在意味着在一个进程内,多个线程是无法同时运行的(无法实现并行),也就说同一个进程的多个线程无法利用多核优势,但是这并不能说Cpython的线程就是一个鸡肋

什么是多核优势

多核:多个CPU(由运算器与控制器组成)--->用来完成计算任务/无法进行IO

于单核来说不存在多核优势这一说法

于多核,分两种情况‘

对于IO密集型的任务,多核是没有意义的,相反多线程的效率更高(开启线程的消耗远小于进程)

import time,os
from multiprocessing import Process
from threading import Thread


def foo(name):
    print('%s正在进行IO...'%name)
    time.sleep(3)


if __name__ == '__main__':
    start = time.time()
    l = []
    for i in range(4):
        # p = Process(target=foo,args=('进程%s'%i,))
        p = Thread(target=foo,args=('线程%s'%i,))
        l.append(p)
        p.start()
    for p in l:
        p.join()
    end = time.time()
    print('花费时间:', end - start)
    print('cpu核数:',os.cpu_count())


# 多进程运行结果:
进程1正在进行IO...
进程0正在进行IO...
进程2正在进行IO...
进程3正在进行IO...
花费时间: 3.2010157108306885
cpu核数: 4

# 多线程运行结果
线程0正在进行IO...
线程1正在进行IO...
线程2正在进行IO...
线程3正在进行IO...
花费时间: 3.0021512508392334
cpu核数: 4

可以从上面看出对于IO密集型任务来说,多进程的花费时间是远大于线程的(而且这种相差,会随着进程数的增大而更加明显)

对于计算密集型的任务,多进程的优势就体现出来了

import time,os
from multiprocessing import Process
from threading import Thread


def task(name):
    num = 0
    print("%s正在计算..."%name)
    for i in range(100000000):
        num *= i


if __name__ == '__main__':
    start = time.time()
    p_list = []
    for i in range(4):
        p = Process(target=task,args=('进程%s'%i,))
        # p = Thread(target=task,args=('线程%s'%i,))
        p_list.append(p)
        p.start()
    for p in p_list:
        p.join()

    end = time.time()
    print('花费时间', end - start)
    print('cpu核数:',os.cpu_count())


# 多进程运行结果
进程2正在计算...
进程3正在计算...
进程1正在计算...
进程0正在计算...
花费时间 18.54850435256958
cpu核数: 4

# 多线程运行结果
线程0正在计算...
线程1正在计算...
线程2正在计算...
线程3正在计算...
花费时间 41.12251615524292
cpu核数: 4

总结

1.对于IO密集型的任务:使用多线程

   对于计算密集型的任务:使用多进程

2.虽然同一个进程下的多个线程无法实现并行,但是不影响并发

GIL与Lock

GIL的存在是为了保护解释器级别的数据安全(垃圾回收),而代码执行的共享数据是需要自定义的互斥锁

import time
from threading import Thread,Lock

x = 100


def foo():
    global x
    with L:
        temp = x-1
        time.sleep(0.1)
        x = temp


l = []
L = Lock()
for i in range(100):
    t = Thread(target=foo)
    l.append(t)
    t.start()

for t in l:
    t.join()

print(x)

# 没有互斥锁的情况下
执行结果 x = 99

# 加了互斥锁的情况下
执行结果 x = 0

分析

第一个起来的线程-----被分配到GIL(相当于获得调用python解释器的权限)------>执行了global,又拿到了互斥锁L,再执行,当遇到sleep(模拟IO行为),发生阻塞------->被操作系统剥夺CPU,被迫释放GIL(注意,互斥锁L并没有释放),其余在等待获得GIL的线程开始争抢(其实是操作系统分配),但是被阻塞在了获得互斥锁L这里,待第一个线程结束IO行为,与其它线程参与到争抢GIL之中(也只有第一个线程拿到GIL才有意义)

猜你喜欢

转载自blog.csdn.net/ltfdsy/article/details/82492876