首页  >  文章  >  后端开发  >  比较 Python 和 ArkScript 异步模型

比较 Python 和 ArkScript 异步模型

WBOY
WBOY原创
2024-09-04 06:51:54753浏览

Comparing Python and ArkScript asynchronous models

Python 最近受到了很多关注。计划于今年 10 月发布的 3.13 版本将开始删除 GIL 的艰巨工作。对于想要尝试(几乎)无 GIL Python 的好奇用户来说,预发行版已经发布。

所有这些炒作让我用我自己的语言 ArkScript 进行挖掘,因为我过去也有一个全局 VM 锁(在 2020 年的 3.0.12 版本中添加,在 2022 年的 3.1.3 中删除),以比较事物并迫使我更深入地研究 Python GIL 的方式和原因。

定义

  1. 首先,让我们定义什么是 GIL(全局解释器锁):

全局解释器锁(GIL)是计算机语言解释器中使用的一种机制,用于同步线程的执行,以便只有一个本机线程(每个进程)可以在同一时间执行基本操作(例如内存分配和引用计数)。时间。

维基百科 — 全局解释器锁

  1. 并发是指两个或多个任务可以在重叠的时间段内启动、运行和完成,但这并不意味着它们将同时运行。

  2. 并行 是指任务同时运行,例如在多核处理器上。

有关深入的解释,请查看 Stack Overflow 的答案。

Python 的 GIL

GIL 可以提高单线程程序的速度因为您不必获取和释放所有数据结构上的锁:整个解释器都被锁定,因此默认情况下您是安全的。

但是,由于每个解释器有一个 GIL,这限制了并行性:您需要在单独的进程中生成一个全新的解释器(使用多处理模块而不是线程)才能使用多个核心!这比仅仅生成一个新线程的成本更高,因为您现在必须担心进程间通信,这会增加不可忽略的开销(有关基准测试,请参阅 GeekPython - Python 3.13 中的 GIL 成为可选)。

它如何影响Python的异步?

就 Python 而言,这取决于主要实现 CPython 没有线程安全的内存管理。如果没有 GIL,以下场景将产生竞争条件:

  1. 创建共享变量 count = 5
  2. 线程 1:计数 *= 2
  3. 线程 2:计数 += 1

如果线程 1 首先运行,计数将为 11(计数 * 2 = 10,然后计数 + 1 = 11)。

如果线程 2 首先运行,计数将为 12(计数 + 1 = 6,然后计数 * 2 = 12)。

执行顺序很重要,但更糟糕的情况可能会发生:如果两个线程同时读取 count,一个线程将擦除另一个线程的结果,并且 count 将是 10 或 6!

总体而言,在一般情况下,拥有 GIL 可以使 (CPython) 实现更轻松、更快:

  • 在单线程情况下更快(无需为每个操作获取/释放锁)
  • 在 IO 绑定程序的多线程情况下更快(因为这些发生在 GIL 之外)
  • 对于在 C 中执行计算密集型工作的 CPU 密集型程序来说,在多线程情况下速度更快(因为 GIL 在调用 C 代码之前被释放)

它还使包装 C 库变得更容易,因为 GIL 保证了线程安全。

缺点是您的代码是异步,如并发,但不是并行

[!注意]
Python 3.13 正在删除 GIL!

PEP 703 添加了构建配置 --disable-gil,以便在安装 Python 3.13+ 后,您可以从多线程程序的性能改进中受益。

Python 异步/等待模型

在 Python 中,函数必须采用颜色:它们要么是“正常”,要么是“异步”。这在实践中意味着什么?

>>> def foo(call_me):
...     print(call_me())
... 
>>> async def a_bar():
...     return 5
... 
>>> def bar():
...     return 6
... 
>>> foo(a_bar)
25c93a42aa09d6ad19dff8feb1023ce5
c2f32e7e9231c3bf5bf9f218b5147824: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 parallelism

In the beginning, ArkScript was using a Global VM Lock (akin to Python's GIL), because the http.arkm module (used to create HTTP servers) was multithreaded and it caused problems with ArkScript's VM by altering its state through modifying variables and calling functions on multiple threads.

Then in 2021, I started working on a new model to handle the VM state so that we could parallelize it easily, and wrote an article about it. It was later implemented by the end of 2021, and the Global VM Lock was removed.

ArkScript async/await

ArkScript does not assign a color to async functions, because they do not exist in the language: you either have a function or a closure, and both can call each other without any additional syntax (a closure is a poor man object, in this language: a function holding a mutable state).

Any function can be made async at the call site (instead of declaration):

(let foo (fun (a b c)
    (+ a b c)))

(print (foo 1 2 3))  # 6

(let future (async foo 1 2 3))
(print future)          # UserType8e288519637d83f9ffb95e89ea0f8cfb
(print (await future))  # 6
(print (await future))  # nil

Using the async builtin, we are spawning a std::future under the hood (leveraging std::async and threads) to run our function given a set of arguments. Then we can call await (another builtin) and get a result whenever we want, which will block the current VM thread until the function returns.

Thus, it is possible to await from any function, and from any thread.

The specificities

All of this is possible because we have a single VM that operates on a state contained inside an Ark::internal::ExecutionContext, which is tied to a single thread. The VM is shared between the threads, not the contexts!

        .---> thread 0, context 0
        |            ^
VM 1d166129c620ba7bd4551066df2b2cf5 thread 1, context 1              

When creating a future by using async, we are:

  1. copying all the arguments to the new context,
  2. creating a brand new stack and scopes,
  3. finally create a separate thread.

This forbids any sort of synchronization between threads since ArkScript does not expose references or any kind of lock that could be shared (this was done for simplicity reasons, as the language aims to be somewhat minimalist but still usable).

However this approach isn't better (nor worse) than Python's, as we create a new thread per call, and the number of threads per CPU is limited, which is a bit costly. Luckily I don't see that as problem to tackle, as one should never create hundreds or thousands of threads simultaneously nor call hundreds or thousands of async Python functions simultaneously: both would result in a huge slow down of your program.

In the first case, this would slowdown your process (even computer) as the OS is juggling to give time to every thread ; in the second case it is Python's scheduler that would have to juggle between all of your coroutines.

[!NOTE]
Out of the box, ArkScript does not provide mechanisms for thread synchronization, but even if we pass a UserType (which is a wrapper on top of type-erased C++ objects) to a function, the underlying object isn't copied.

With some careful coding, one could create a lock using the UserType construct, that would allow synchronization between threads.

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

Conclusion

ArkScript and Python use two very different kinds of async / await: the first one requires the use of async at the call site and spawns a new thread with its own context, while the latter requires the programmer to mark functions as async to be able to use await, and those async functions are coroutines, running in the same thread as the interpreter.

Sources

  1. Stack Exchange — Why was Python written with the GIL?
  2. Python Wiki — GlobalInterpreterLock
  3. stuffwithstuff - What color is your function?

Originally from lexp.lt

以上是比较 Python 和 ArkScript 异步模型的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn