Kursus Python / Membandingkan thread, proses, dan asyncio

Thread, proses, dan asyncio adalah cara berbeda untuk membuat beberapa pekerjaan berjalan maju. Pilih berdasarkan dominasi perhitungan CPU, waktu menunggu I/O, dan dukungan pustaka, lalu ukur beban yang sebenarnya. Menambah konkurensi tidak otomatis membuat program lebih cepat.

Membedakan konkurensi dan paralelisme

Konkurensi berarti beberapa pekerjaan dapat mengalami kemajuan selama rentang waktu yang sama. Paralelisme berarti pekerjaan benar-benar dieksekusi pada saat bersamaan. Program dapat bersifat konkuren dengan bergantian saat menunggu tanpa menjalankan semua perhitungan sekaligus.

Cara Model Biaya yang perlu diperiksa
Thread Berbagi memori dalam satu proses. Sinkronisasi keadaan bersama dan perilaku pustaka.
Proses Ruang memori terpisah. Startup, serialisasi, dan pengiriman data.
asyncio Kemajuan kooperatif pada titik await. Pustaka I/O asinkron dan pemblokiran event loop.

Pada CPython biasa dengan GIL aktif, Global Interpreter Lock membatasi eksekusi bytecode Python oleh thread. Menambah thread tidak langsung memparalelkan semua perhitungan CPU Python murni. Ekstensi yang melepaskan GIL dan build free-threaded memiliki kondisi berbeda, sehingga periksa interpreter serta pustaka yang dipakai sebelum membuat kesimpulan kinerja.

Thread sering dipertimbangkan untuk I/O blocking, proses untuk perhitungan CPU Python murni, dan asyncio ketika pustaka I/O mendukung operasi asinkron. Ini titik awal penilaian, bukan jaminan kecepatan. Perhatikan juga batas layanan dan penyimpanan seperti pada proyek kolektor API sebelum memperbanyak permintaan.

Membandingkan antarmuka dengan program lengkap

Simpan contoh berikut sebagai concurrency.py dengan UTF-8 dalam folder latihan baru. Gunakan Python 3.12 atau lebih baru dan jalankan python concurrency.py sebagai file .py pada terminal lingkungan proyek. Jangan menganggap perilaku pembuatan proses sama pada notebook atau sesi interaktif.

import asyncio
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def square(value):
    return value * value

async def async_square(value):
    await asyncio.sleep(0.01)
    return square(value)

async def gather_values():
    return await asyncio.gather(*(async_square(x) for x in [1, 2, 3]))

def main():
    with ThreadPoolExecutor(max_workers=2) as pool:
        print(list(pool.map(square, [1, 2, 3])))
    with ProcessPoolExecutor(max_workers=2) as pool:
        print(list(pool.map(square, [1, 2, 3])))
    print(asyncio.run(gather_values()))

if __name__ == "__main__":
    main()

Hasil yang diharapkan:

[1, 4, 9]
[1, 4, 9]
[1, 4, 9]

Ketiga cara mengembalikan hasil kuadrat yang sama. ThreadPoolExecutor dan ProcessPoolExecutor memakai paling banyak dua worker. Bagian asyncio menjadwalkan tiga coroutine melalui gather; sleep 0,01 detik menggambarkan penantian kooperatif, bukan permintaan HTTP nyata. Contoh kecil ini menunjukkan antarmuka, bukan benchmark keunggulan salah satu cara.

Fungsi square didefinisikan pada tingkat modul agar proses worker dapat mengaksesnya. Main guard mencegah proses baru mengulang pemanggilan main ketika modul dimuat, yang penting pada cara startup seperti spawn di Windows. Letakkan program dalam file, bukan menyalin bagian pembuatan pool ke konteks yang tidak mendukung impor worker.

Menghormati titik tunggu dan pembatalan task

Penulisan async def tidak otomatis membuat operasi sinkron menjadi nonblocking. Perhitungan CPU panjang atau panggilan jaringan sinkron di dalam coroutine dapat menghalangi event loop sampai kontrol diberikan kembali. Gunakan operasi await yang benar-benar sesuai dengan pustaka dan batasi jumlah pekerjaan.

Simpan contoh berikut sebagai cancel.py, lalu jalankan python cancel.py:

import asyncio
async def worker():
    try:
        await asyncio.sleep(60)
    finally:
        print("worker cleanup")
async def main():
    task = asyncio.create_task(worker())
    await asyncio.sleep(0)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("cancel confirmed")
if __name__ == "__main__":
    asyncio.run(main())

Hasil yang diharapkan:

worker cleanup
cancel confirmed

await asyncio.sleep(0) pada main memberi kesempatan worker mulai dan masuk ke blok try. Setelah task.cancel(), pembatalan disampaikan pada coroutine; finally membersihkan resource sebelum pemanggil mengamati CancelledError. Program ini tidak perlu menunggu sleep 60 detik selesai karena task dibatalkan lebih dahulu.

Pembatalan bersifat kooperatif. Kode yang memblokir tanpa memberi kontrol tidak dapat dianggap langsung berhenti ketika cancel dipanggil. Lakukan pembersihan dengan finally dan teruskan pembatalan kecuali memang ada kebijakan pemulihan yang jelas. Rincian tersedia dalam pembatalan task asyncio.

Membaca kegagalan Future dan arti timeout

Simpan kode lengkap berikut sebagai future_failure.py dan jalankan python future_failure.py:

from concurrent.futures import ThreadPoolExecutor
def calculate(value):
    if value < 0:
        raise ValueError("negative input")
    return value * value
with ThreadPoolExecutor(max_workers=2) as pool:
    futures = [pool.submit(calculate, value) for value in [2, -1]]
    for future in futures:
        try:
            print(future.result(timeout=5))
        except ValueError as error:
            print("failed:", error)

Hasil yang diharapkan:

4
failed: negative input

submit mengembalikan Future yang mewakili pekerjaan. Exception pada worker disampaikan ketika pemanggil membaca result(), sehingga kegagalan tidak boleh diabaikan hanya karena proses pengajuan berhasil. Contoh membaca Future menurut urutan pengajuan; urutan selesai pekerjaan dapat berbeda.

result(timeout=5) membatasi berapa lama pemanggil menunggu, bukan menghentikan secara paksa fungsi yang sudah berjalan. Pembatalan Future yang sudah berjalan juga tidak sama dengan mematikan thread. Context manager executor menunggu pekerjaan yang masih berjalan saat shutdown normal. Rincian hasil, exception, dan shutdown dijelaskan dalam concurrent.futures.

max_workers membatasi worker yang berjalan, bukan seluruh antrean pengajuan. Pecah masukan besar menjadi batch atau mekanisme pembatasan lain sesuai versi Python. Pada asyncio, gunakan batas I/O seperti Semaphore bila banyak task berhubungan dengan layanan luar; membatasi resource aktif dan membatasi jumlah task yang dibuat adalah dua kebutuhan yang perlu dinilai terpisah.

Mengukur beban nyata dan menjaga hasil yang benar

Ukur data yang mewakili pekerjaan sebenarnya dan catat versi interpreter, build, jumlah worker, ukuran masukan, memori, serta waktu. Startup proses dapat lebih mahal daripada perhitungan kecil. Jangan memakai sleep sebagai bukti bahwa HTTP atau CPU aplikasi akan memperoleh percepatan yang sama.

map dan gather pada contoh mengembalikan hasil sesuai urutan masukan, walaupun pekerjaan dapat selesai dalam urutan lain. Tentukan kebijakan hasil sebagian, penyebaran error, pembatalan, dan penutupan sebelum mengubah alur menjadi konkuren. Keadaan bersama yang dimodifikasi oleh beberapa thread memerlukan sinkronisasi sesuai operasi dan pustakanya.

asyncio.run adalah pintu masuk program asinkron dan tidak boleh dipanggil ketika event loop lain sedang berjalan pada thread yang sama. Dalam lingkungan yang sudah mengelola loop, gunakan pola yang sesuai seperti await dari coroutine yang tersedia. Panduan asyncio runner menjelaskan batas tersebut.