zoukankan      html  css  js  c++  java
  • day 37

    什么是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.)
    '''

    释义:

    存在于cpython中是一个全局解释锁,也被称为GIL锁,也是一把互斥锁,是为了防止多个线程在同一个时间执行python字节码,

    这个锁是非常重要的,因为CPython的内存管理非线程安全的,很多其他的特性依赖于GIL,所以即使它影响了程序效率也无法将其直接去除。

    在cpython中,GIL锁会仈=把线程的并行变成串行,导致运行效率变低。

    另外解释器也不只有cpython还有pypy,jpython等等,GIL锁也只是存在于cpython中,所以这不是语言的问题,是解释器本身的问题,而cpython可以

    调用c语言中一大堆库,python使用者来说是非常实用的。

    GIL带来的问题

    运行py文件的三个步骤:

    1  从硬盘中加载python解释器到内存中

    2  从硬盘中加载到py文件到内存中

    3 解释器对py文件内容进行解析,再交给cpu执行

    需要明确的是每当执行一个py文件就会开启一个python解释器

    一一条线程在解释器上运行的时候GIL几乎没有任何影响,但是到解释器开始跑多线程时,GIL锁带来的问题就出现了,GIL是一种
    互斥锁,多个进程之间需要共享解释器资源,为了避免共享带来的数据错乱的问题,于是就给解释器加上了互斥锁。

    由于互斥锁的特性,并行的程序变成了串行,保证数据安全,但也降低了执行效率,GIL将使得程序整体效率降低!

     

    为什么需要GIL

    GIL 与GC之间的渊源

    在使用Python中进行编程时,程序员无需参与内存的管理工作,这是因为Python有自带的内存管理机制,简称GC。那么GC与GIL有什么关联?

    要搞清楚这个问题,需先了解GC的工作原理,Python中内存管理使用的是引用计数,每个数会被加上一个整型的计数器,表示这个数据被引用的次数,当这个整数变为0时则表示该数据已经没有人使用,成了垃圾数据。

    当内存占用达到某个阈值时,GC会将其他线程挂起,然后执行垃圾清理操作,垃圾清理也是一串代码,也就需要一条线程来执行。

    示范代码

    from threading import  Thread
    def task():
        a = 10
        print(a)
    
    # 开启三个子线程执行task函数
    Thread(target=task).start()
    Thread(target=task).start()
    Thread(target=task).start()

    #打印结果

    10
    10
    10

    ![image-20190102224044044](https://ws1.sinaimg.cn/large/006tNbRwly1fysmcbuzcxj30r00ix3zs.jpg)

    从图中可以看出GC在于线程之间竞争解释器的执行权,cpu什么时候切换,切换到哪里去我们并不知道,假如线程正在定义

    a = 10首先先申请内存空间将10放进去后,这时候切换到GC持有执行权,一看有个10没有被引用,那肯定就把它清理掉了,这样

    还怎么定义变量a = 10,所以加互斥锁非常有必要了,


    有了GIL锁以后多个线程不可能同时在同一个时间内使用解释器,保证了解释器的数据安全。

    GIL的加锁与解锁时机

    枷锁时机:在解释器被调用时立即加锁。

    解锁时机:当线程遇到IO时或者线程执行时间到达设定值。

    GIL的优点:保证了CPython中的内存管理是线程安全的

    GIL的缺点:互斥锁的特性使得多线程无法并行

    在单核的情况下,无论是IO密集还是计算莫密集GIL都不会产生任何影响

    多核下对于IO密集任务,GIL会有细微的影响,基本可以忽略

    Cpython中IO密集任务应该采用多线程,计算密集型应该采用多进程

    另外:之所以广泛采用CPython解释器,就是因为大量的应用程序都是IO密集型的,还有另一个很重要的原因是CPython可以无缝对接各种C语言实现的库,这对于一些数学计算相关的应用程序而言非常的happy,直接就能使用各种现成的算法

    IO密集测试

    #多线程
    def task():
        time.sleep(1)
    
    if __name__ == "__main__":
        sta = time.time()
        ls = []
        for i in range(50):
            # p = Process(target=task)
            p = Thread(target=task)
            p.start()
            ls.append(p)
        for p in ls:
            p.join()
        print(time.time() - sta)
    #打印结果
    1.008044958114624
    #多进程
    def task():
        time.sleep(1)
    
    if __name__ == "__main__":
        sta = time.time()
        ls = []
        for i in range(50):
            p = Process(target=task)
            # p = Thread(target=task)
            p.start()
            ls.append(p)
        for p in ls:
            p.join()
        print(time.time() - sta)
    #打印结果
    9.1587073802948

    自定义锁与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()
    只是有GIL锁的情况下,会先有一个线程拿到GIL锁,遇到IO后会释放锁,此时数据还没有被修改,当锁释放后
    下一个进程也会读取数据,读取到的是没有被修改过的数据,进程只会修改自己读取到的数据,所以最终运行完的
    结果为1
    from threading import Thread,Lock
    import time
    
    lock = Lock()
    a = 0
    def task():
        global a
        lock.acquire()
        temp = a
        time.sleep(0.01)
        a = temp + 1
        lock.release() 
    
        
    t1 = Thread(target=task)
    t2 = Thread(target=task)
    
    t1.start()
    t2.start()
    
    t1.join()
    t2.join()
    print(a)
    加上自己的锁以后,子线程会取得GIL锁后对需要进行修改的地方上锁没有完成修改不会释放锁,这样下一个线程拿到的数据就是修改过的数据
    这样重复在子线程运行完毕以后就可以完成对数据的修改。

    进程池与线程池

    什么是进程/线程池?

    池表示一个容器,本质上就是一个存储进程或线程的列表

    当是IO密集型任务使用线程池,是计算密集型就使用进程池

    为什么需要进程/线程池

    1 创建线程池   可以自己指定线程数,不指定就默认cpu个数乘以5
    不会立即加载,等到有任务提交后才会开启
    不仅帮我们管理线程的开启和销毁,还帮助我们管理任务的分配,
    避免了频繁开启和销毁线程的资源浪费,任务的参数直接写道后面,是可变位置参数

    进程池的使用

    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) #再有新任务是 直接使用之前已经创建好的进程来执行

    线程池的使用

    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) #再有新任务时 直接使用之前已经创建好的线程来执行

    tcp是IO密集型所以使用多线程

    同步-异步 指的是提交任务的方式

    同步指调用:发起任务后必须在原地等待任务执行完成,才能继续执行

    异步指调用:发起任务后必须不用等待任务执行,可以立即开启执行其他操作

    同步调用不等于阻塞虽然也有等待的效果,阻塞时程序会剥夺cpu的执行权,同步调用不会。

    从字面上也可以看出是异步调用的效率更高。

    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")

    异步回调

    异步回调指的是:在发起一个异步任务的同时指定一个函数,在异步任务完成时会自动的调用这个函数

    为什么需要异步回调

    之前在使用线程池或进程池提交任务时,如果想要处理任务的执行结果则必须调用result函数或是shutdown函数,而它们都是是阻塞的,会等到任务执行完毕后才能继续执行,这样一来在这个等待过程中就无法执行其他任务,降低了效率,所以需要一种方案,即保证解析结果的线程不用等待,又能保证数据能够及时被解析,该方案就是异步回调

    import time
    from threading import Thread,current_thread
    from concurrent.futures import ThreadPoolExecutor
    import concurrent
    pool = ThreadPoolExecutor(5)
    def task():
        time.sleep(1)
        print("任务完成")
        return "我好帅"
    
    def finished(res):             #异步回调,保证主线程线程转成串行
        print(res.result())
        print("确实帅")
    
    print("start")
    
    res = pool.submit(task)         #将任务提交给线程池
    
    res.add_done_callback(finished)       #当子线程结束后异步调用取得子线程结果返回给主线程
    print(res)              #<所属的类:Future 该线程自己内存空间:at 0x20ca77fbe48 线程目前所处状态:state=running>
    print("over")

    还有一个函数 shutdown()可以关闭线程池,但是他是默认让主进程处于等待状态,这样异步调用就变成了同步调用
    这样可不是我们想要的结果

    在编写爬虫程序时,通常都是两个步骤:

    1.从服务器下载一个网页文件

    2.读取并且解析文件内容,提取有用的数据

    按照以上流程可以编写一个简单的爬虫程序

    要请求网页数据则需要使用到第三方的请求库requests可以通过pip或是pycharm来安装,在pycharm中点击settings->解释器->点击+号->搜索requests->安装

    import requests
    
    from concurrent.futures import ThreadPoolExecutor     #导入线程池
    from threading import current_thread
    
    pool = ThreadPoolExecutor()      #创建线程池
    
    def get_data(url):
        response = requests.get(url)         #抓取网页文件
        print("%s下载完成"%current_thread().name)
        return url,response.text
        # parser(url,response.text)       #解析网站文本信息
    
    
    def parser(*args):      #传过来的是一个元组形式的参数,元组内只有一个参数
        print(args)
        url,data = obj.result()             #对返回过来的线程结果进行解压赋值传回来的是一个两个值
        print("%s 长度 %s"%(url,len(data)))
        print("%s 解析完成"% current_thread().name)
    
    url = 'http://www.jd.com'
    
    obj = pool.submit(get_data,url)
    obj.add_done_callback(parser)          # 异步调用将子线程运行结束后的结果立即返回给主线程
    print("任务完成")

    总结:异步回调使用方法就是在提交任务后得到一个Futures对象,调用对象的add_done_callback来指定一个回调函数,

    如果把任务比喻为烧水,没有回调时就只能守着水壶等待水开,有了回调相当于换了一个会响的水壶,烧水期间可用作其他的事情,等待水开了水壶会自动发出声音,这时候再回来处理。水壶自动发出声音就是回调。

    注意:

    1. 使用进程池时,回调函数都是主进程中执行执行

    2. 使用线程池时,回调函数的执行线程是不确定的,哪个线程空闲就交给哪个线程

    3. 回调函数默认接收一个参数就是这个任务对象自己,再通过对象的result函数来获取任务的处理结果

     

  • 相关阅读:
    集合框架(三)
    集合框架(二)
    集合框架(一)
    第九章 持有你的对象
    UML类图
    用a标签设置锚点
    设计原则
    第八章 接口与内部类
    装配Bean
    第33条:用EnumMap代替序数索引
  • 原文地址:https://www.cnblogs.com/1624413646hxy/p/10982140.html
Copyright © 2011-2022 走看看