Tutorial Python Multithreading dan Multiprocessing

Diterbitkan: 2022-03-11
Catatan: Dengan permintaan populer bahwa saya mendemonstrasikan beberapa teknik alternatif---termasuk async/menunggu, hanya tersedia sejak munculnya Python 3.5---Saya telah menambahkan beberapa pembaruan di akhir artikel. Menikmati!

Diskusi yang mengkritik Python sering kali membicarakan tentang betapa sulitnya menggunakan Python untuk pekerjaan multithread, menunjuk jari pada apa yang dikenal sebagai kunci penerjemah global (yang disebut sebagai GIL) yang mencegah beberapa utas kode Python berjalan secara bersamaan. Karena ini, modul multithreading Python tidak berperilaku seperti yang Anda harapkan jika Anda bukan pengembang Python dan Anda berasal dari bahasa lain seperti C++ atau Java. Harus dijelaskan bahwa seseorang masih dapat menulis kode dengan Python yang berjalan secara bersamaan atau paralel dan membuat perbedaan mencolok dalam kinerja yang dihasilkan, selama hal-hal tertentu dipertimbangkan. Jika Anda belum membacanya, saya sarankan Anda melihat artikel Eqbal Quran tentang konkurensi dan paralelisme di Ruby di sini di Blog Toptal Engineering.

Dalam tutorial konkurensi Python ini, kami akan menulis skrip Python kecil untuk mengunduh gambar populer teratas dari Imgur. Kami akan mulai dengan versi yang mengunduh gambar secara berurutan, atau satu per satu. Sebagai prasyarat, Anda harus mendaftarkan aplikasi di Imgur. Jika Anda belum memiliki akun Imgur, silakan buat terlebih dahulu.

Skrip dalam contoh threading ini telah diuji dengan Python 3.6.4. Dengan beberapa perubahan, mereka juga harus dijalankan dengan Python 2—urllib adalah yang paling banyak berubah di antara kedua versi Python ini.

Memulai dengan Python Multithreading

Mari kita mulai dengan membuat modul Python, bernama download.py . File ini akan berisi semua fungsi yang diperlukan untuk mengambil daftar gambar dan mengunduhnya. Kami akan membagi fungsi ini menjadi tiga fungsi terpisah:

  • get_links
  • download_link
  • setup_download_dir

Fungsi ketiga, setup_download_dir , akan digunakan untuk membuat direktori tujuan unduhan jika belum ada.

API Imgur memerlukan permintaan HTTP untuk memuat header Authorization dengan ID klien. Anda dapat menemukan ID klien ini dari dasbor aplikasi yang telah Anda daftarkan di Imgur, dan responsnya akan dikodekan JSON. Kita dapat menggunakan pustaka JSON standar Python untuk memecahkan kodenya. Mengunduh gambar adalah tugas yang lebih sederhana, karena yang harus Anda lakukan hanyalah mengambil gambar dengan URL-nya dan menulisnya ke file.

Seperti inilah tampilan skripnya:

 import json import logging import os from pathlib import Path from urllib.request import urlopen, Request logger = logging.getLogger(__name__) types = {'image/jpeg', 'image/png'} def get_links(client_id): headers = {'Authorization': 'Client-ID {}'.format(client_id)} req = Request('https://api.imgur.com/3/gallery/random/random/', headers=headers, method='GET') with urlopen(req) as resp: data = json.loads(resp.read().decode('utf-8')) return [item['link'] for item in data['data'] if 'type' in item and item['type'] in types] def download_link(directory, link): download_path = directory / os.path.basename(link) with urlopen(link) as image, download_path.open('wb') as f: f.write(image.read()) logger.info('Downloaded %s', link) def setup_download_dir(): download_dir = Path('images') if not download_dir.exists(): download_dir.mkdir() return download_dir

Selanjutnya, kita perlu menulis modul yang akan menggunakan fungsi-fungsi ini untuk mengunduh gambar, satu per satu. Kami akan memberi nama single.py ini. Ini akan berisi fungsi utama dari pengunduh gambar Imgur versi naif pertama kami. Modul akan mengambil ID klien Imgur dalam variabel lingkungan IMGUR_CLIENT_ID . Ini akan memanggil setup_download_dir untuk membuat direktori tujuan unduhan. Terakhir, ini akan mengambil daftar gambar menggunakan fungsi get_links , menyaring semua GIF dan URL album, dan kemudian menggunakan download_link untuk mengunduh dan menyimpan masing-masing gambar tersebut ke disk. Berikut tampilan single.py :

 import logging import os from time import time from download import setup_download_dir, get_links, download_link logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) def main(): ts = time() client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() links = get_links(client_id) for link in links: download_link(download_dir, link) logging.info('Took %s seconds', time() - ts) if __name__ == '__main__': main()

Di laptop saya, skrip ini membutuhkan waktu 19,4 detik untuk mengunduh 91 gambar. Harap perhatikan bahwa angka-angka ini dapat bervariasi berdasarkan jaringan tempat Anda berada. 19,4 detik tidak terlalu lama, tetapi bagaimana jika kita ingin mengunduh lebih banyak gambar? Mungkin 900 gambar, bukan 90. Dengan rata-rata 0,2 detik per gambar, 900 gambar akan memakan waktu sekitar 3 menit. Untuk 9000 gambar itu akan memakan waktu 30 menit. Berita baiknya adalah dengan memperkenalkan konkurensi atau paralelisme, kita dapat mempercepat ini secara dramatis.

Semua contoh kode berikutnya hanya akan menampilkan pernyataan impor yang baru dan khusus untuk contoh tersebut. Untuk kenyamanan, semua skrip Python ini dapat ditemukan di repositori GitHub ini.

Konkurensi dan Paralelisme dengan Python: Contoh Threading

Threading adalah salah satu pendekatan paling terkenal untuk mencapai konkurensi dan paralelisme Python. Threading adalah fitur yang biasanya disediakan oleh sistem operasi. Utas lebih ringan dari proses, dan berbagi ruang memori yang sama.

Model memori multithreading Python

Dalam contoh threading Python ini, kita akan menulis modul baru untuk menggantikan single.py . Modul ini akan membuat kumpulan delapan utas, membuat total sembilan utas termasuk utas utama. Saya memilih delapan utas pekerja karena komputer saya memiliki delapan inti CPU dan satu utas pekerja per inti tampaknya merupakan angka yang bagus untuk berapa banyak utas yang dijalankan sekaligus. Dalam praktiknya, nomor ini dipilih dengan lebih hati-hati berdasarkan faktor lain, seperti aplikasi dan layanan lain yang berjalan di mesin yang sama.

Ini hampir sama dengan yang sebelumnya, dengan pengecualian bahwa kita sekarang memiliki kelas baru, DownloadWorker , yang merupakan turunan dari kelas Thread Python. Metode run telah diganti, yang menjalankan infinite loop. Pada setiap iterasi, ia memanggil self.queue.get() untuk mencoba dan mengambil URL dari antrian thread-safe. Ini memblokir sampai ada item dalam antrian untuk diproses pekerja. Setelah pekerja menerima item dari antrian, ia kemudian memanggil metode download_link yang sama yang digunakan dalam skrip sebelumnya untuk mengunduh gambar ke direktori gambar. Setelah unduhan selesai, pekerja memberi sinyal antrian bahwa tugas itu selesai. Ini sangat penting, karena Antrian melacak berapa banyak tugas yang diantrekan. Panggilan ke queue.join() akan memblokir utas utama selamanya jika pekerja tidak memberi sinyal bahwa mereka menyelesaikan tugas.

 import logging import os from queue import Queue from threading import Thread from time import time from download import setup_download_dir, get_links, download_link logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class DownloadWorker(Thread): def __init__(self, queue): Thread.__init__(self) self.queue = queue def run(self): while True: # Get the work from the queue and expand the tuple directory, link = self.queue.get() try: download_link(directory, link) finally: self.queue.task_done() def main(): ts = time() client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() links = get_links(client_id) # Create a queue to communicate with the worker threads queue = Queue() # Create 8 worker threads for x in range(8): worker = DownloadWorker(queue) # Setting daemon to True will let the main thread exit even though the workers are blocking worker.daemon = True worker.start() # Put the tasks into the queue as a tuple for link in links: logger.info('Queueing {}'.format(link)) queue.put((download_dir, link)) # Causes the main thread to wait for the queue to finish processing all the tasks queue.join() logging.info('Took %s', time() - ts) if __name__ == '__main__': main()

Menjalankan skrip contoh threading Python ini pada mesin yang sama yang digunakan sebelumnya menghasilkan waktu pengunduhan 4,1 detik! Itu 4,7 kali lebih cepat dari contoh sebelumnya. Meskipun ini jauh lebih cepat, perlu disebutkan bahwa hanya satu utas yang dieksekusi pada satu waktu selama proses ini karena GIL. Oleh karena itu, kode ini bersamaan tetapi tidak paralel. Alasan masih lebih cepat adalah karena ini adalah tugas terikat IO. Prosesor hampir tidak berkeringat saat mengunduh gambar-gambar ini, dan sebagian besar waktu dihabiskan untuk menunggu jaringan. Inilah sebabnya mengapa multithreading Python dapat memberikan peningkatan kecepatan yang besar. Prosesor dapat beralih di antara utas setiap kali salah satu dari mereka siap untuk melakukan beberapa pekerjaan. Menggunakan modul threading dengan Python atau bahasa lain yang ditafsirkan dengan GIL sebenarnya dapat mengakibatkan penurunan kinerja. Jika kode Anda melakukan tugas terikat CPU, seperti mendekompresi file gzip, menggunakan modul threading akan menghasilkan waktu eksekusi yang lebih lambat. Untuk tugas terikat CPU dan eksekusi yang benar-benar paralel, kita dapat menggunakan modul multiprosesor.

Sementara implementasi Python referensi de facto —CPython–memiliki GIL, ini tidak berlaku untuk semua implementasi Python. Misalnya, IronPython, implementasi Python menggunakan kerangka kerja .NET, tidak memiliki GIL, dan Jython juga tidak, implementasi berbasis Java. Anda dapat menemukan daftar implementasi Python yang berfungsi di sini.

Terkait: Praktik Terbaik dan Tip Python oleh Pengembang Toptal

Konkurensi dan Paralelisme dalam Python Contoh 2: Pemijahan Beberapa Proses

Modul multiprocessing lebih mudah untuk dimasukkan daripada modul threading, karena kita tidak perlu menambahkan kelas seperti contoh threading Python. Satu-satunya perubahan yang perlu kita lakukan adalah pada fungsi utama.

Tutorial multiprosesor python: Modul

Untuk menggunakan beberapa proses, kami membuat Pool multiprosesor . Dengan metode peta yang disediakan, kami akan meneruskan daftar URL ke kumpulan, yang pada gilirannya akan menelurkan delapan proses baru dan menggunakan masing-masing untuk mengunduh gambar secara paralel. Ini adalah paralelisme sejati, tetapi ada biayanya. Seluruh memori skrip disalin ke setiap subproses yang muncul. Dalam contoh sederhana ini, ini bukan masalah besar, tetapi dapat dengan mudah menjadi overhead yang serius untuk program non-sepele.

 import logging import os from functools import partial from multiprocessing.pool import Pool from time import time from download import setup_download_dir, get_links, download_link logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.getLogger('requests').setLevel(logging.CRITICAL) logger = logging.getLogger(__name__) def main(): ts = time() client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() links = get_links(client_id) download = partial(download_link, download_dir) with Pool(4) as p: p.map(download, links) logging.info('Took %s seconds', time() - ts) if __name__ == '__main__': main()

Konkurensi dan Paralelisme dalam Python Contoh 3: Mendistribusikan ke Banyak Pekerja

Meskipun modul threading dan multiprocessing sangat bagus untuk skrip yang berjalan di komputer pribadi Anda, apa yang harus Anda lakukan jika Anda ingin pekerjaan dilakukan pada mesin yang berbeda, atau Anda perlu meningkatkan lebih dari yang dapat dilakukan CPU pada satu mesin? menangani? Kasus penggunaan yang bagus untuk ini adalah tugas back-end yang berjalan lama untuk aplikasi web. Jika Anda memiliki beberapa tugas yang berjalan lama, Anda tidak ingin menjalankan sekelompok sub-proses atau utas pada mesin yang sama yang perlu menjalankan sisa kode aplikasi Anda. Ini akan menurunkan kinerja aplikasi Anda untuk semua pengguna Anda. Apa yang hebat adalah dapat menjalankan pekerjaan ini di mesin lain, atau banyak mesin lain.

Pustaka Python yang bagus untuk tugas ini adalah RQ, pustaka yang sangat sederhana namun kuat. Anda terlebih dahulu mengantrekan fungsi dan argumennya menggunakan perpustakaan. Ini mengambil representasi panggilan fungsi, yang kemudian ditambahkan ke daftar Redis. Mengantrekan pekerjaan adalah langkah pertama, tetapi belum akan melakukan apa pun. Kami juga membutuhkan setidaknya satu pekerja untuk mendengarkan antrian pekerjaan itu.

Model perpustakaan antrian RQ Python

Langkah pertama adalah menginstal dan menjalankan server Redis di komputer Anda, atau memiliki akses ke server Redis yang sedang berjalan. Setelah itu, hanya ada beberapa perubahan kecil yang dilakukan pada kode yang ada. Pertama-tama kita membuat instance RQ Queue dan meneruskannya ke instance server Redis dari pustaka redis-py. Kemudian, alih-alih hanya memanggil metode download_link kami, kami memanggil q.enqueue(download_link, download_dir, link) . Metode enqueue mengambil fungsi sebagai argumen pertamanya, lalu argumen lain atau argumen kata kunci diteruskan ke fungsi itu saat pekerjaan benar-benar dijalankan.

Satu langkah terakhir yang perlu kita lakukan adalah memulai beberapa pekerja. RQ menyediakan skrip praktis untuk menjalankan pekerja pada antrian default. Jalankan saja rqworker di jendela terminal dan itu akan memulai pekerja yang mendengarkan pada antrian default. Harap pastikan direktori kerja Anda saat ini sama dengan tempat skrip berada. Jika Anda ingin mendengarkan antrian yang berbeda, Anda dapat menjalankan rqworker queue_name dan itu akan mendengarkan antrian bernama itu. Hal hebat tentang RQ adalah selama Anda dapat terhubung ke Redis, Anda dapat menjalankan pekerja sebanyak yang Anda suka di mesin yang berbeda sebanyak yang Anda suka; oleh karena itu, sangat mudah untuk meningkatkannya seiring dengan berkembangnya aplikasi Anda. Berikut adalah sumber untuk versi RQ:

 import logging import os from redis import Redis from rq import Queue from download import setup_download_dir, get_links, download_link logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logging.getLogger('requests').setLevel(logging.CRITICAL) logger = logging.getLogger(__name__) def main(): client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() links = get_links(client_id) q = Queue(connection=Redis(host='localhost', port=6379)) for link in links: q.enqueue(download_link, download_dir, link) if __name__ == '__main__': main()

Namun, RQ bukan satu-satunya solusi antrian pekerjaan Python. RQ mudah digunakan dan mencakup kasus penggunaan sederhana dengan sangat baik, tetapi jika opsi lebih lanjut diperlukan, solusi antrian Python 3 lainnya (seperti Seledri) dapat digunakan.

Python Multithreading vs. Multiprocessing

Jika kode Anda terikat IO, multiprocessing dan multithreading dengan Python akan bekerja untuk Anda. Multiprocessing lebih mudah untuk mampir daripada threading tetapi memiliki overhead memori yang lebih tinggi. Jika kode Anda terikat CPU, multiprosesor kemungkinan besar akan menjadi pilihan yang lebih baik—terutama jika mesin target memiliki banyak inti atau CPU. Untuk aplikasi web, dan ketika Anda perlu menskalakan pekerjaan di beberapa mesin, RQ akan lebih baik untuk Anda.

Terkait: Menjadi Lebih Maju: Hindari 10 Kesalahan Paling Umum yang Dilakukan Pemrogram Python

Memperbarui

Python concurrent.futures

Sesuatu yang baru sejak Python 3.2 yang tidak disinggung dalam artikel asli adalah paket concurrent.futures . Paket ini menyediakan cara lain untuk menggunakan konkurensi dan paralelisme dengan Python.

Dalam artikel asli, saya menyebutkan bahwa modul multiprosesor Python akan lebih mudah dimasukkan ke dalam kode yang ada daripada modul threading. Ini karena modul threading Python 3 memerlukan subkelas kelas Thread dan juga membuat Queue agar thread dapat dipantau agar berfungsi.

Menggunakan konkuren.futures.ThreadPoolExecutor membuat kode contoh threading Python hampir identik dengan modul multiprosesor.

 import logging import os from concurrent.futures import ThreadPoolExecutor from functools import partial from time import time from download import setup_download_dir, get_links, download_link logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) def main(): client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() links = get_links(client_id) # By placing the executor inside a with block, the executors shutdown method # will be called cleaning up threads. # # By default, the executor sets number of workers to 5 times the number of # CPUs. with ThreadPoolExecutor() as executor: # Create a new partially applied function that stores the directory # argument. # # This allows the download_link function that normally takes two # arguments to work with the map function that expects a function of a # single argument. fn = partial(download_link, download_dir) # Executes fn concurrently using threads on the links iterable. The # timeout is for the entire process, not a single call, so downloading # all images must complete within 30 seconds. executor.map(fn, links, timeout=30) if __name__ == '__main__': main()

Sekarang setelah semua gambar ini diunduh dengan Python ThreadPoolExecutor kami, kami dapat menggunakannya untuk menguji tugas terikat CPU. Kami dapat membuat versi thumbnail dari semua gambar dalam skrip single-threaded, single-process dan kemudian menguji solusi berbasis multiprocessing.

Kita akan menggunakan perpustakaan Bantal untuk menangani pengubahan ukuran gambar.

Berikut adalah skrip awal kami.

 import logging from pathlib import Path from time import time from PIL import Image logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) def create_thumbnail(size, path): """ Creates a thumbnail of an image with the same name as image but with _thumbnail appended before the extension. Eg: >>> create_thumbnail((128, 128), 'image.jpg') A new thumbnail image is created with the name image_thumbnail.jpg :param size: A tuple of the width and height of the image :param path: The path to the image file :return: None """ image = Image.open(path) image.thumbnail(size) path = Path(path) name = path.stem + '_thumbnail' + path.suffix thumbnail_path = path.with_name(name) image.save(thumbnail_path) def main(): ts = time() for image_path in Path('images').iterdir(): create_thumbnail((128, 128), image_path) logging.info('Took %s', time() - ts) if __name__ == '__main__': main()

Skrip ini mengulangi jalur di folder images dan untuk setiap jalur menjalankan fungsi create_thumbnail. Fungsi ini menggunakan Pillow untuk membuka gambar, membuat thumbnail, dan menyimpan gambar baru yang lebih kecil dengan nama yang sama seperti aslinya tetapi dengan _thumbnail ditambahkan ke namanya.

Menjalankan skrip ini pada 160 gambar dengan total 36 juta membutuhkan waktu 2,32 detik. Mari kita lihat apakah kita dapat mempercepat ini menggunakan ProcessPoolExecutor.

 import logging from pathlib import Path from time import time from functools import partial from concurrent.futures import ProcessPoolExecutor from PIL import Image logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) def create_thumbnail(size, path): """ Creates a thumbnail of an image with the same name as image but with _thumbnail appended before the extension. Eg: >>> create_thumbnail((128, 128), 'image.jpg') A new thumbnail image is created with the name image_thumbnail.jpg :param size: A tuple of the width and height of the image :param path: The path to the image file :return: None """ path = Path(path) name = path.stem + '_thumbnail' + path.suffix thumbnail_path = path.with_name(name) image = Image.open(path) image.thumbnail(size) image.save(thumbnail_path) def main(): ts = time() # Partially apply the create_thumbnail method, setting the size to 128x128 # and returning a function of a single argument. thumbnail_128 = partial(create_thumbnail, (128, 128)) # Create the executor in a with block so shutdown is called when the block # is exited. with ProcessPoolExecutor() as executor: executor.map(thumbnail_128, Path('images').iterdir()) logging.info('Took %s', time() - ts) if __name__ == '__main__': main()

Metode create_thumbnail identik dengan skrip terakhir. Perbedaan utama adalah pembuatan ProcessPoolExecutor . Metode peta pelaksana digunakan untuk membuat thumbnail secara paralel. Secara default, ProcessPoolExecutor membuat satu subproses per CPU. Menjalankan skrip ini pada 160 gambar yang sama membutuhkan waktu 1,05 detik—2,2 kali lebih cepat!

Async/Menunggu (khusus Python 3.5+)

Salah satu item yang paling banyak diminta dalam komentar pada artikel asli adalah untuk contoh menggunakan modul asyncio Python 3. Dibandingkan dengan contoh lain, ada beberapa sintaks Python baru yang mungkin baru bagi kebanyakan orang dan juga beberapa konsep baru. Lapisan kompleksitas tambahan yang disayangkan disebabkan oleh modul urllib Python tidak menjadi asinkron. Kita perlu menggunakan pustaka HTTP async untuk mendapatkan manfaat penuh dari asyncio. Untuk ini, kita akan menggunakan aiohttp.

Mari langsung masuk ke kode dan penjelasan yang lebih rinci akan menyusul.

 import asyncio import logging import os from time import time import aiohttp from download import setup_download_dir, get_links logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) async def async_download_link(session, directory, link): """ Async version of the download_link method we've been using in the other examples. :param session: aiohttp ClientSession :param directory: directory to save downloads :param link: the url of the link to download :return: """ download_path = directory / os.path.basename(link) async with session.get(link) as response: with download_path.open('wb') as f: while True: # await pauses execution until the 1024 (or less) bytes are read from the stream chunk = await response.content.read(1024) if not chunk: # We are done reading the file, break out of the while loop break f.write(chunk) logger.info('Downloaded %s', link) # Main is now a coroutine async def main(): client_id = os.getenv('IMGUR_CLIENT_ID') if not client_id: raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!") download_dir = setup_download_dir() # We use a session to take advantage of tcp keep-alive # Set a 3 second read and connect timeout. Default is 5 minutes async with aiohttp.ClientSession(conn_timeout=3, read_timeout=3) as session: tasks = [(async_download_link(session, download_dir, l)) for l in get_links(client_id)] # gather aggregates all the tasks and schedules them in the event loop await asyncio.gather(*tasks, return_exceptions=True) if __name__ == '__main__': ts = time() # Create the asyncio event loop loop = asyncio.get_event_loop() try: loop.run_until_complete(main()) finally: # Shutdown the loop even if there is an exception loop.close() logger.info('Took %s seconds to complete', time() - ts)

Ada sedikit yang harus dibongkar di sini. Mari kita mulai dengan titik masuk utama program. Hal baru pertama yang kita lakukan dengan modul asyncio adalah mendapatkan loop acara. Loop peristiwa menangani semua kode asinkron. Kemudian, loop dijalankan hingga selesai dan melewati fungsi main . Ada sepotong sintaks baru dalam definisi main: async def . Anda juga akan melihat await dan with async .

Sintaks async/await diperkenalkan di PEP492. Sintaks async def menandai fungsi sebagai coroutine. Secara internal, coroutine didasarkan pada generator Python, tetapi tidak persis sama. Coroutine mengembalikan objek coroutine mirip dengan bagaimana generator mengembalikan objek generator. Setelah Anda memiliki coroutine, Anda mendapatkan hasilnya dengan ekspresi await . Ketika panggilan coroutine await , eksekusi coroutine ditangguhkan sampai waitable selesai. Penangguhan ini memungkinkan pekerjaan lain untuk diselesaikan sementara coroutine ditangguhkan "menunggu" beberapa hasil. Secara umum, hasil ini akan menjadi semacam I/O seperti permintaan database atau dalam kasus kami permintaan HTTP.

Fungsi download_link harus diubah cukup signifikan. Sebelumnya, kami mengandalkan urllib untuk melakukan tugas membaca gambar untuk kami. Sekarang, untuk memungkinkan metode kami bekerja dengan baik dengan paradigma pemrograman asinkron, kami telah memperkenalkan loop while yang membaca potongan gambar pada satu waktu dan menunda eksekusi sambil menunggu I/O selesai. Hal ini memungkinkan loop acara untuk mengulang melalui pengunduhan gambar yang berbeda karena masing-masing memiliki data baru yang tersedia selama pengunduhan.

Harus Ada Satu—Sebaiknya Hanya Satu—Cara yang Jelas untuk Melakukannya

Sementara zen dari Python memberitahu kita harus ada satu cara yang jelas untuk melakukan sesuatu, ada banyak cara di Python untuk memperkenalkan konkurensi ke dalam program kita. Metode terbaik untuk dipilih akan bergantung pada kasus penggunaan spesifik Anda. Paradigma asinkron menskalakan lebih baik untuk beban kerja konkurensi tinggi (seperti server web) dibandingkan dengan threading atau multiprosesor, tetapi memerlukan kode Anda (dan dependensi) untuk menjadi asinkron untuk mendapatkan manfaat sepenuhnya.

Semoga contoh threading Python dalam artikel ini—dan pembaruan—akan mengarahkan Anda ke arah yang benar sehingga Anda memiliki gagasan tentang di mana mencarinya di pustaka standar Python jika Anda perlu memperkenalkan konkurensi ke dalam program Anda.