GIL pada dasarnya adalah kunci. Pelajar yang telah mempelajari sistem pengendalian tahu bahawa kunci diperkenalkan untuk mengelakkan ketidakkonsistenan data yang disebabkan oleh akses serentak. Terdapat banyak pembolehubah global yang ditakrifkan fungsi luar dalam CPython, seperti usale_arenas dan usedpools dalam pengurusan memori Jika berbilang benang digunakan untuk memori pada masa yang sama, pembolehubah ini boleh diubah suai pada masa yang sama, menyebabkan kekeliruan data. Selain itu, mekanisme pengumpulan sampah Python adalah berdasarkan pengiraan rujukan Semua objek mempunyai medan ob_refcnt yang menunjukkan bilangan pembolehubah yang merujuk kepada objek semasa Operasi seperti penetapan pembolehubah dan lulus parameter akan meningkatkan kiraan rujukan fungsi akan mengurangkan jumlah rujukan. Begitu juga, jika berbilang benang mengubah suai kiraan rujukan objek yang sama pada masa yang sama, ob_refcnt mungkin berbeza daripada nilai sebenar, yang boleh menyebabkan kebocoran memori Objek yang tidak akan digunakan tidak akan dikitar semula dan banyak lagi serius, mereka mungkin dikitar semula Objek yang dirujuk menyebabkan penterjemah Python ranap.
Takrifan GIL dalam CPython adalah seperti berikut
struct _gil_runtime_state { unsigned long interval; // 请求 GIL 的线程在 interval 毫秒后还没成功,就会向持有 GIL 的线程发出释放信号 _Py_atomic_address last_holder; // GIL 上一次的持有线程,强制切换线程时会用到 _Py_atomic_int locked; // GIL 是否被某个线程持有 unsigned long switch_number; // GIL 的持有线程切换了多少次 // 条件变量和互斥锁,一般都是成对出现 PyCOND_T cond; PyMUTEX_T mutex; // 条件变量,用于强制切换线程 PyCOND_T switch_cond; PyMUTEX_T switch_mutex; };
Perkara yang paling penting ialah medan terkunci yang dilindungi oleh mutex, yang menunjukkan sama ada GIL pada masa ini diadakan. Medan lain adalah untuk Digunakan untuk mengoptimumkan GIL. Apabila benang digunakan untuk GIL, ia memanggil kaedah take_gil() dan apabila ia mengeluarkan GIL, ia memanggil kaedah drop_gil(). Untuk mengelakkan kelaparan, apabila benang menunggu milisaat selang (lalai ialah 5 milisaat) dan belum memohon untuk GIL, ia akan menghantar isyarat secara aktif kepada benang yang memegang GIL, dan pemegang GIL akan menyemak isyarat pada masa yang sesuai . , jika didapati bahawa benang lain memohon, GIL akan dilepaskan secara paksa. Masa yang sesuai yang dinyatakan di sini adalah berbeza dalam versi yang berbeza Pada hari-hari awal, ia telah disemak setiap 100 arahan Dalam Python 3.10.4, ia telah diperiksa pada penghujung pernyataan bersyarat, penghujung setiap badan gelung pernyataan gelung. , dan penghujung panggilan fungsi Ia akan diperiksa apabila tiba masanya.
Fungsi take_gil() yang digunakan untuk GIL dipermudahkan seperti berikut
static void take_gil(PyThreadState *tstate) { ... // 申请互斥锁 MUTEX_LOCK(gil->mutex); // 如果 GIL 空闲就直接获取 if (!_Py_atomic_load_relaxed(&gil->locked)) { goto _ready; } // 尝试等待 while (_Py_atomic_load_relaxed(&gil->locked)) { unsigned long saved_switchnum = gil->switch_number; unsigned long interval = (gil->interval >= 1 ? gil->interval : 1); int timed_out = 0; COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out); if (timed_out && _Py_atomic_load_relaxed(&gil->locked) && gil->switch_number == saved_switchnum) { SET_GIL_DROP_REQUEST(interp); } } _ready: MUTEX_LOCK(gil->switch_mutex); _Py_atomic_store_relaxed(&gil->locked, 1); _Py_ANNOTATE_RWLOCK_ACQUIRED(&gil->locked, /*is_write=*/1); if (tstate != (PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) { _Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate); ++gil->switch_number; } // 唤醒强制切换的线程主动等待的条件变量 COND_SIGNAL(gil->switch_cond); MUTEX_UNLOCK(gil->switch_mutex); if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request)) { RESET_GIL_DROP_REQUEST(interp); } else { COMPUTE_EVAL_BREAKER(interp, ceval, ceval2); } ... // 释放互斥锁 MUTEX_UNLOCK(gil->mutex); }
Untuk memastikan atomicity, seluruh badan fungsi perlu memohon dan melepaskan kunci mutex gil->mutex pada awal dan akhir masing-masing. Jika GIL semasa melahu, dapatkan GIL secara langsung Jika ia tidak melahu, tunggu pembolehubah keadaan gil->kon selang milisaat (tidak kurang daripada 1 milisaat Jika masa tamat dan tiada penukaran GIL berlaku dalam tempoh tersebut). , tetapkan gil_drop_request untuk meminta penukaran paksa GIL memegang benang, jika tidak, ia terus menunggu. Setelah GIL berjaya diperoleh, nilai gil->locked, gil->last_holder dan gil->switch_number perlu dikemas kini, pembolehubah keadaan gil->switch_cond mesti dibangkitkan, dan kunci mutex gil->mutex mesti dilepaskan.
Fungsi drop_gil() yang melepaskan GIL dipermudahkan seperti berikut
static void drop_gil(struct _ceval_runtime_state *ceval, struct _ceval_state *ceval2, PyThreadState *tstate) { ... if (tstate != NULL) { _Py_atomic_store_relaxed(&gil->last_holder, (uintptr_t)tstate); } MUTEX_LOCK(gil->mutex); _Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1); // 释放 GIL _Py_atomic_store_relaxed(&gil->locked, 0); // 唤醒正在等待 GIL 的线程 COND_SIGNAL(gil->cond); MUTEX_UNLOCK(gil->mutex); if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request) && tstate != NULL) { MUTEX_LOCK(gil->switch_mutex); // 强制等待一次线程切换才被唤醒,避免饥饿 if (((PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) == tstate) { assert(is_tstate_valid(tstate)); RESET_GIL_DROP_REQUEST(tstate->interp); COND_WAIT(gil->switch_cond, gil->switch_mutex); } MUTEX_UNLOCK(gil->switch_mutex); } }
Mula-mula melepaskan GIL di bawah perlindungan gil->mutex, dan kemudian membangunkan benang lain yang sedang menunggu untuk GIL. Dalam persekitaran berbilang CPU, utas semasa mempunyai kebarangkalian yang lebih tinggi untuk memperoleh semula GIL selepas melepaskan GIL Untuk mengelakkan kebuluran benang lain, utas semasa perlu dipaksa menunggu pembolehubah keadaan gil->switch_cond. Ia hanya boleh mendapatkan GIL apabila benang lain Hanya kemudian benang semasa akan dibangkitkan.
Kod tertakluk kepada kekangan GIL tidak boleh dilaksanakan secara selari, yang mengurangkan prestasi keseluruhan untuk meminimumkan kehilangan prestasi, Python tidak melaksanakan operasi IO atau tidak Apabila pengiraan CPU intensif yang melibatkan akses objek berlaku, GIL akan dikeluarkan secara aktif, mengurangkan butiran GIL, seperti
membaca dan menulis fail
Akses rangkaian
Data yang disulitkan/data termampat
Secara tegasnya, dalam kes satu proses , berbilang utas Python mungkin berjalan serentak Perlaksanaan, contohnya, satu utas berjalan seperti biasa dan satu lagi utas sedang memampatkan data.
GIL ialah kunci yang dijana untuk mengekalkan konsistensi pembolehubah dalaman penterjemah Python. Konsistensi data pengguna tidak bertanggungjawab untuk GIL. Walaupun GIL juga memastikan ketekalan data pengguna pada tahap tertentu Contohnya, arahan yang tidak melibatkan lompatan dan panggilan fungsi dalam Python 3.10.4 akan dilaksanakan secara atom di bawah kekangan GIL, tetapi ketekalan data dalam logik perniagaan. pengguna perlu menguncinya sendiri untuk memastikannya.
Kod berikut menggunakan dua utas untuk mensimulasikan koleksi serpihan pengguna untuk memenangi ganjaran
from threading import Thread def main(): stat = {"piece_count": 0, "reward_count": 0} t1 = Thread(target=process_piece, args=(stat,)) t2 = Thread(target=process_piece, args=(stat,)) t1.start() t2.start() t1.join() t2.join() print(stat) def process_piece(stat): for i in range(10000000): if stat["piece_count"] % 10 == 0: reward = True else: reward = False if reward: stat["reward_count"] += 1 stat["piece_count"] += 1 if __name__ == "__main__": main()
Dengan mengandaikan bahawa pengguna boleh mendapat ganjaran setiap kali dia mengumpul 10 serpihan, dan setiap urutan telah mengumpul 10,000,000 serpihan, sepatutnya 9999999 ganjaran diperolehi (kali terakhir tidak dikira), sejumlah 20000000 serpihan harus dikumpul, dan 1999998 ganjaran diperoleh, tetapi keputusan larian pertama pada komputer saya adalah seperti berikut
{'piece_count': 20000000, 'reward_count': 1999987}
Jumlah serpihan adalah seperti yang dijangkakan, tetapi bilangan ganjaran Tetapi terdapat 12 yang hilang. Bilangan kepingan adalah betul kerana dalam Python 3.10.4, stat["piece_count"] += 1 dilakukan secara atom di bawah kekangan GIL. Memandangkan benang pelaksanaan boleh ditukar pada penghujung setiap gelung, ada kemungkinan bahawa benang t1 akan meningkatkan piece_count kepada 100 pada penghujung gelung tertentu, tetapi sebelum gelung seterusnya mula menilai modulo 10, penterjemah Python bertukar kepada benang t2 untuk pelaksanaan, dan t2 akan meningkatkan piece_count Jika anda mencapai 101, anda akan terlepas ganjaran.
Lampiran: Bagaimana untuk mengelak daripada terjejas oleh GIL
Setelah berkata begitu banyak, jika saya tidak menyebut penyelesaiannya, ia hanyalah siaran sains yang popular, tetapi ia tidak berguna. GIL sangat teruk, adakah cara untuk mengatasinya? Mari kita lihat penyelesaian yang tersedia.
Gunakan berbilang proses untuk menggantikan Benang
Kemunculan pustaka berbilang proses adalah sebahagian besarnya untuk mengimbangi ketidakcekapan pustaka benang akibat GIL. Ia mereplikasi sepenuhnya satu set antara muka yang disediakan oleh benang untuk memudahkan penghijrahan. Satu-satunya perbezaan ialah ia menggunakan berbilang proses dan bukannya berbilang benang. Setiap proses mempunyai GIL bebasnya sendiri, jadi tidak akan ada perbalahan GIL antara proses.
Sudah tentu pelbagai proses bukanlah ubat penawar. Pengenalannya akan meningkatkan kesukaran komunikasi data dan penyegerakan antara benang masa dalam program. Ambil pembilang sebagai contoh Jika kita mahu berbilang utas mengumpul pembolehubah yang sama, untuk utas, isytiharkan pembolehubah global dan bungkus tiga baris dengan utas. Kunci konteks. Dalam berbilang proses, kerana proses tidak dapat melihat data satu sama lain, mereka hanya boleh mengisytiharkan Baris dalam urutan utama, meletakkan dan kemudian mendapatkan, atau menggunakan memori yang dikongsi. Kos pelaksanaan tambahan ini menjadikan pengekodan program berbilang benang, yang sudah sangat menyakitkan, malah lebih menyakitkan. Di manakah kesukaran tertentu? Pembaca yang berminat boleh membaca artikel ini dengan lebih lanjut
Gunakan penghurai lain
Seperti yang dinyatakan sebelum ini, memandangkan GIL hanyalah produk CPython, adakah penghurai lain lebih baik? Ya, penghurai seperti JPython dan IronPython tidak memerlukan bantuan GIL kerana sifat bahasa pelaksanaannya. Walau bagaimanapun, dengan menggunakan Java/C# untuk pelaksanaan parser, mereka juga kehilangan peluang untuk memanfaatkan banyak ciri berguna modul bahasa C komuniti. Jadi penghurai ini sentiasa bersifat niche. Lagipun, semua orang akan memilih yang pertama daripada fungsi dan prestasi pada peringkat awal Selesai adalah lebih baik daripada sempurna.
Jadi tiada harapan?
Sudah tentu, komuniti Python juga bekerja keras untuk menambah baik GIL secara berterusan, malah cuba mengalih keluar GIL. Dan terdapat banyak penambahbaikan dalam setiap versi kecil. Pembaca yang berminat boleh membaca Slaid ini dengan lebih lanjut
Satu lagi penambahbaikan Mengolah semula GIL
&ndash ; utas yang baru-baru ini melepaskan kunci GIL daripada dijadualkan semula serta-merta
&ndash fungsi keutamaan utas (benang keutamaan tinggi boleh memaksa utas lain melepaskan kunci GIL yang dipegangnya)
Atas ialah kandungan terperinci Apakah GIL dalam Python. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!