• 37 GIL 线程池 同步异步 阻塞非阻塞


    GIL锁

     

    GIL 全局解释器锁,是一个互斥锁. 是为了防止多个本地线程同一时间执行python代码,,Cpython的内存管理是非线程安全的

    非线程安全 即 多个线程访问同一个资源,会 有问题

    线程安全 即 多个线程访问同一个资源,不会有问题

    该锁只存在Cpython中,这并不是Python这门语言的 除了Cpython之外 Jpython, pypy,解释器

    之所以使用Cpython的原因??

    C编译过的结果可以计算机直接识别

    最主要的语言,C语言以后大量现成的,库(算法,通讯),Cpython可以无缝连接C语言的任何现成代码

    内存管理

    垃圾回收机制

    python中不需要手动管理内存 ,C,OC

    引用计数

    a = 10 10地址次数计数为1

    b = a 计数2

    b = 1 计数1

    a = 0 计数0

    当垃圾回收启动后会将计数为0的数据清除掉,回收内存

    分代回收

    自动垃圾回收其实就是说,内部会有一个垃圾回收线程,会在某一时间运行起来,开始清理垃圾

    这是可能会产生问题,例如线程1申请了内存,但是还没有使用CPU切换到了GC,GC将数据当成垃圾清理掉了

    为了解决这个问题,Cpython就给解释器加上了互斥锁!

    GIL锁作用:

    开启子线程时,给子线程指定了一个target表示该子线程要处理的任务即要执行的代码。代码要执行则必须交由解释器,即多个线程之间就需要共享解释器,为了避免共享带来的数据竞争问题,于是就给解释器加上了互斥锁!

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

    GIL锁的加锁与解锁时机

    加锁: 只要有一个线程要使用解释器就立马枷锁

    释放:

    该线程任务结束

    该线程遇到IO

    该线程使用解释器过长 默认100纳秒

    GIL的优点:

    - 保证了CPython中的内存管理是线程安全的

    GIL的缺点:

    - 互斥锁的特性使得多线程无法并行
    但我们并不能因此就否认Python这门语言,其原因如下:

    1. GIL仅仅在CPython解释器中存在,在其他的解释器中没有,并不是Python这门语言的缺点

    2. 在单核处理器下,多线程之间本来就无法真正的并行执行

    3. 在多核处理下,运算效率的确是比单核处理器高,但是要知道现代应用程序多数都是基于网络的(qq,微信,爬虫,浏览器等等),CPU的运行效率是无法决定网络速度的,而网络的速度是远远比不上处理器的运算速度,则意味着每次处理器在执行运算前都需要等待网络IO,这样一来多核优势也就没有那么明显了

    ##### 举个例子:

    任务1 从网络上下载一个网页,等待网络IO的时间为1分钟,解析网页数据花费,1秒钟

    任务2 将用户输入数据并将其转换为大写,等待用户输入时间为1分钟,转换为大写花费,1秒钟

    **单核CPU下:**1.开启第一个任务后进入等待。2.切换到第二个任务也进入了等待。一分钟后解析网页数据花费1秒解析完成切换到第二个任务,转换为大写花费1秒,那么总耗时为:1分+1秒+1秒 = 1分钟2秒

    **多核CPU下:**1.CPU1处理第一个任务等待1分钟,解析花费1秒钟。1.CPU2处理第二个任务等待1分钟,转换大写花费1秒钟。由于两个任务是并行执行的所以总的执行时间为1分钟+1秒钟 = 1分钟1秒

    可以发现,多核CPU对于总的执行时间提升只有1秒,但是这边的1秒实际上是夸张了,转换大写操作不可能需要1秒,时间非常短!

    上面的两个任务都是需要大量IO时间的,这样的任务称之为IO密集型,与之对应的是计算密集型即IO操作较少大部分都是计算任务。

    对于计算密集型任务,Python多线程的确比不上其他语言!为了解决这个弊端,Python推出了多进程技术,可以良好的利用多核处理器来完成计算密集任务。

    计算密集型的效率测试

    from multiprocessing import Process
    from threading import Thread
    import time

    def task():
       for i  in range(10000000):
           i += 1

    if __name__ == '__main__':
       start_time = time.time()
       # 多进程
       # p1 = Process(target=task)
       # p2 = Process(target=task)
       # p3 = Process(target=task)
       # p4 = Process(target=task)

       # 多线程
       p1 = Thread(target=task)
       p2 = Thread(target=task)
       p3 = Thread(target=task)
       p4 = Thread(target=task)

       p1.start()
       p2.start()
       p3.start()
       p4.start()

       p1.join()
       p2.join()
       p3.join()
       p4.join()
       
       print(time.time()-start_time)

    IO密集型的效率测试

    from multiprocessing import Process
    from threading import Thread
    import time
    def task():
       with open("test.txt",encoding="utf-8") as f:
           f.read()
    if __name__ == '__main__':
       start_time = time.time()
       # 多进程
       # p1 = Process(target=task)
       # p2 = Process(target=task)
       # p3 = Process(target=task)
       # p4 = Process(target=task)

       # 多线程
       p1 = Thread(target=task)
       p2 = Thread(target=task)
       p3 = Thread(target=task)
       p4 = Thread(target=task)

       p1.start()
       p2.start()
       p3.start()
       p4.start()

       p1.join()
       p2.join()
       p3.join()
       p4.join()

       print(time.time()-start_time)

    自定义的线程锁与GIL的区别

    GIL保护的是解释器级别的数据安全,比如对象的引用计数,垃圾分代数据等等

    对于程序中自己定义的数据则没有任何的保护效果,所以当程序中出现了共享自定义的数据时就要自己加锁

    l例子:

    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操作时释放

    时间片用完 (最大设置为100)

    进程池与线程池

    什么是进程/线程池?

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

    池子中存储线程还是进程?

    如果是IO密集型任务使用线程池,如果是计算密集任务则使用进程池

    python中ThreadPoolExecutor(线程池)与ProcessPoolExecutor(进程池)都是concurrent.futures模块下的,主线程(或进程)中可以获取某一个线程(进程)执行的状态或者某一个任务执行的状态及返回值。

    通过submit返回的是一个future对象,它是一个未来可期的对象,通过它可以获悉线程的状态



    import os,time

    # 获取CPU核心数
    print(os.cpu_count())

    # 1.创建池子 可以指定池子里有多少线程 如果不指定默认为CPU个数 * 5
    # 不会立即开启线程 会等到有任务提交后在开启线程

    from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
    # 1.创建池子 可以指定池子里有多少线程 如果不指定默认为CPU个数 * 5
    # 不会立即开启线程 会等到有任务提交后在开启线程

    pool = ThreadPoolExecutor(10)
    # 线程池最大值,机器所能承受的最大值 当然需要考虑你的机器有几个任务要做

    from threading import enumerate,current_thread



    print(enumerate())
    def task(name,age):
    print(name)
    print(current_thread().name,'run')
    time.sleep(2)

    # 该函数提交任务到线程池中
    pool.submit(task,'jerry',10)
    #任务的参数 直接写到后面不需要定义参数名称 因为是可变位置参数
    pool.submit(task,'qw',20)
    pool.submit(task)
    time.sleep(2)

    print(enumerate())

    """
    线程池,不仅帮我们管理了线程的开启和销毁,还帮我们管理任务的分配
    特点: 线程池中的线程只要开启之后 即使任务结束也不会立即结束 因为后续可能会有新任务
    避免了频繁开启和销毁线程造成的资源浪费
    1.创建一个线程池
    2.使用submit提交任务到池子中 ,线程池会自己为任务分配线程


    """

    # 进程池的使用 同样可以设置最大进程数量,默认为CPU的个数

    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中的应用

    首先要明确,TCP是IO密集型,应该使用线程池

    线程池的shutdown

    from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
    from threading import current_thread,enumerate

    import time
    pool = ThreadPoolExecutor(3)
    def task():
    print(current_thread().name)
    print(current_thread().isDaemon())
    time.sleep(1)

    for i in range(5):
    pool.submit(task)

    st=time.time()

    pool.shutdown()
    # 等待所有任务全部完毕 销毁所有线程 后关闭线程池
    print(time.time()-st)

    print('over')

    同步异步-阻塞非阻塞

    同步异步-阻塞非阻塞,经常会被程序员提及,并且概念非常容易混淆!

    阻塞非阻塞指的是程序的运行状态

    阻塞:当程序执行过程中遇到了IO操作,在执行IO操作时,程序无法继续执行其他代码,称为阻塞!

    非阻塞:程序在正常运行没有遇到IO操作,或者通过某种方式使程序即时遇到了也不会停在原地,还可以执行其他操作,以提高CPU的占用率

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

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

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

    同步会有等待的效果但是这和阻塞是完全不同的,阻塞时程序会被剥夺CPU执行权,而同步调用则不会!

    程序中的异步调用并获取结果方式1:

    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:

    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函数,而它们都是是阻塞的,会等到任务执行完毕后才能继续执行,这样一来在这个等待过程中就无法执行其他任务,降低了效率,所以需要一种方案,即保证解析结果的线程不用等待,又能保证数据能够及时被解析,该方案就是异步回调

    异步回调的使用

    先来看一个案例:

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

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

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

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

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

    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.解析任务变成了串行,

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

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

    注意:

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

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

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





     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

  • 相关阅读:
    JS中原型对象中的constructor的作用?
    ES Module,commonjs和Typescript模块系统
    webpack中的hash、chunkhash和contenthash
    react-spring介绍(翻译)
    Typescript中的对象多可能类型推导的解决办法
    博客定制样式和脚本代码
    React和Vue对比
    CSS动画属性/重绘重排组合层/GPU加速 渲染优化相关及联系
    Object.create()探索
    await的错误处理问题,一个issue引发的ts社区的讨论
  • 原文地址:https://www.cnblogs.com/komorebi/p/10982142.html
Copyright © 2020-2023  润新知