博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Python GIL
阅读量:6270 次
发布时间:2019-06-22

本文共 4069 字,大约阅读时间需要 13 分钟。

在这篇博文中,我们将介绍Python GIL, Threads, Processes和AsyncIO

假设我们想要编写一个函数,该函数将数字作为参数并简单的倒计时,挺容易:

def count_down(n):    while n > 0:        n -= 1

让我们用一个大数字来调用这个函数并测量函数耗时:

from time import timebefore = time()count_down(100000000)after = time()print(after - before)

在我的机器上,需要5.62秒。现在,让我们调用它两次并测量耗时

from time import timebefore = time()count_down(100000000)count_down(100000000)after = time()print(after - before)

在我的机器上,需要11.58秒

它正常工作,但是我们的老板不满意,他希望执行的更快。我们如何让它更快?我们使用多线程使这两次函数调用并行运行。理论上,并行运行函数两次应该花费函数一次执行的时间,因为函数的调用是并行执行的。让我们通过在不同的线程中调用这个函数两次:

from threading import Threadfrom time import timebefore = time()threads = []for item in range(1, 3):    t = Thread(target=count_down, args=(100000000,))    t.start()    threads.append(t)[thread.join() for thread in threads]after = time()print(after - before)

我的机器大约需要11.2秒。等等,什么?它不仅没有之前的快,但实际上它甚至比不使用多线程更慢。使用Python2.7你会得到更糟糕的结果。在一个着名的软件开发人员David Beazly报告(inside the Python GIl)中提到:它甚至花费几乎两倍的时间

But, WHY?

请欢迎臭名昭著的GIL(Global Interpreter Lock). 这家伙应该为Python世界上所有的事情负责。GIL在同一时间只允许一个线程运行。在上面的例子中,当我们认为我们使用了两个线程,我们实际只是用了一个,因为GIL不允许我们这么做。这就是它无法运行得更快的原因。但是,为什么它甚至更慢?因为Python试图不停的切换线程让两个线程同时工作。GIL不允许这么做。然而,无用的切换会带来额外的开销。因此,最终结果甚至更慢。

But, WHY?

我们都知道多线程是个好东西,可以帮我们让程序运行更快。 为什么有GIL的存在呢?该死的,GIL就像行尸走肉中的Negan

image

好吧,事实证明GIL并不是一个恶棍。实际上,GIL是一个好东西。实际上Python的内存管理不是线程安全的!也就是说,如果你同时运行多个线程,你的程序会出现各种莫名其妙的现象甚至是灾难性的。GIL可以帮我们摆脱这种情况。

image

但是,删除Thread类不是更容易吗?

很好的问题!既然想到要编写一个额外的工具来防止Threads对我们的程序产生不好影响,为什么不直接删除Threads类,让程序员根本无法使用它们呢?

实际上,有些情况Thread很有用。以上的示例都是CPU密集型计算,意味着它们只需在CPU上运行并且在CPU上等待。但是,如果你的代码是IO密集型计算或者是图像处理或者是NumPy数字运算,GIL将不会妨碍,因为这些操作发生在GIL之外。在这些地方你可以安全有效的使用Thread

让我们编写一个IO密集型计算的函数,函数完成请求一个url并将结果作为文本返回。

import requestsdef get_content(url):    response = requests.get(url)    return response.text

在我的机器上,使用参数谷歌调用一次此函数需要1.1 秒,调用两次需要2.1秒。现在让我们在不同的线程中调用此函数两次,看看Thread 的效果。

urls = ['https://google.com'] * 2before = time()threads = []for url in urls:    thread1 = Thread(target=get_content, args=(url,))    thread1.start()    threads.append(thread1)[thread.join() for thread in threads]after = time()print(after - before)

只需要1.15秒,万岁!在不同的线程中调用两次相当于调用一次的时间。因为我们的函数是IO密集型计算。当我们的函数是CPU密集型计算(之前的示例中函数完成递减就是CPU密集型计算)时同样的事情就不会发生。

还有一个问题

每一个线程需要一些额外的内存,线程切换需要一些时间。虽然不是很多,但是当你运行数千个线程的时候资源的消耗就会增加。想象一下数千兆字节的额外RAM和至少5%的CPU时间仅用于上下文切换。

为了解决这个问题,Python开发人员提供了一个asyncio库。它有自己的事件循环来控制单个线程内异步方式的函数执行。如果线程被底层的操作系统控制,asyncio知道何时借助于开发人员自己编写的关键词切换任务。没错!你来决定何时切换。让我们将上面的示例转换成asyncio实现:

import asyncioimport aiohttploop = asyncio.get_event_loop()session = aiohttp.ClientSession(loop=loop)async def get_content(pid, url):    async with session.get(url) as response:        content = await response.read()        print(pid, content)loop.create_task(get_content(1, 'http://asyncio.readthedocs.io/'))loop.create_task(get_content(2, 'http://asyncio.readthedocs.io/'))loop.create_task(get_content(3, 'http://asyncio.readthedocs.io/'))loop.create_task(get_content(4, 'http://asyncio.readthedocs.io/'))loop.create_task(get_content(5, 'http://asyncio.readthedocs.io/'))loop.run_forever()

注意我们使用了aiohttp代替了requests ,因为它是一个异步的HTTP包。在不涉及细节的情况下,这段代码做所的是定义一个异步函数(aka协成)并使用不同的ID(仅仅是为了说明其异步性)调用五次,输出应该是这样的:

1 b'\n

请注意,ID不是按照他们的调用顺序来显示的,也就是说当遇到关键词await时任务被切换了。我们可以使用Thread 得到相同的结果,但是asyncio使用了较少的开销并且您控制切换过程。

虽然这个示例没有解释asyncio的细节, 但我希望它至少能解释为什么它是有用的。

好了,我们现在处理了IO密集型任务,让我们回到我们第一个CPU密集型计算的问题。

CPU密集型计算任务的解决方式

在Python中,multiprocessing.Process提供了和Thread相似的功能和接口。区别是它使用了子进程而不是线程。这就是它不会被GIL阻塞的原因。太棒了!让我们使用Process重写上面的示例。正如我所说,Process提供了和Thread形似的接口,我们只需要把Thread替换成Process即可完成这个示例:

from multiprocessing import Processfrom time import timedef count_down(n):    while n > 0:        n -= 1before = time()processes = []for item in range(1, 2):    process1 = Process(target=count_down, args=(100000000,))    process1.start()    processes.append(process1)[process.join() for process in processes]after = time()print(after - before)

我的机器花了11秒。这是我们之前调用一次函数的时间。酷!试着再添加一个进程仍然需要同样的时间。太棒了!但是,请记住,进程在一个单独的内容空间运行,因此不能彼此共享数据,而线程可以。

我们这里谈的是Cpython。还有一些其他的实现,像Jpython和IronPython没有GIL,所以可以直接使用线程。如果你不知道Jpython或者IronPython是什么,请自行GOOGLE,这里有一篇博文仅供参考:What is Python?

原文发布时间为:2018-08-01

本文作者: Tonner
本文来自云栖社区合作伙伴“”,了解相关信息可以关注“ ”

转载地址:http://ljvpa.baihongyu.com/

你可能感兴趣的文章
Yacc 与 Lex 快速入门
查看>>
Unity中HDR外发光的使用
查看>>
Flume负载均衡配置
查看>>
Ajax详解
查看>>
Ubuntu C/C++开发环境的安装和配置
查看>>
百世汇通快递地区选择插件,单独剥离
查看>>
Linux系统调用---同步IO: sync、fsync与fdatasync【转】
查看>>
【MyBatis学习06】输入映射和输出映射
查看>>
[LeetCode] Decode String 解码字符串
查看>>
数字逻辑的一些基本运算和概念
查看>>
ant重新编译打包hadoop-core-1.2.1.jar时遇到的错
查看>>
【★★★★★】提高PHP代码质量的36个技巧
查看>>
3 weekend110的配置hadoop(格式化) + 一些问题解决 + 未免密码配置
查看>>
JavaScript Creating 对象
查看>>
Java compiler level does not match the version of the installed Java project facet.(转)
查看>>
WPF MediaElement.Position属性
查看>>
sqoop数据迁移(基于Hadoop和关系数据库服务器之间传送数据)
查看>>
spring mysql多数据源配置
查看>>
[React] Override webpack config for create-react-app without ejection
查看>>
检索 COM 类工厂中 CLSID 为{00024500-0000-0000-C000-000000000046} 的组件时失败,原因是出现以下错误: 80070005。...
查看>>