Python GIL

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

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

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

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

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

from time import time

before = time()
count_down(100000000)
after = time()
print(after - before)

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

from time import time

before = time()
count_down(100000000)
count_down(100000000)
after = time()
print(after - before)

在我的机器上,需要11.58秒

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

from threading import Threadfrom time import time

before = 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 requests

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

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

urls = ['https://google.com'] * 2
before = 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 asyncio
import aiohttp

loop = 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<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0
3 b'\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0
4 b'\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0
5 b'\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0
2 b'\n<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0

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

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

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

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

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

from multiprocessing import Process
from time import time

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

before = 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?
http://rahmonov.me/posts/what-is-python/

原文发布时间为:2018-08-01
本文作者: Tonner
本文来自云栖社区合作伙伴“ Python爱好者社区”,了解相关信息可以关注“ Python爱好者社区

相关文章
|
分布式计算 并行计算 安全
在Python Web开发过程中:详述Python中的GIL及其对多线程的影响。
Python的GIL是CPython中的全局锁,限制了多线程并行执行,尤其在CPU密集型任务上导致性能瓶颈。虽然GIL限制了多线程的并行计算,但在I/O密集型任务中,线程可交替执行提升吞吐量。为利用多核,开发者常选择多进程或使用无GIL的解释器,如Jython、PyPy。异步IO和分布式计算也是应对策略。
232 9
|
并行计算 Python
python并发编程: Python速度慢的罪魁祸首,全局解释器锁GIL
python并发编程: Python速度慢的罪魁祸首,全局解释器锁GIL
187 1
python并发编程: Python速度慢的罪魁祸首,全局解释器锁GIL
|
安全 Java C++
解释Python中的全局解释器锁(GIL)和线程安全的概念。
解释Python中的全局解释器锁(GIL)和线程安全的概念。
142 0
|
10月前
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
693 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
分布式计算 并行计算 安全
在Python Web开发中,Python的全局解释器锁(Global Interpreter Lock,简称GIL)是一个核心概念,它直接影响了Python程序在多线程环境下的执行效率和性能表现
【6月更文挑战第30天】Python的GIL是CPython中的全局锁,限制了多线程并行执行,尤其是在多核CPU上。GIL确保同一时间仅有一个线程执行Python字节码,导致CPU密集型任务时多线程无法充分利用多核,反而可能因上下文切换降低性能。然而,I/O密集型任务仍能受益于线程交替执行。为利用多核,开发者常选择多进程、异步IO或使用不受GIL限制的Python实现。在Web开发中,理解GIL对于优化并发性能至关重要。
224 0
|
Java C语言 Python
解析Python中的全局解释器锁(GIL):影响、工作原理及解决方案
解析Python中的全局解释器锁(GIL):影响、工作原理及解决方案
194 0
|
安全 Python
Python 中的全局解释器锁(GIL)详解
【8月更文挑战第24天】
404 0
|
安全 Java Python
GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。
【6月更文挑战第20天】GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。线程池通过预创建线程池来管理资源,减少线程创建销毁开销,提高效率。示例展示了如何使用Python实现一个简单的线程池,用于执行多个耗时任务。
146 6
|
开发框架 并行计算 安全
Python的GIL限制了CPython在多核下的并行计算,但通过替代解释器(如Jython, IronPython, PyPy)和多进程、异步IO可规避
【6月更文挑战第26天】Python的GIL限制了CPython在多核下的并行计算,但通过替代解释器(如Jython, IronPython, PyPy)和多进程、异步IO可规避。Numba、Cython等工具编译优化代码,未来社区可能探索更高级的并发解决方案。尽管GIL仍存在,现有策略已能有效提升并发性能。
214 3

推荐镜像

更多