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函数来获取任务的处理结果

     

  • 相关阅读:
    LeetCode Missing Number (简单题)
    LeetCode Valid Anagram (简单题)
    LeetCode Single Number III (xor)
    LeetCode Best Time to Buy and Sell Stock II (简单题)
    LeetCode Move Zeroes (简单题)
    LeetCode Add Digits (规律题)
    DependencyProperty深入浅出
    SQL Server存储机制二
    WPF自定义RoutedEvent事件示例代码
    ViewModel命令ICommand对象定义
  • 原文地址:https://www.cnblogs.com/1624413646hxy/p/10982140.html
Copyright © 2011-2022 走看看