zoukankan      html  css  js  c++  java
  • Python Day 37:GIL锁/进程池/线程池/阻塞与非阻塞/同步与异步/异步回调/

    ## GIL锁:
    
    ```python
    官方解释:
    '''
    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.)
    '''
    
    释义:
    在CPython中,这个全局解释器锁,也称为GIL,是一个互斥锁,防止多个线程在同一时间执行Python字节码,这个锁是非常重要的,因为CPython的内存管理非线程安全的,很多其他的特性依赖于GIL,所以即使它影响了程序效率也无法将其直接去除
    需要知道的是,解释器并不只有 CPython,还有PyPy,JPython等等。GIL也仅存在与CPython中,这并不是Python这门语言的问题,而是CPython解释器的问题!
    
    
    首先必须明确执行一个py文件,分为三个步骤
    1. 从硬盘加载Python解释器到内存
    2. 从硬盘加载py文件到内存
    3. 解释器解析py文件内容,交给CPU执行
    其次需要明确的是每当执行一个py文件,就会立即启动一个python解释器
    
    GIL,叫做全局解释器锁,加到了解释器上,并且是一把互斥锁,那么这把锁对应用程序到底有什么影响?
    这就需要知道解释器的作用,以及解释器与应用程序代码之间的关系
    py文件中的内容本质都是字符串,只有在被解释器解释时,才具备语法意义,解释器会将py代码翻译为当前系统支持的指令交给系统执行。
    当进程中仅存在一条线程时,GIL锁的存在没有不会有任何影响,但是如果进程中有多个线程时,GIL锁就开始发挥作用了
    
    开启子线程时,给子线程指定了一个target表示该子线程要处理的任务即要执行的代码。代码要执行则必须交由解释器,即多个线程之间就需要共享解释器,为了避免共享带来的数据竞争问题,于是就给解释器加上了互斥锁!
    
    由于互斥锁的特性,程序串行,保证数据安全,降低执行效率,GIL将使得程序整体效率降低!
    
    ```
    
    ## 为什么需要GIL互斥锁
    
    ```python
    在使用Python中进行编程时,程序员无需参与内存的管理工作,这是因为Python有自带的内存管理机制,简称GC。那么GC与GIL有什么关联?
    
    要搞清楚这个问题,需先了解GC的工作原理,Python中内存管理使用的是引用计数,每个数会被加上一个整型的计数器,表示这个数据被引用的次数,当这个整数变为0时则表示该数据已经没有人使用,成了垃圾数据。
    
    当内存占用达到某个阈值时,GC会将其他线程挂起,然后执行垃圾清理操作,垃圾清理也是一串代码,也就需要一条线程来执行。
    
    ,GC与其他线程都在竞争解释器的执行权,而CPU何时切换,以及切换到哪个线程都是无法预支的,这样一来就造成了竞争问题,假设线程1正在定义变量a=10,而定义变量第一步会先到到内存中申请空间把10存进去,第二步将10的内存地址与变量名a进行绑定,如果在执行完第一步后,CPU切换到了GC线程,GC线程发现10的地址引用计数为0则将其当成垃圾进行了清理,等CPU再次切换到线程1时,刚刚保存的数据10已经被清理掉了,导致无法正常定义变量。
    
    当然其他一些涉及到内存的操作同样可能产生问题问题,为了避免GC与其他线程竞争解释器带来的问题,CPython简单粗暴的给解释器加了互斥锁
    有了GIL后,多个线程将不可能在同一时间使用解释器,从而保证了解释器的数据安全。
    '''# GIL的加锁与解锁时机
    加锁的时机:在调用解释器时立即加锁
    解锁时机:
    - 当前线程遇到了IO时释放
    - 当前线程执行时间超过设定值时释放'''
    
    ```
    
    
    
    ## 四.关于GIL的性能讨论
    
    ```python
    GIL的优点:
    - 保证了CPython中的内存管理是线程安全的
    GIL的缺点:
    - 互斥锁的特性使得多线程无法并行
    但我们并不能因此就否认Python这门语言,其原因如下:
    
    1. GIL仅仅在CPython解释器中存在,在其他的解释器中没有,并不是Python这门语言的缺点
    2. 在单核处理器下,多线程之间本来就无法真正的并行执行
    3. 在多核处理下,运算效率的确是比单核处理器高,但是要知道现代应用程序多数都是基于网络的(qq,微信,爬虫,浏览器等等),CPU的运行效率是无法决定网络速度的,而网络的速度是远远比不上处理器的运算速度,则意味着每次处理器在执行运算前都需要等待网络IO,这样一来多核优势也就没有那么明显了
    
    1. 总结:**
       1.单核下无论是IO密集还是计算密集GIL都不会产生任何影响
       2.多核下对于IO密集任务,GIL会有细微的影响,基本可以忽略
       3.Cpython中IO密集任务应该采用多线程,计算密集型应该采用多进程
        '对于io密集型 使用多进程反而可能效率比不上多线程 
    另外:之所以广泛采用CPython解释器,就是因为大量的应用程序都是IO密集型的,还有另一个很重要的原因是CPython可以无缝对接各种C语言实现的库,这对于一些数学计算相关的应用程序而言非常的happy,直接就能使用各种现成的算法
        '对于运算密集型程序,使用多进程要比多线程效率高'
        '注意:并行的任务不能太多 开启进程非常消耗资源
    ```
    
    
    
    ## 五自定义线程锁与GIL的区别
    
    ```python
    GIL保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等,具体参考垃圾回收机制详解。
    
    对于程序中自己定义的数据则没有任何的保护效果,这一点在没有介绍GIL前我们就已经知道了,所以当程序中出现了共享自定义的数据时就要自己加锁,如下例:
    
    from threading import Thread,Lock
    import time
    a = 0
    def task():
        global a
        temp = a
        time.sleep(0.01) 
        a = temp + 1
       
    t1 = Thread(target=task)
    t2 = Thread(target=task)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(a)
    过程分析:
    
    1.线程1获得CPU执行权,并获取GIL锁执行代码 ,得到a的值为0后进入睡眠,释放CPU并释放GIL
    2.线程2获得CPU执行权,并获取GIL锁执行代码 ,得到a的值为0后进入睡眠,释放CPU并释放GIL
    3.线程1睡醒后获得CPU执行权,并获取GIL执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU并释放GIL
    4.线程2睡醒后获得CPU执行权,并获取GIL执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU并释放GIL,最后a的值也就是1
    之所以出现问题是因为两个线程在并发的执行同一段代码,解决方案就是加锁!
    ```
    
    ## 六:进程池与线程池
    
    ### 什么是进程/线程池?
    
    池表示一个容器,本质上就是一个存储进程或线程的列表
    
    ### 池子中存储线程还是进程?
    
    如果是IO密集型任务使用线程池,如果是计算密集任务则使用进程池
    
    ### 为什么需要进程/线程池?
    
    在很多情况下需要控制进程或线程的数量在一个合理的范围,例如TCP程序中,一个客户端对应一个线程,虽然线程的开销小,但肯定不能无限的开,否则系统资源迟早被耗尽,解决的办法就是控制线程的数量。
    
    线程/进程池不仅帮我们控制线程/进程的数量,还帮我们完成了线程/进程的创建,销毁,以及任务的分配
    
    进程池的使用:
    
    ```python
    from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
    import time,os
    
    # 创建进程池,指定最大进程数为3,此时不会创建进程,不指定数量时,默认为CPU和核数
    pool = ProcessPoolExecutor(3)
    def task():
        time.sleep(1)
        print(os.getpid(),"working..")
    
    if __name__ == '__main__':
        for i in range(10):
            pool.submit(task) # 提交任务时立即创建进程
    
        # 任务执行完成后也不会立即销毁进程
        time.sleep(2)
        
        for i in range(10):
            pool.submit(task) #再有新任务是 直接使用之前已经创建好的进程来执行
    
    ```
    
    线程池的使用
    
    ```python
    from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
    from threading import current_thread,active_count
    import time,os
    
    # 创建进程池,指定最大线程数为3,此时不会创建线程,不指定数量时,默认为CPU和核数*5
    pool = ThreadPoolExecutor(3)
    print(active_count()) # 只有一个主线
    
    def task():
        time.sleep(1)
        print(current_thread().name,"working..")
    
    if __name__ == '__main__':
        for i in range(10):
            pool.submit(task) # 第一次提交任务时立即创建线程
    
        # 任务执行完成后也不会立即销毁
        time.sleep(2)
    
        for i in range(10):
            pool.submit(task) #再有新任务时 直接使用之前已经创建好的线程来执行
    
    
    ```
    
    # 七.同步异步-阻塞非阻塞
    
    同步异步-阻塞非阻塞,经常会被程序员提及,并且概念非常容易混淆!
    
    #### 阻塞非阻塞指的是程序的运行状态
    
    阻塞:当程序执行过程中遇到了IO操作,在执行IO操作时,程序无法继续执行其他代码,称为阻塞!
    
    非阻塞:程序在正常运行没有遇到IO操作,或者通过某种方式使程序即时遇到了也不会停在原地,还可以执行其他操作,以提高CPU的占用率
    
    #### 同步-异步 指的是提交任务的方式
    
    同步指调用:发起任务后必须在原地等待任务执行完成,才能继续执行
    
    异步指调用:发起任务后必须不用等待任务执行,可以立即开启执行其他操作
    
    **同步会有等待的效果但是这和阻塞是完全不同的,阻塞时程序会被剥夺CPU执行权,而同步调用则不会!**
    
    很明显异步调用效率更高,但是任务的执行结果如何获取呢?
    
    程序中的异步调用并获取结果方式1:pool.shutdown(wait=True)
    
    ```python
    from concurrent.futures import ThreadPoolExecutor
    from threading import current_thread
    import time
    
    pool = ThreadPoolExecutor(3)
    def task(i):
        time.sleep(0.01)
        print(current_thread().name,"working..")
        return i ** i
    
    if __name__ == '__main__':
        objs = []
        for i in range(3):
            res_obj = pool.submit(task,i) # 异步方式提交任务# 会返回一个对象用于表示任务结果
            objs.append(res_obj)
    
    # 该函数默认是阻塞的 会等待池子中所有任务执行结束后执行
    pool.shutdown(wait=True)
    
    # 从结果对象中取出执行结果
    for res_obj in objs:
        print(res_obj.result())
    print("over")
    
    
    
    ```
    
    程序中的异步调用并获取结果方式2:任务结果的对象:res_obj.result()
    
    ```python
    from concurrent.futures import ThreadPoolExecutor
    from threading import current_thread
    import time
    
    pool = ThreadPoolExecutor(3)
    def task(i):
        time.sleep(0.01)
        print(current_thread().name,"working..")
        return i ** i
    
    if __name__ == '__main__':
        objs = []
        for i in range(3):
            res_obj = pool.submit(task,i) # 会返回一个对象用于表示任务结果
            print(res_obj.result()) #result是同步的一旦调用就必须等待 任务执行完成拿到结果
    print("over")
    
    
    
    ```
    
    # 8.异步回调
    
    ### 什么是异步回调
    
    异步回调指的是:在发起一个异步任务的同时指定一个函数,在异步任务完成时会自动的调用这个函数
    
    ### 为什么需要异步回调
    
    之前在使用线程池或进程池提交任务时,如果想要处理任务的执行结果则必须调用result函数或是shutdown函数,而它们都是是阻塞的,会等到任务执行完毕后才能继续执行,这样一来在这个等待过程中就无法执行其他任务,降低了效率,所以需要一种方案,即保证解析结果的线程不用等待,又能保证数据能够及时被解析,该方案就是异步回调
    
    异步回调实例:
    
    ```python
    import requests,re,os,random,time
    from concurrent.futures import ProcessPoolExecutor
    
    def get_data(url):
        print("%s 正在请求%s" % (os.getpid(),url))
        time.sleep(random.randint(1,2))
        response = requests.get(url)
        print(os.getpid(),"请求成功 数据长度",len(response.content))
        #parser(response) # 3.直接调用解析方法  哪个进程请求完成就那个进程解析数据  强行使两个操作耦合到一起了
        return response
    
    def parser(obj):
        data = obj.result()
        htm = data.content.decode("utf-8")
        ls = re.findall("href=.*?com",htm)
        print(os.getpid(),"解析成功",len(ls),"个链接")
    
    if __name__ == '__main__':
        pool = ProcessPoolExecutor(3)
        urls = ["https://www.baidu.com",
                "https://www.sina.com",
                "https://www.python.org",
                "https://www.tmall.com",
                "https://www.mysql.com",
                "https://www.apple.com.cn"]
        # objs = []
        for url in urls:
            # res = pool.submit(get_data,url).result() # 1.同步的方式获取结果 将导致所有请求任务不能并发
            # parser(res)
    
            obj = pool.submit(get_data,url) # 
            obj.add_done_callback(parser) # 4.使用异步回调,保证了数据可以被及时处理,并且请求和解析解开了耦合
            # objs.append(obj)
            
        # pool.shutdown() # 2.等待所有任务执行结束在统一的解析
        # for obj in objs:
        #     res = obj.result()
        #     parser(res)
        # 1.请求任务可以并发 但是结果不能被及时解析 必须等所有请求完成才能解析
        # 2.解析任务变成了串行,
        
    ```
  • 相关阅读:
    android代码签名和混乱的包装
    C# 中对WinForm窗体中的控件快速设置TableIndex次序
    常用的Oracle数据库语句 (待更新完毕)
    [转] C# 键盘中的按键对应的KeyValue
    Oracle 数据库中日期时间的插入操作
    C#操作Excel,对Sheet插入次序的控制 (有待完善)
    Simditor图片上传
    文件类似的推理 -- 超级本征值(super feature)
    leetcode 名单 Insertion Sort List
    汉高澳大利亚sinox2014电影播放flash最好的办法是安装游戏windows文本firefox
  • 原文地址:https://www.cnblogs.com/huhongpeng/p/10994393.html
Copyright © 2011-2022 走看看