Rumah  >  Artikel  >  pembangunan bahagian belakang  >  Membandingkan model tak segerak Python dan ArkScript

Membandingkan model tak segerak Python dan ArkScript

WBOY
WBOYasal
2024-09-04 06:51:54740semak imbas

Comparing Python and ArkScript asynchronous models

Python telah mendapat perhatian ramai sejak kebelakangan ini. Keluaran 3.13, yang dirancang untuk Oktober tahun ini, akan memulakan kerja besar untuk mengalih keluar GIL. Prakeluaran sudah dikeluarkan untuk pengguna yang ingin tahu yang ingin mencuba Python (hampir) tanpa GIL.

Semua gembar-gembur ini membuatkan saya menggali dalam bahasa saya sendiri, ArkScript, kerana saya juga mempunyai Kunci VM Global, pada masa lalu (ditambah dalam versi 3.0.12, pada 2020, dialih keluar pada 3.1.3 pada 2022), untuk bandingkan sesuatu dan paksa saya untuk menggali lebih mendalam tentang bagaimana dan mengapa Python GIL.

Definisi

  1. Untuk bermula, mari kita tentukan apa itu GIL (kunci jurubahasa global) ialah:

Kunci jurubahasa global (GIL) ialah mekanisme yang digunakan dalam penterjemah bahasa komputer untuk menyegerakkan pelaksanaan utas supaya hanya satu utas asli (setiap proses) boleh melaksanakan operasi asas (seperti peruntukan memori dan pengiraan rujukan) pada masa.

Wikipedia — Kunci penterjemah global

  1. Concurrency ialah apabila dua atau lebih tugasan boleh dimulakan, dijalankan dan diselesaikan dalam tempoh masa yang bertindih, tetapi itu tidak bermakna kedua-duanya akan dijalankan serentak.

  2. Paralelisme ialah apabila tugas benar-benar dijalankan pada masa yang sama, contohnya pada pemproses berbilang teras.

Untuk penjelasan yang mendalam, semak jawapan Stack Overflow ini.

GIL Python

GIL boleh meningkatkan kelajuan program satu benang kerana anda tidak perlu memperoleh dan melepaskan kunci pada semua struktur data: keseluruhan penterjemah dikunci supaya anda selamat secara lalai.

Walau bagaimanapun, memandangkan terdapat satu GIL bagi setiap jurubahasa, yang mengehadkan keselarian: anda perlu melahirkan jurubahasa baharu dalam proses yang berasingan (menggunakan modul berbilang pemprosesan dan bukannya penjalinan) untuk menggunakan lebih daripada satu teras! Ini mempunyai kos yang lebih besar daripada hanya menghasilkan utas baharu kerana anda kini perlu bimbang tentang komunikasi antara proses, yang menambah overhed yang tidak boleh diabaikan (lihat GeekPython — GIL Menjadi Pilihan dalam Python 3.13 untuk penanda aras).

Bagaimanakah ia menjejaskan async Python?

Dalam kes Python, ia terletak pada pelaksanaan utama, CPython, tidak mempunyai pengurusan memori selamat benang. Tanpa GIL, senario berikut akan menjana keadaan perlumbaan:

  1. buat kiraan pembolehubah yang dikongsi = 5
  2. benang 1: kiraan *= 2
  3. benang 2: kiraan += 1

Jika benang 1 dijalankan dahulu, kiraan akan menjadi 11 (kira * 2 = 10, kemudian kira + 1 = 11).

Jika benang 2 dijalankan dahulu, kiraan akan menjadi 12 (kiraan + 1 = 6, kemudian kira * 2 = 12).

Urutan pelaksanaan adalah penting, tetapi lebih teruk lagi boleh berlaku: jika kedua-dua utas dibaca dikira pada masa yang sama, satu akan memadamkan keputusan yang lain dan kiraan akan sama ada 10 atau 6!

Secara keseluruhan, mempunyai GIL menjadikan pelaksanaan (CPython) lebih mudah dan pantas dalam kes umum:

  • lebih pantas dalam sarung berbenang tunggal (tidak perlu memperoleh/melepaskan kunci untuk setiap operasi)
  • lebih pantas dalam kes berbilang benang untuk program terikat IO (kerana ia berlaku di luar GIL)
  • lebih pantas dalam kes berbilang benang untuk program terikat CPU yang melakukan kerja intensif pengiraan mereka dalam C (kerana GIL dikeluarkan sebelum memanggil kod C)

Ia juga memudahkan pembungkusan perpustakaan C, kerana anda dijamin keselamatan benang berkat GIL.

Kelemahannya ialah kod anda tak segerak seperti dalam serempak, tetapi tidak selari.

[!NOTA]
Python 3.13 sedang mengalih keluar GIL!

PEP 703 menambah konfigurasi bangunan --disable-gil supaya selepas memasang Python 3.13+, anda boleh mendapat manfaat daripada peningkatan prestasi dalam program berbilang benang.

Model async/menunggu Python

Dalam Python, fungsi perlu mengambil warna: ia sama ada "normal" atau "async". Apakah maksud ini dalam amalan?

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

Oleh kerana fungsi tak segerak tidak mengembalikan nilai serta-merta, sebaliknya memanggil coroutine, kami tidak boleh menggunakannya di mana-mana sahaja sebagai panggilan balik, melainkan fungsi yang kami panggil direka untuk mengambil panggilan balik tak segerak.

Kami mendapat hierarki fungsi, kerana fungsi "biasa" perlu dibuat asinkron untuk menggunakan kata kunci tunggu, diperlukan untuk memanggil fungsi tak segerak:

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

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

Selain daripada mempercayai pemanggil, tiada cara untuk mengetahui sama ada panggilan balik tidak segerak atau tidak (melainkan anda cuba memanggilnya dahulu di dalam blok cuba/kecuali untuk menyemak pengecualian, tetapi itu buruk).

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

Atas ialah kandungan terperinci Membandingkan model tak segerak Python dan ArkScript. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn