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.
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
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.
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 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).
Dalam kes Python, ia terletak pada pelaksanaan utama, CPython, tidak mempunyai pengurusan memori selamat benang. Tanpa GIL, senario berikut akan menjana keadaan perlumbaan:
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:
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.
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) <coroutine object a_bar at 0x10491f480> <stdin>: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).
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 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) # UserType<0, 0x0x7f0e84d85dd0> (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.
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 <----+ can't interact | v .---> thread 1, context 1
When creating a future by using async, we are:
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)Salin selepas log masuk
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.
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!