Python 最近受到了很多关注。计划于今年 10 月发布的 3.13 版本将开始删除 GIL 的艰巨工作。对于想要尝试(几乎)无 GIL Python 的好奇用户来说,预发行版已经发布。
所有这些炒作让我用我自己的语言 ArkScript 进行挖掘,因为我过去也有一个全局 VM 锁(在 2020 年的 3.0.12 版本中添加,在 2022 年的 3.1.3 中删除),以比较事物并迫使我更深入地研究 Python GIL 的方式和原因。
全局解释器锁(GIL)是计算机语言解释器中使用的一种机制,用于同步线程的执行,以便只有一个本机线程(每个进程)可以在同一时间执行基本操作(例如内存分配和引用计数)。时间。
维基百科 — 全局解释器锁
并发是指两个或多个任务可以在重叠的时间段内启动、运行和完成,但这并不意味着它们将同时运行。
并行性 是指任务同时运行,例如在多核处理器上。
如需深入解释,请查看 Stack Overflow 的答案。
GIL 可以提高单线程程序的速度,因为您不必获取和释放所有数据结构上的锁:整个解释器都被锁定,因此默认情况下您是安全的。
然而,由于每个解释器有一个 GIL,这限制了并行性:您需要在单独的进程中生成一个全新的解释器(使用多处理模块而不是线程)才能使用多个核心!这比仅仅生成一个新线程的成本更高,因为您现在必须担心进程间通信,这会增加不可忽略的开销(有关基准测试,请参阅 GeekPython - Python 3.13 中的 GIL 成为可选)。
就 Python 而言,这取决于主要实现 CPython 没有线程安全的内存管理。如果没有 GIL,以下情况将产生竞争条件:
如果线程 1 首先运行,则计数将为 11(计数 * 2 = 10,则计数 1 = 11)。
如果 线程 2 首先运行,则计数将为 12(计数 1 = 6,则计数 * 2 = 12)。
执行顺序很重要,但更糟糕的情况可能会发生:如果两个线程同时读取 count,一个线程将擦除另一个线程的结果,并且 count 将是 10 或 6!
总体而言,在一般情况下,拥有 GIL 可以使 (CPython) 实现更轻松、更快:
它还使包装 C 库变得更容易,因为通过 GIL 保证了线程安全。
缺点是您的代码是异步,如并发,但不是并行。
[!笔记]
Python 3.13 正在删除 GIL!PEP 703 添加了构建配置 --disable-gil,以便在安装 Python 3.13 后,您可以从多线程程序的性能改进中受益。
在Python中,函数必须采用颜色:它们要么是“正常”,要么是“异步”。这在实践中意味着什么?
>>> def foo(call_me): ... print(call_me()) ... >>> async def a_bar(): ... return 5 ... >>> def bar(): ... return 6 ... >>> foo(a_bar):2: RuntimeWarning: coroutine 'a_bar' was never awaited RuntimeWarning: Enable tracemalloc to get the object allocation traceback >>> foo(bar) 6
因为异步函数不会立即返回值,而是调用协程,所以我们不能在任何地方都将它们用作回调,除非我们调用的函数被设计为采用异步回调。
我们得到了函数的层次结构,因为“普通”函数需要异步才能使用await关键字,需要调用异步函数:
can call normal -----------> normal can call async - -----------> normal | .-----------> async
除了信任调用者之外,没有办法知道回调是否是异步的(除非您尝试首先在 try/ except 块中调用它来检查异常,但这很丑陋)。
一开始,ArkScript 使用全局 VM 锁(类似于 Python 的 GIL),因为 http.arkm 模块(用于创建 HTTP 服务器)是多线程的,它通过修改变量来改变其状态,从而导致 ArkScript 的 VM 出现问题并在多个线程上调用函数。
然后在 2021 年,我开始研究一种新模型来处理虚拟机状态,以便我们可以轻松并行化它,并写了一篇关于它的文章。后来在 2021 年底实现,全局虚拟机锁被移除。
ArkScript 不会为异步函数分配颜色,因为它们在语言中不存在:你要么有一个函数,要么有一个闭包,两者都可以相互调用而无需任何额外的语法(闭包是一个穷人对象,在这种语言中:保持可变状态的函数)。
任何函数都可以在调用站点上异步(而不是声明):
(let foo (fun (a b c) ( a b c))) (print (foo 1 2 3)) # 6 (let future (async foo 1 2 3)) (print future) # UserType (print (await future)) # 6 (print (await future)) # nil
使用 async 内置函数,我们在后台生成一个 std::future (利用 std::async 和线程)来运行给定一组参数的函数。然后我们可以调用await(另一个内置函数)并在需要时获取结果,这将阻塞当前VM线程直到函数返回。
因此,可以从任何函数和任何线程等待。
所有这一切都是可能的,因为我们有一个虚拟机,它在 Ark::internal::ExecutionContext 内包含的状态上运行,该状态与单个线程绑定。 VM 在线程之间共享,而不是在上下文之间共享!
.---> thread 0, context 0 | ^ VM thread 1, context 1
当使用异步创建future时,我们是:
这禁止线程之间任何类型的同步,因为 ArkScript 不会公开引用或任何可以共享的锁(这样做是为了简单起见,因为该语言的目标是有点简约但仍然可用)。
然而,这种方法并不比 Python 更好(也不更差),因为我们每次调用都会创建一个新线程,并且每个 CPU 的线程数量是有限的,这有点昂贵。幸运的是,我不认为这是需要解决的问题,因为永远不应该同时创建数百或数千个线程,也不应同时调用数百或数千个异步 Python 函数:两者都会导致程序速度大幅减慢。
在第一种情况下,这会减慢您的进程(甚至是计算机),因为操作系统正在努力为每个线程提供时间;在第二种情况下,Python 的调度程序必须在所有协程之间进行处理。
[!笔记]
开箱即用,ArkScript 不提供线程同步机制,但即使我们将 UserType (它是 type-erased C 对象之上的包装器)传递给函数,底层对象也不是已复制。
通过一些仔细的编码,可以使用 UserType 构造创建一个锁,这将允许线程之间的同步。(let lock (module:createLock)) (let foo (fun (lock i) { (lock true) (print (str:format "hello {}" i)) (lock false) })) (async foo lock 1) (async foo lock 2)
ArkScript 和 Python 使用两种截然不同的 async/await:第一种需要在调用站点使用 async 并生成一个具有自己上下文的新线程,而后者则要求程序员将函数标记为 async能够使用await,并且这些异步函数是协程,与解释器在同一线程中运行。
源自 lexp.lt
免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。
Copyright© 2022 湘ICP备2022001581号-3