GIL dan free-threading

Peran GIL, alasannya, serta build free-threaded pada Python 3.13 dan 3.14.

Daftar isi

GIL (Global Interpreter Lock), yaitu kunci global yang membuat hanya satu thread yang dapat mengeksekusi bytecode Python pada satu waktu, adalah salah satu keputusan desain CPython yang paling sering dibicarakan. Topik ini membahas apa yang sebenarnya dikunci GIL, alasan historis dan teknisnya, dampak GIL terhadap program CPU-bound dan I/O-bound, serta build free-threaded yang memungkinkan GIL dimatikan pada Python 3.13 dan 3.14. Seluruh contoh dijalankan pada CPython 3.12 standar, sehingga bagian free-threading bersifat konseptual dan tidak didemokan langsung.

Apa yang dikunci GIL#

GIL adalah mutex pada tingkat interpreter, yaitu kunci yang hanya boleh dipegang satu pihak, bukan kunci pada tingkat objek maupun program. Satu interpreter hanya mengizinkan satu thread menjalankan bytecode pada satu waktu, sementara thread lain menunggu giliran. Thread tetap dijadwalkan oleh sistem operasi dan benar-benar berjalan, hanya eksekusi bytecode-nya yang diserialkan.

python Contoh: versi interpreter dan interval ganti GIL
import sys

print(sys.version_info[:3])
print(sys.implementation.name)
print(sys.getswitchinterval())
text Keluaran
(3, 12, 16)
cpython
0.005

Angka 0.005 adalah interval ganti (switch interval) dalam detik, yaitu jeda maksimum sebelum thread yang memegang GIL dipaksa melepasnya agar thread lain mendapat giliran. Nilai ini dapat diubah dengan sys.setswitchinterval(), dan nilai bawaan lima milidetik itulah yang membuat thread tetap bergantian secara wajar tanpa pindah konteks yang terlalu sering. Pindah konteks (context switch) adalah perpindahan eksekusi dari satu thread ke thread lain.

python Contoh: tiga thread berbagi satu interpreter
import threading
import time

hasil = []

def kerja(no):
    time.sleep(0.01)
    hasil.append(no)

threads = [threading.Thread(target=kerja, args=(i,)) for i in range(3)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(sorted(hasil))
print(len(hasil))
text Keluaran
[0, 1, 2]
3

Ketiga thread benar-benar berjalan dan semuanya selesai, tetapi bagian bytecode-nya tetap bergiliran memegang GIL. Waktu tunggu time.sleep() melepas GIL, sehingga thread lain dapat bekerja sementara thread yang tidur tidak memegang apa pun. Inilah alasan program yang menunggu jaringan atau berkas tetap diuntungkan oleh threading, dan sifat dilepas-ketika-menunggu ini pula yang nanti dipakai oleh pustaka C untuk memberi kesempatan thread lain bekerja.

Mengapa GIL ada#

Alasan utamanya ada pada manajemen memori CPython, yang bertumpu pada penghitungan referensi, yaitu bilangan pada tiap objek yang bertambah dan berkurang setiap kali referensi dibuat atau dilepas. Tanpa kunci, dua thread dapat memperbarui penghitung yang sama secara bersamaan dan merusaknya, sehingga koreksinya memerlukan kunci pada tiap objek. GIL menggantikan banyaknya kunci kecil itu dengan satu kunci global yang lebih murah, dan sebagai bonus menyederhanakan penulisan ekstensi C karena objek CPython aman dipakai lintas thread selama GIL dipegang. Harga yang dibayarkan adalah hilangnya paralelisme, yaitu program CPU-bound murni Python tidak menjadi lebih cepat meskipun thread-nya ditambah.

Keputusan ini juga berdiri di atas dasar praktis. Program beruntun tidak membayar biaya kunci sama sekali karena hanya ada satu thread, sedangkan sebagian besar beban kerja pada masa itu bersifat I/O-bound, yaitu pekerjaan yang justru terbantu oleh pelepasan kunci saat menunggu. Dengan demikian, GIL awalnya menawarkan keseimbangan yang nyaman, yaitu kecepatan program tunggal yang terjaga, ekstensi C yang mudah ditulis, dan konkurensi yang cukup untuk pekerjaan menunggu.

Dampak untuk CPU-bound dan I/O-bound#

Beban kerja terbagi dua kubu. Program I/O-bound menghabiskan sebagian besar waktunya untuk menunggu, dan GIL dilepas selama menunggu, sehingga thread tetap efektif untuk pekerjaan semacam ini. Program CPU-bound menghabiskan waktunya di dalam bytecode, dan GIL membuat eksekusinya serial, sehingga empat thread tetap secepat satu thread. Untuk paralelisme CPU murni, jalan keluarnya adalah berpindah ke multiprocessing yang menyiapkan interpreter terpisah, atau ke pustaka C yang sengaja melepas GIL pada bagian komputasinya.

Diagram berikut merangkum perilaku tersebut, yaitu thread-thread yang antre memegang kunci sebelum bytecode-nya boleh berjalan.

Thread 1Thread 2Thread 3GILsatu pemegangdipegang bergiliranBytecode berjalan
Thread bergiliran memegang GIL, sehingga hanya satu bytecode yang berjalan pada satu waktu

Dalam satu saat hanya ada satu pemegang kunci, dan pergantian pemegang berlangsung setiap kali interval ganti tercapai. Pola inilah yang membuat profil program ber-thread terlihat bergiliran alih-alih sejajar, dan menjadi dasar untuk membaca hasil pengukuran pada topik profiling dan benchmarking.

Free-threading pada Python 3.13 dan 3.14#

PEP 703 mengusulkan menjadikan GIL opsional, dan usulan itu diterima. Implementasinya mengganti kunci global dengan kunci per objek beserta penghitungan referensi terbias (biased reference counting), sehingga bytecode dapat dijalankan secara paralel oleh banyak thread. Python 3.13 mengirimkannya sebagai build eksperimental yang terpisah dari build standar, dengan biner seperti python3.13t dan tag ABI cp313t. Python 3.14 menjadikannya build yang didukung resmi melalui PEP 779, tetapi tetap opsional, sedangkan build standar tetap memakai GIL.

Build free-threaded dapat dikenali dan diatur saat runtime. Variabel lingkungan PYTHON_GIL dan opsi baris perintah -X gil menyalakan atau mematikan GIL pada build yang mendukungnya, sedangkan fungsi sys._is_gil_enabled() melaporkan keadaan kuncinya. Ekstensi C perlu dibangun ulang untuk build free-threaded, sehingga ketersediaan wheel yang sesuai menjadi faktor utama kesiapan sebuah proyek.

python Contoh: memeriksa keadaan GIL pada build free-threaded
# tidak-dijalankan
import sys

print(sys.version_info[:3])
print(sys._is_gil_enabled())

Pada build standar, blok tersebut mencetak True, sedangkan pada build free-threaded mencetak False. Fungsi sys._is_gil_enabled() baru tersedia sejak Python 3.13, sehingga blok ini tidak dijalankan pada CPython 3.12 yang menjadi lingkungan jalur ini.

Kesalahan umum#

GIL sering dianggap membuat kode yang menggunakan thread menjadi aman, padahal jaminannya hanya berlaku per bytecode. Urutan antar-bytecode tetap berselang-seling, sehingga operasi majemuk seperti membaca nilai, menghitung, lalu menuliskannya kembali dapat saling menindih dan memicu race condition. Keadaan yang dibagi antar-thread tetap memerlukan kunci seperti threading.Lock.

Kesimpulan sebaliknya juga keliru, yaitu menganggap thread tidak pernah berguna selama GIL ada. Beban kerja I/O-bound tetap diuntungkan, dan pustaka C yang melepas GIL pun ikut terbantu. Pemilihan model konkurensi sebaiknya mengikuti sifat beban kerjanya, yaitu menunggu atau menghitung, bukan kehadiran GIL saja.

Kesalahan ketiga adalah menjalankan beban berat di dalam fungsi Python murni sambil berharap thread menolong, padahal jalannya tetap bergiliran. Pekerjaan semacam itu dipindahkan ke proses terpisah atau ke pustaka yang melepas GIL, dan keputusannya kembali mengikuti hasil pengukuran alih-alih intuisi. Bagian terbesar perbaikannya kerap berupa memangkas pekerjaan yang tidak perlu dilakukan dua kali.

Ringkasan#

  • GIL menserialkan eksekusi bytecode Python per interpreter, dengan interval ganti bawaan lima milidetik yang dapat diubah melalui sys.setswitchinterval().
  • GIL lahir dari penghitungan referensi yang tidak aman antar-thread, dan harganya adalah hilangnya paralelisme untuk program CPU-bound murni Python.
  • Beban kerja I/O-bound tetap diuntungkan karena GIL dilepas saat menunggu, termasuk oleh pustaka C yang melepasnya secara sengaja.
  • Build free-threaded mengganti GIL dengan kunci per objek, tersedia eksperimental pada 3.13 dan didukung resmi pada 3.14, dengan pengecekan melalui sys._is_gil_enabled().

Referensi#