Multithread masuk Python dengan Contoh: Pelajari GIL di Python

Bahasa pemrograman python memungkinkan Anda untuk menggunakan multiprocessing atau multithreading. Dalam tutorial ini, Anda akan mempelajari cara menulis aplikasi multithreaded di Python.

Apa itu Benang?

Thread adalah unit eksekusi pada pemrograman bersamaan. Multithreading adalah teknik yang memungkinkan CPU menjalankan banyak tugas dari satu proses pada waktu yang bersamaan. Thread ini dapat dijalankan secara individual sambil berbagi sumber daya prosesnya.

Apa itu Proses?

Proses pada dasarnya adalah program yang sedang dijalankan. Saat Anda memulai aplikasi di komputer (seperti browser atau editor teks), sistem operasi membuat proses.

Apa itu Multithreading Python?

Multithread masuk Python pemrograman adalah teknik terkenal di mana beberapa thread dalam suatu proses berbagi ruang datanya dengan thread utama yang membuat berbagi informasi dan komunikasi dalam thread menjadi mudah dan efisien. Thread lebih ringan dibandingkan proses. Multi thread dapat dijalankan secara individual sambil berbagi sumber daya prosesnya. Tujuan multithreading adalah menjalankan banyak tugas dan fungsi sel secara bersamaan.

Apa itu Multiprosesor?

Multiprosesor memungkinkan Anda menjalankan beberapa proses yang tidak terkait secara bersamaan. Proses-proses ini tidak berbagi sumber daya dan berkomunikasi melalui IPC.

Python Multithreading vs Multiprosesing

Untuk memahami proses dan thread, pertimbangkan skenario ini: File .exe di komputer Anda adalah sebuah program. Saat Anda membukanya, OS memuatnya ke dalam memori, dan CPU menjalankannya. Contoh program yang sedang berjalan disebut proses.

Setiap proses akan memiliki 2 komponen mendasar:

  • Kode
  • Data

Sekarang, suatu proses dapat berisi satu atau lebih sub-bagian yang disebut utas. Hal ini bergantung pada arsitektur OS. Anda dapat menganggap thread sebagai bagian dari proses yang dapat dieksekusi secara terpisah oleh sistem operasi.

Dengan kata lain, ini adalah aliran instruksi yang dapat dijalankan secara independen oleh OS. Thread dalam satu proses berbagi data dari proses tersebut dan dirancang untuk bekerja sama untuk memfasilitasi paralelisme.

Mengapa menggunakan Multithreading?

Multithreading memungkinkan Anda memecah aplikasi menjadi beberapa subtugas dan menjalankan tugas-tugas ini secara bersamaan. Jika Anda menggunakan multithreading dengan benar, kecepatan, kinerja, dan rendering aplikasi Anda dapat ditingkatkan.

Python MultiThreading

Python mendukung konstruksi untuk multiprocessing dan multithreading. Dalam tutorial ini, Anda terutama akan fokus pada penerapan multithread aplikasi dengan python. Ada dua modul utama yang dapat digunakan untuk menangani thread di Python:

  1. benang modul, dan
  2. threading modul

Namun di python juga ada yang disebut global interpreter lock (GIL). Itu tidak memungkinkan banyak peningkatan kinerja dan bahkan mungkin menurunkan kinerja beberapa aplikasi multithread. Anda akan mempelajari semuanya di bagian selanjutnya dari tutorial ini.

Modul Thread dan Threading

Dua modul yang akan Anda pelajari dalam tutorial ini adalah modul benang dan modul penguliran.

Namun, modul thread sudah lama tidak digunakan lagi. Dimulai dengan Python 3, telah ditetapkan sebagai usang dan hanya dapat diakses sebagai __benang untuk kompatibilitas mundur.

Anda harus menggunakan level yang lebih tinggi threading modul untuk aplikasi yang ingin Anda terapkan. Modul thread hanya dibahas di sini untuk tujuan pendidikan.

Modul Benang

Sintaks untuk membuat thread baru menggunakan modul ini adalah sebagai berikut:

thread.start_new_thread(function_name, arguments)

Baiklah, sekarang Anda telah membahas teori dasar untuk memulai coding. Jadi, buka milikmu IDLE atau gunakan notepad dan ketik yang berikut ini:

import time
import _thread

def thread_test(name, wait):
   i = 0
   while i <= 3:
      time.sleep(wait)
      print("Running %s\n" %name)
      i = i + 1

   print("%s has finished execution" %name)

if __name__ == "__main__":
    
    _thread.start_new_thread(thread_test, ("First Thread", 1))
    _thread.start_new_thread(thread_test, ("Second Thread", 2))
    _thread.start_new_thread(thread_test, ("Third Thread", 3))

Simpan file dan tekan F5 untuk menjalankan program. Jika semuanya dilakukan dengan benar, inilah output yang akan Anda lihat:

Modul Benang

Anda akan mempelajari lebih lanjut tentang kondisi balapan dan cara menanganinya di bagian selanjutnya

Modul Benang

PENJELASAN KODE

  1. Pernyataan ini mengimpor modul waktu dan thread yang digunakan untuk menangani eksekusi dan penundaan Python utas.
  2. Di sini, Anda telah mendefinisikan fungsi yang dipanggil uji_utas, yang akan dipanggil oleh mulai_utas_baru metode. Fungsi ini menjalankan perulangan while selama empat iterasi dan mencetak nama thread yang memanggilnya. Setelah iterasi selesai, ia akan mencetak pesan yang mengatakan bahwa thread telah selesai dieksekusi.
  3. Ini adalah bagian utama dari program Anda. Di sini, Anda cukup menelepon mulai_utas_baru metode dengan thread_test berfungsi sebagai argumen. Ini akan membuat thread baru untuk fungsi yang Anda berikan sebagai argumen dan mulai menjalankannya. Perhatikan bahwa Anda dapat mengganti ini (thread_test) dengan fungsi lain yang ingin Anda jalankan sebagai thread.

Modul Penguliran

Modul ini adalah implementasi threading tingkat tinggi dengan python dan standar de facto untuk mengelola aplikasi multithread. Ini menyediakan berbagai fitur jika dibandingkan dengan modul thread.

Struktur modul Threading
Struktur modul Threading

Berikut adalah daftar beberapa fungsi berguna yang didefinisikan dalam modul ini:

Nama Fungsi Description
jumlah aktif() Mengembalikan hitungan Benang benda yang masih hidup
benang saat ini() Mengembalikan objek kelas Thread saat ini.
menghitung() Mencantumkan semua objek Thread yang aktif.
isDaemon() Mengembalikan nilai benar jika thread adalah daemon.
hidup() Mengembalikan nilai benar jika thread masih hidup.
Metode Kelas Thread
Mulailah() Memulai aktivitas thread. Itu harus dipanggil hanya sekali untuk setiap thread karena akan menimbulkan kesalahan runtime jika dipanggil beberapa kali.
Lari() Metode ini menunjukkan aktivitas thread dan dapat ditimpa oleh kelas yang memperluas kelas Thread.
Ikuti() Ini memblokir eksekusi kode lain hingga thread tempat metode join() dipanggil dihentikan.

Latar Belakang: Kelas Thread

Sebelum Anda mulai membuat kode program multithread menggunakan modul threading, penting untuk memahami kelas Thread. Kelas thread adalah kelas utama yang mendefinisikan templat dan operasi thread dalam python.

Cara paling umum untuk membuat aplikasi python multithread adalah dengan mendeklarasikan kelas yang memperluas kelas Thread dan mengganti metode run()-nya.

Kelas Thread, secara ringkas, menandakan urutan kode yang berjalan secara terpisah benang kontrol.

Jadi, saat menulis aplikasi multithread, Anda akan melakukan hal berikut:

  1. mendefinisikan kelas yang memperluas kelas Thread
  2. Ganti __init__ pembina
  3. Ganti Lari() metode

Setelah objek thread dibuat, Mulailah() metode yang dapat digunakan untuk memulai pelaksanaan kegiatan ini dan Ikuti() metode dapat digunakan untuk memblokir semua kode lainnya hingga aktivitas saat ini selesai.

Sekarang, mari coba gunakan modul threading untuk mengimplementasikan contoh Anda sebelumnya. Sekali lagi, nyalakan IDLE dan ketik yang berikut ini:

import time
import threading

class threadtester (threading.Thread):
    def __init__(self, id, name, i):
       threading.Thread.__init__(self)
       self.id = id
       self.name = name
       self.i = i
       
    def run(self):
       thread_test(self.name, self.i, 5)
       print ("%s has finished execution " %self.name)

def thread_test(name, wait, i):

    while i:
       time.sleep(wait)
       print ("Running %s \n" %name)
       i = i - 1

if __name__=="__main__":
    thread1 = threadtester(1, "First Thread", 1)
    thread2 = threadtester(2, "Second Thread", 2)
    thread3 = threadtester(3, "Third Thread", 3)

    thread1.start()
    thread2.start()
    thread3.start()

    thread1.join()
    thread2.join()
    thread3.join()

Ini akan menjadi output ketika Anda menjalankan kode di atas:

Latar Belakang: Kelas Thread

PENJELASAN KODE

Latar Belakang: Kelas Thread

  1. Bagian ini sama dengan contoh kita sebelumnya. Di sini, Anda mengimpor modul time dan thread yang digunakan untuk menangani eksekusi dan penundaan Python utas.
  2. Dalam bit ini, Anda membuat kelas bernama threadtester, yang mewarisi atau memperluas Benang kelas modul threading. Ini adalah salah satu cara paling umum untuk membuat thread dengan python. Namun, Anda sebaiknya hanya mengganti konstruktor dan Lari() metode di aplikasi Anda. Seperti yang Anda lihat pada contoh kode di atas, __init__ metode (konstruktor) telah diganti. Demikian pula, Anda juga telah menggantinya Lari() metode. Ini berisi kode yang ingin Anda jalankan di dalam thread. Dalam contoh ini, Anda memanggil fungsi thread_test().
  3. Ini adalah metode thread_test() yang mengambil nilai i sebagai argumen, kurangi sebanyak 1 pada setiap iterasi dan ulangi sisa kode hingga i menjadi 0. Dalam setiap iterasi, ia mencetak nama thread yang sedang dijalankan dan tidur selama beberapa detik tunggu (yang juga dianggap sebagai argumen ).
  4. thread1 = threadtester(1, “First Thread”, 1) Di sini, kita membuat thread dan meneruskan tiga parameter yang kita deklarasikan di __init__. Parameter pertama adalah id thread, parameter kedua adalah nama thread, dan parameter ketiga adalah counter, yang menentukan berapa kali perulangan while harus dijalankan.
  5. thread2.start() Metode start digunakan untuk memulai eksekusi thread. Secara internal, fungsi start() memanggil metode run() kelas Anda.
  6. thread3.join() Metode join() memblokir eksekusi kode lain dan menunggu hingga thread yang dipanggil selesai.

Seperti yang telah Anda ketahui, utas yang berada dalam proses yang sama memiliki akses ke memori dan data dari proses tersebut. Akibatnya, jika lebih dari satu utas mencoba mengubah atau mengakses data secara bersamaan, kesalahan dapat terjadi.

Di bagian berikutnya, Anda akan melihat berbagai jenis komplikasi yang dapat muncul ketika thread mengakses data dan bagian kritis tanpa memeriksa transaksi akses yang ada.

Kebuntuan dan Kondisi Balapan

Sebelum mempelajari tentang deadlock dan kondisi balapan, ada baiknya untuk memahami beberapa definisi dasar terkait pemrograman konkuren:

  • Bagian KritisIni adalah fragmen kode yang mengakses atau mengubah variabel bersama dan harus dilakukan sebagai transaksi atom.
  • Peralihan KonteksIni adalah proses yang diikuti CPU untuk menyimpan status suatu utas sebelum beralih dari satu tugas ke tugas lain sehingga dapat dilanjutkan dari titik yang sama nanti.

Kebuntuan

Kebuntuan adalah masalah yang paling ditakuti oleh para pengembang ketika menulis aplikasi bersamaan/multithreaded dalam bahasa python. Cara terbaik untuk memahami deadlock adalah dengan menggunakan contoh masalah ilmu komputer klasik yang dikenal sebagai Makan PhiloMasalah sopher.

Pernyataan masalah bagi para filsuf makan adalah sebagai berikut:

Lima filsuf duduk di meja bundar dengan lima piring spageti (sejenis pasta) dan lima garpu, seperti yang ditunjukkan dalam diagram.

Makan PhiloMasalah sopher

Makan PhiloMasalah sopher

Pada waktu tertentu, seorang filsuf pasti sedang makan atau berpikir.

Selain itu, seorang filsuf harus mengambil dua garpu yang bersebelahan dengannya (yaitu garpu kiri dan kanan) sebelum ia dapat memakan spageti. Masalah kebuntuan terjadi ketika kelima filsuf mengambil garpu kanan mereka secara bersamaan.

Karena setiap filsuf memiliki satu garpu, mereka semua akan menunggu yang lain meletakkan garpu mereka. Akibatnya, tidak ada satu pun dari mereka yang bisa makan spageti.

Demikian pula, dalam sistem konkuren, kebuntuan terjadi ketika beberapa thread atau proses (filosofer) mencoba memperoleh sumber daya sistem bersama (fork) pada saat yang sama. Akibatnya, tidak ada proses yang mendapat kesempatan untuk mengeksekusi karena mereka menunggu sumber daya lain yang dimiliki oleh proses lain.

Kondisi Balapan

Kondisi balapan adalah kondisi program yang tidak diinginkan yang terjadi saat sistem menjalankan dua atau lebih operasi secara bersamaan. Misalnya, perhatikan loop for sederhana ini:

i=0; # a global variable
for x in range(100):
    print(i)
    i+=1;

Jika Anda membuat n jumlah thread yang menjalankan kode ini sekaligus, Anda tidak dapat menentukan nilai i (yang dibagikan oleh thread) ketika program selesai dieksekusi. Hal ini karena dalam lingkungan multithreading nyata, thread dapat tumpang tindih, dan nilai i yang diambil dan diubah oleh thread dapat berubah di antara saat thread lain mengaksesnya.

Ini adalah dua kelas masalah utama yang dapat terjadi dalam aplikasi python multithreaded atau terdistribusi. Di bagian berikutnya, Anda akan mempelajari cara mengatasi masalah ini dengan menyinkronkan thread.

Syncbenang kronis

Untuk menangani kondisi balapan, kebuntuan, dan masalah berbasis utas lainnya, modul threading menyediakan Mengunci objek. Idenya adalah bahwa ketika sebuah thread ingin mengakses sumber daya tertentu, ia memperoleh kunci untuk sumber daya tersebut. Setelah sebuah thread mengunci sumber daya tertentu, tidak ada thread lain yang dapat mengaksesnya hingga kunci tersebut dilepaskan. Akibatnya, perubahan pada sumber daya akan bersifat atomik, dan kondisi persaingan akan dihindari.

Kunci adalah primitif sinkronisasi tingkat rendah yang diterapkan oleh __benang modul. Pada waktu tertentu, gembok dapat berada dalam salah satu dari 2 kondisi berikut: terkunci or tidak terkunci. Ini mendukung dua metode:

  1. mendapatkan()Ketika status kunci tidak terkunci, memanggil metode acquire() akan mengubah status menjadi terkunci dan kembali. Namun, jika keadaan terkunci, panggilan ke acquire() diblokir hingga metode rilis() dipanggil oleh thread lain.
  2. melepaskan()Metode rilis() digunakan untuk mengatur keadaan menjadi tidak terkunci, yaitu untuk melepaskan kunci. Itu bisa dipanggil oleh thread apa pun, belum tentu thread yang memperoleh kuncinya.

Berikut ini contoh penggunaan kunci di aplikasi Anda. Nyalakan milikmu IDLE dan ketik yang berikut ini:

import threading
lock = threading.Lock()

def first_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the first funcion')
        lock.release()

def second_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the second funcion')
        lock.release()

if __name__=="__main__":
    thread_one = threading.Thread(target=first_function)
    thread_two = threading.Thread(target=second_function)

    thread_one.start()
    thread_two.start()

    thread_one.join()
    thread_two.join()

Sekarang, tekan F5. Anda akan melihat keluaran seperti ini:

Syncmengkronisasikan Thread

PENJELASAN KODE

Syncmengkronisasikan Thread

  1. Di sini, Anda cukup membuat kunci baru dengan memanggil threading.Kunci() fungsi pabrik. Secara internal, Lock() mengembalikan sebuah instance dari kelas Lock beton paling efektif yang dikelola oleh platform.
  2. Pada pernyataan pertama, Anda memperoleh kunci dengan memanggil metode acquire(). Ketika kunci telah diberikan, Anda mencetak “kunci diperoleh” ke konsol. Setelah semua kode yang ingin dijalankan thread telah selesai dieksekusi, lepaskan kunci dengan memanggil metode rilis().

Teorinya bagus, tetapi bagaimana Anda tahu bahwa kunci tersebut benar-benar berfungsi? Jika Anda melihat outputnya, Anda akan melihat bahwa setiap pernyataan print mencetak tepat satu baris dalam satu waktu. Ingat kembali bahwa, dalam contoh sebelumnya, output dari print bersifat acak karena beberapa thread mengakses metode print() secara bersamaan. Di sini, fungsi print dipanggil hanya setelah kunci diperoleh. Jadi, output ditampilkan satu per satu dan baris demi baris.

Selain kunci, python juga mendukung beberapa mekanisme lain untuk menangani sinkronisasi utas seperti yang tercantum di bawah ini:

  1. RKunci
  2. Semaphores
  3. Kondisi
  4. Acara, dan
  5. Hambatan

Global Interpreter Lock (dan cara mengatasinya)

Sebelum membahas detail GIL Python, mari kita definisikan beberapa istilah yang berguna untuk memahami bagian selanjutnya:

  1. Kode terikat CPU: ini mengacu pada setiap bagian kode yang akan langsung dieksekusi oleh CPU.
  2. Kode terikat I/O: ini bisa berupa kode apa pun yang mengakses sistem file melalui OS
  3. CPython: itu adalah referensi implementasi of Python dan dapat digambarkan sebagai penerjemah yang ditulis dalam C dan Python (bahasa pemrograman).

Apa itu GIL Python?

Kunci Penerjemah Global (GIL) di python adalah kunci proses atau mutex yang digunakan saat menangani proses. Ini memastikan bahwa satu thread dapat mengakses sumber daya tertentu pada satu waktu dan juga mencegah penggunaan objek dan bytecode sekaligus. Hal ini menguntungkan program single-threaded dalam peningkatan kinerja. GIL dengan python sangat sederhana dan mudah diimplementasikan.

Kunci dapat digunakan untuk memastikan bahwa hanya satu thread yang memiliki akses ke sumber daya tertentu pada waktu tertentu.

Salah satu fitur dari Python ialah menggunakan kunci global pada setiap proses penerjemah, artinya setiap proses memperlakukan penerjemah python itu sendiri sebagai sumber daya.

Misalnya, anggaplah Anda telah menulis program python yang menggunakan dua thread untuk menjalankan operasi CPU dan 'I/O'. Saat Anda menjalankan program ini, berikut yang terjadi:

  1. Penerjemah python membuat proses baru dan memunculkan utas
  2. Saat thread-1 mulai berjalan, thread-XNUMX akan memperoleh GIL terlebih dahulu dan menguncinya.
  3. Jika thread-2 ingin dijalankan sekarang, ia harus menunggu hingga GIL dirilis meskipun prosesor lain tersedia.
  4. Sekarang, misalkan thread-1 sedang menunggu operasi I/O. Pada saat ini, thread-2 akan melepaskan GIL, dan thread-XNUMX akan memperolehnya.
  5. Setelah menyelesaikan operasi I/O, jika thread-1 ingin dijalankan sekarang, thread-2 harus menunggu lagi hingga GIL dirilis oleh thread-XNUMX.

Oleh karena itu, hanya satu thread yang dapat mengakses interpreter kapan saja, artinya hanya akan ada satu thread yang mengeksekusi kode python pada titik waktu tertentu.

Ini baik-baik saja dalam prosesor inti tunggal karena akan menggunakan pemotongan waktu (lihat bagian pertama tutorial ini) untuk menangani thread. Namun, dalam kasus prosesor multi-inti, fungsi terikat CPU yang dijalankan pada banyak thread akan berdampak besar pada efisiensi program karena program tersebut tidak benar-benar menggunakan semua inti yang tersedia pada saat yang bersamaan.

Mengapa GIL dibutuhkan?

CPython pemungut sampah menggunakan teknik manajemen memori yang efisien yang dikenal sebagai penghitungan referensi. Begini cara kerjanya: Setiap objek dalam python memiliki jumlah referensi, yang bertambah saat ditetapkan ke nama variabel baru atau ditambahkan ke kontainer (seperti tuple, daftar, dsb.). Demikian pula, jumlah referensi berkurang saat referensi keluar dari cakupan atau saat pernyataan del dipanggil. Saat jumlah referensi objek mencapai 0, objek tersebut dipungut sampah, dan memori yang dialokasikan dibebaskan.

Namun masalahnya adalah variabel jumlah referensi rentan terhadap kondisi persaingan seperti variabel global lainnya. Untuk mengatasi masalah ini, pengembang python memutuskan untuk menggunakan kunci interpreter global. Pilihan lainnya adalah menambahkan kunci ke setiap objek yang akan mengakibatkan kebuntuan dan peningkatan overhead dari panggilan acquire() dan release().

Oleh karena itu, GIL merupakan pembatasan yang signifikan untuk program python multithreaded yang menjalankan operasi berat yang terikat CPU (yang secara efektif menjadikannya single-threaded). Jika Anda ingin menggunakan beberapa inti CPU dalam aplikasi Anda, gunakan multiproses modul sebagai gantinya.

Kesimpulan

  • Python mendukung 2 modul untuk multithreading:
    1. __benang modul: Ini menyediakan implementasi tingkat rendah untuk threading dan sudah usang.
    2. modul penguliran: Ini menyediakan implementasi tingkat tinggi untuk multithreading dan merupakan standar saat ini.
  • Untuk membuat thread menggunakan modul threading, Anda harus melakukan hal berikut:
    1. Buat kelas yang memperluas Benang kelas.
    2. Ganti konstruktornya (__init__).
    3. Ganti itu Lari() Metode.
    4. Buat objek kelas ini.
  • Sebuah thread dapat dieksekusi dengan memanggil Mulailah() Metode.
  • Ikuti() metode ini dapat digunakan untuk memblokir thread lain hingga thread ini (thread yang dipanggil join) menyelesaikan eksekusi.
  • Kondisi balapan terjadi ketika beberapa thread mengakses atau mengubah sumber daya bersama secara bersamaan.
  • Hal ini dapat dihindari dengan Syncbenang kronis.
  • Python mendukung 6 cara untuk menyinkronkan thread:
    1. Kunci
    2. RKunci
    3. Semaphores
    4. Kondisi
    5. Acara, dan
    6. Hambatan
  • Kunci hanya mengizinkan thread tertentu yang telah memperoleh kunci untuk memasuki bagian kritis.
  • Kunci memiliki 2 metode utama:
    1. mendapatkan(): Ini menyetel status kunci ke terkunci. Jika dipanggil pada objek yang terkunci, objek tersebut akan diblokir hingga sumber dayanya bebas.
    2. melepaskan(): Ini menyetel status kunci ke terkunci dan kembali. Jika dipanggil pada objek yang tidak terkunci, ia mengembalikan false.
  • Kunci juru bahasa global adalah mekanisme yang hanya melalui 1 CPython proses penerjemah dapat dijalankan pada suatu waktu.
  • Itu digunakan untuk memfasilitasi fungsi penghitungan referensi CPythons pemulung.
  • Untuk membuat Python aplikasi dengan operasi berat yang terikat CPU, Anda harus menggunakan modul multiprosesor.