Python的协程与GIL

1 协程是什么?

我们知道多线程 / 多进程模型,是解决并发问题的经典模型之一。但是随刻客户端数量达到一定量级,进程上下文切换占用了大量的资源,线程也顶不住如此巨大的压力,对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。

协程 ,又称为微线程,它是实现多任务的另一种方式,只不过是比线程更小的执行单元。因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程。协程,只使用一个线程,在一个线程中规定某个代码块执行顺序。线程是抢占式的调度,而协程是协同式的调度,也就是说,协程需要自己做调度。

通俗的理解: 在一个线程中的某个函数中,我们可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的 ,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定。

协程与线程的差异:在实现多任务时, 线程切换从系统层面看远不止保存和恢复CPU上下文这么简单。操作系统为了程序运行的高效性,每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作,所以线程的切换非常耗性能。但是协程的切换只是单纯地操作CPU的上下文,性能就高多了。

2 协程有什么用?

在别的语言中协程意义不大,多线程可解决问题,但是python因为他有GIL(Global Interpreter Lock 全局解释器锁 )在同一时间只有一个线程在工作,如果一个线程里面I/O操作特别多,协程就比较适用;

在python中多线程的执行情况如下图:

 

3 python中的GIL

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。Python的一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。

那么CPython实现中的GIL又是什么呢?GIL全称Global Interpreter Lock,官方给出的解释:

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

GIL是一个防止多线程并发执行机器码的一个Mutex,就是一个全局锁。GIL的存在会对多线程的效率有不小影响。

4 例子

python3可以使用asyncio 和 async / await 的方法实现协程

例子1

 

import asyncio
import time


async def sub_function(str):
    print(' {}'.format(str))
    sleep_time = int(str.split('_')[1])
    await asyncio.sleep(sleep_time)
    print('OK {}'.format(str))


async def main(strs):
    for str in strs:
        await sub_function(str)

# asyncio.run(main()) 作为主程序的入口函数,在程序运行周期内,只调用一次 asyncio.run
t0 = time.time()
asyncio.run(main(['str_1', 'str_2', 'str_3', 'str_4']))
#如果python版本低于3.7,使用下面的代码
#asyncio.get_event_loop().run_until_complete(main(['str_1', 'str_2', 'str_3', 'str_4']))
t1 = time.time()
print("Total time running: %s seconds" %(str(t1 - t0)))

 执行结果:

$ python3 ./coroutine.py 
 str_1
OK str_1
 str_2
OK str_2
 str_3
OK str_3
 str_4
OK str_4
Total time running: 10.013939380645752 seconds

一共是 10s 和我们顺序分析 分别等待 1 2 3 4 秒 好像没有什么提升作用,主要是因为使用了await 来调用实现,但是await 执行的效果,和 Python 正常执行是一样的,也就是说程序会阻塞在这里,进入被调用的协程函数,执行完毕返回后再继续,await 是同步调用,相当于我们用异步接口写了个同步代码,所以运行时间没有得到提升。

例子2

import time
import asyncio

async def sub_function(str):
    print(' {}'.format(str))
    sleep_time = int(str.split('_')[1])
    await asyncio.sleep(sleep_time)
    print('OK {}'.format(str))


async def main(strs):
    #tasks = [asyncio.create_task(sub_function(str)) for str in strs]
    tasks = [asyncio.get_event_loop().create_task(sub_function(str)) for str in strs]
    for task in tasks:
        await task
    ‘’‘
    # *tasks 解包列表,将列表变成了函数的参数;与之对应的是, ** dict 将字典变成了函数的参数。
    await asyncio.gather(*tasks)
    ’‘’


# asyncio.run(main()) 作为主程序的入口函数,在程序运行周期内,只调用一次 asyncio.run
t0 = time.time()
#asyncio.run(main(['str_1', 'str_2', 'str_3', 'str_4']))
asyncio.get_event_loop().run_until_complete(main(['str_1', 'str_2', 'str_3', 'str_4']))
t1 = time.time()
print("Total time running: %s seconds" %(str(t1 - t0)))

执行结果:

$ python3 ./coroutine2.py 
 str_1
 str_2
 str_3
 str_4
OK str_1
OK str_2
OK str_3
OK str_4
Total time running: 4.002293109893799 seconds

使用create_task任务创建后很快就会被调度执行,这样,我们的代码也不会阻塞在任务这里。结果显示,运行总时长等于运行时间最长的一句。

例子3

处理exception 或者协程超时的处理

import time
import asyncio

async def worker_1():
    await asyncio.sleep(1)
    return 1

async def worker_2():
    await asyncio.sleep(2)
    return 2 / 0

async def worker_3():
    await asyncio.sleep(3)
    return 3

async def main():
    '''
    #python3.7 use this code
    task_1 = asyncio.create_task(worker_1())
    task_2 = asyncio.create_task(worker_2())
    task_3 = asyncio.create_task(worker_3())
    '''
    task_1 = asyncio.get_event_loop().create_task(worker_1())
    task_2 = asyncio.get_event_loop().create_task(worker_2())
    task_3 = asyncio.get_event_loop().create_task(worker_3())

    await asyncio.sleep(2)
    task_3.cancel()

    res = await asyncio.gather(task_1, task_2, task_3, return_exceptions=True)
    print(res)
# asyncio.run(main()) 作为主程序的入口函数,在程序运行周期内,只调用一次 asyncio.run
t0 = time.time()
#asyncio.run(main())
asyncio.get_event_loop().run_until_complete(main())
t1 = time.time()
print("Total time running: %s seconds" %(str(t1 - t0)))

 执行结果:

$ python3 ./coroutine3.py 
[1, ZeroDivisionError('division by zero',), CancelledError()]
Total time running: 2.0031282901763916 seconds

return_exceptions=True 用于内部处理exception。这个参数默认值为False, 如果不设置这个参数,错误就会完整地 throw 到执行层,从而需要 try except 来捕捉,

这也就意味着其他还没被执行的任务会被全部取消掉。为了避免这个情况,我们将 return_exceptions 设置为 True 即可。

5 Trouble shooting

1 使用asyncio.run()会报错:

AttributeError: module 'asyncio' has no attribute 'run'

分析:

   Python 版本低于 3.7

 

     asyncio.run(main())  换成 asyncio.get_event_loop().run_until_complete(main())

 

reference:

https://www.cnblogs.com/SuKiWX/p/8804974.html

https://blog.csdn.net/weixin_41599977/article/details/93656042

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值