>  기사  >  백엔드 개발  >  Python과 ArkScript 비동기 모델 비교

Python과 ArkScript 비동기 모델 비교

WBOY
WBOY원래의
2024-09-04 06:51:54659검색

Comparing Python and ArkScript asynchronous models

요즘 Python이 많은 주목을 받고 있습니다. 올해 10월로 예정된 3.13 릴리스에서는 GIL을 제거하는 대규모 작업이 시작됩니다. (거의) GIL이 없는 Python을 사용해 보고 싶어하는 호기심 많은 사용자를 위해 사전 릴리스가 이미 출시되었습니다.

과거에 전역 VM 잠금(버전 3.0.12에 추가됨, 2020년에 추가됨, 2022년에 3.1.3에서 제거됨)이 있었기 때문에 이 모든 과대 광고로 인해 내 언어인 ArkScript를 파헤치게 되었습니다. 사물을 비교하고 Python GIL의 방법과 이유를 더 깊이 파고들도록 강요합니다.

정의

  1. 시작하려면 GIL(전역 인터프리터 잠금)이 무엇인지 정의해 보겠습니다.

GIL(전역 인터프리터 잠금)은 컴퓨터 언어 인터프리터에서 스레드 실행을 동기화하여 프로세스당 하나의 네이티브 스레드만 기본 작업(예: 메모리 할당 및 참조 계산)을 실행할 수 있도록 하는 메커니즘입니다. 시간.

Wikipedia — 전역 통역사 잠금

  1. 동시성은 두 개 이상의 작업이 겹치는 기간에 시작, 실행 및 완료될 수 있는 경우를 의미하지만, 그렇다고 두 작업이 동시에 실행된다는 의미는 아닙니다.

  2. 병렬성은 작업이 말 그대로 동시에 실행되는 것입니다(예: 멀티코어 프로세서에서).

자세한 설명을 보려면 스택 오버플로 답변을 확인하세요.

파이썬의 GIL

GIL은 모든 데이터 구조에 대한 잠금을 획득하고 해제할 필요가 없기 때문에 단일 스레드 프로그램의 속도를 높일 수 있습니다. 전체 인터프리터가 잠겨 있으므로 기본적으로 안전합니다.

그러나 인터프리터당 하나의 GIL이 있으므로 병렬 처리가 제한됩니다. 두 개 이상의 코어를 사용하려면 별도의 프로세스(스레딩 대신 멀티프로세싱 모듈 사용)에서 완전히 새로운 인터프리터를 생성해야 합니다! 이제 무시할 수 없는 오버헤드를 추가하는 프로세스 간 통신에 대해 걱정해야 하기 때문에 새 스레드를 생성하는 것보다 비용이 더 많이 듭니다(벤치마크는 GeekPython — Python 3.13에서 GIL이 옵션이 됨 참조).

Python의 비동기에 어떤 영향을 미치나요?

Python의 경우 스레드로부터 안전한 메모리 관리 기능이 없는 주요 구현인 CPython에 달려 있습니다. GIL이 없으면 다음 시나리오에서 경쟁 조건이 발생합니다.

  1. 공유 변수 생성 개수 = 5
  2. 스레드 1: 개수 *= 2
  3. 스레드 2: 개수 += 1

스레드 1이 먼저 실행되면 개수는 11이 됩니다(개수 * 2 = 10, 그 다음 개수 + 1 = 11).

스레드 2가 먼저 실행되면 개수는 12가 됩니다(개수 + 1 = 6, 그 다음 개수 * 2 = 12).

실행 순서는 중요하지만 더 나쁜 경우도 발생할 수 있습니다. 두 스레드가 동시에 카운트를 읽는 경우 하나는 다른 스레드의 결과를 지우고 카운트는 10 또는 6이 됩니다!

전반적으로 GIL을 사용하면 일반적인 경우에 (CPython) 구현이 더 쉽고 빨라집니다.

  • 단일 스레드의 경우 더 빠릅니다(모든 작업에 대해 잠금을 획득/해제할 필요가 없음)
  • IO 바인딩 프로그램의 멀티 스레드 경우 더 빠릅니다(GIL 외부에서 발생하기 때문)
  • C에서 계산 집약적인 작업을 수행하는 CPU 바인딩 프로그램의 멀티 스레드 경우 더 빠릅니다(GIL은 C 코드를 호출하기 전에 릴리스되기 때문입니다)

또한 GIL 덕분에 스레드 안전성이 보장되므로 C 라이브러리 래핑이 더 쉬워집니다.

단점은 코드가 동시처럼 비동기이지만 병렬이 아니라는 점입니다.

[!참고]
Python 3.13에서 GIL이 제거됩니다!

PEP 703에는 Python 3.13+ 설치 시 멀티스레드 프로그램의 성능 향상 혜택을 누릴 수 있도록 --disable-gil 빌드 구성이 추가되었습니다.

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

비동기 함수는 값을 즉시 반환하지 않고 코루틴을 호출하기 때문에 호출하는 함수가 비동기 콜백을 받도록 설계되지 않는 한 코루틴을 어디에서나 콜백으로 사용할 수 없습니다.

비동기 함수를 호출하려면 "일반" 함수를 비동기로 만들어야 wait 키워드를 사용할 수 있으므로 함수의 계층 구조를 얻습니다.

         can call
normal -----------> normal

         can call
async -+-----------> normal
       |
       .-----------> async                    

호출자를 신뢰하는 것 외에는 콜백이 비동기인지 아닌지 알 수 있는 방법이 없습니다(예외를 확인하기 위해 try/제외 블록 내에서 먼저 호출하지 않는 한, 보기 흉합니다).

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으로 문의하세요.