GIL全局解释器锁和进程池.线程池

时间:2022-10-19 18:08:04

GIL全局解释器锁

  GIL本质就是一把互斥锁,是夹在解释器身上的,同一个进程内的所有线程都需要先抢到GIl锁,才能执行解释器代码

 

  GIL的优缺点:

    优点:保证Cpython解释器内存管理的线程安全

    缺点:同一个进程内所有的线程同一时刻只能有一个执行,也就是说Cpython解释器的多线程无法实现并行,无法取得多核优势

  GIL与单线程

    每个进程的内存空间中都有一份python解释器的代码,所以在单线程的情况下,GIL锁没有线程争抢,只有垃圾回收机制线程会定时获取GIL权限

  GIL与多线程

    有了GIL存在,每个进程内同一时间只能有一个线程执行。

    由于CPU只能提升运算能力,所以:

      在处理IO密集型的任务时,应该使用  多线程

      在处理计算密集型的任务时,应该使用 多进程

GIL的原理图示

GIL全局解释器锁和进程池.线程池

 

解释:py程序的执行,每次都是先启动python解释器,开辟的内存空间中首先就会有Python解释器的代码,其他py程序的代码,都是要交给解释器的代码去解释执行的,那么在多线程的情况下,为了解释器级别的数据安全,每个线程必须在拿到GIL锁的权限后,才能执行,GIL锁实际上就实现了Python的线程安全。(需要注意的是,Python的垃圾回收机制也是一个线程,默认开启。)

 

 

GIL与互斥锁

  GIL只能实现解释器级别的数据安全,对于线程内部的用户自己的数据,还是需要枷锁处理

  GIL全局解释器锁和进程池.线程池

GIL全局解释器锁和进程池.线程池GIL全局解释器锁和进程池.线程池
分析:
  #1.100个线程去抢GIL锁,即抢执行权限
     #2. 肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
     #3. 极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,*交出执行权限,即释放GIL
    #4.直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
GIL和互斥锁分析
GIL全局解释器锁和进程池.线程池GIL全局解释器锁和进程池.线程池
#不加锁:并发执行,速度快,数据不安全
from threading import current_thread,Thread,Lock
import os,time
def task():
    global n
    print('%s is running' %current_thread().getName())
    temp=n
    time.sleep(0.5)
    n=temp-1


if __name__ == '__main__':
    n=100
    lock=Lock()
    threads=[]
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()

    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:0.5216062068939209 n:99
'''


#不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
from threading import current_thread,Thread,Lock
import os,time
def task():
    #未加锁的代码并发运行
    time.sleep(3)
    print('%s start to run' %current_thread().getName())
    global n
    #加锁的代码串行运行
    lock.acquire()
    temp=n
    time.sleep(0.5)
    n=temp-1
    lock.release()

if __name__ == '__main__':
    n=100
    lock=Lock()
    threads=[]
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:53.294203758239746 n:0
'''

#有的同学可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊
#没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是
#start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
#单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.
from threading import current_thread,Thread,Lock
import os,time
def task():
    time.sleep(3)
    print('%s start to run' %current_thread().getName())
    global n
    temp=n
    time.sleep(0.5)
    n=temp-1


if __name__ == '__main__':
    n=100
    lock=Lock()
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        t.start()
        t.join()
    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 start to run
Thread-2 start to run
......
Thread-100 start to run
主:350.6937336921692 n:0 #耗时是多么的恐怖
'''
互斥锁与join的区别

 

进程池和线程池

  为什么要用‘池’:

      使用池子来限制并发的任务数目,限制我们的计算机在一个自己可承受的范围内去并发地执行任务

  什么时候使用进程池:并发的任务属于计算密集型

  什么时候使用线程池:并发的任务属于IO密集型

语法  

concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor: 进程池,提供异步调用

进程池实例

 from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
 import time,os,random

 def task(x):
     print('%s 接客' %os.getpid())
    time.sleep(random.randint(2,5))
     return x**2

 if __name__ == '__main__':
     p=ProcessPoolExecutor() # 默认开启的进程数是cpu的核数

     for i in range(20):
         p.submit(task,i)

线程池实例

from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import time,os,random

def task(x):
    print('%s 接客' %x)
    time.sleep(random.randint(2,5))
    return x**2

if __name__ == '__main__':
    p=ThreadPoolExecutor(4) # 默认开启的线程数是cpu的核数*5

    # alex,武佩奇,杨里,吴晨芋,张三

    for i in range(20):
        p.submit(task,i)

 

阻塞与非阻塞(指的是程序的两种运行状态)

  阻塞:遇到IO就发生阻塞,程序一旦遇到阻塞操作就会停在原地,并且立刻释放CPU资源

  非阻塞(就绪态或者运行态):没有遇到IO操作,或者通过某种手段让程序即便是遇到IO操作也不会停在原地,可以继续执行其他操作,力求尽可能多的占有CPU

同步与异步指的是提交任务的两种方式

  同步调用:提交完任务后,就在原地等待,直到任务运行完毕后,拿到任务的返回值,才继续执行下一行代码、

  异步调用:提交完任务后,不在原地等待,直接执行下一行代码,在最后可以代码每次任务的返回值