Python adalah salah satu bahasa yang paling populer untuk pemrosesan data dan ilmu data secara umum. Ekosistem menyediakan banyak pustaka dan kerangka kerja yang memfasilitasi komputasi berkinerja tinggi. Melakukan pemrograman paralel dengan Python dapat membuktikan cukup rumit. Show Dalam tutorial ini, kita akan mempelajari mengapa paralelisme sulit terutama dalam konteks Python, dan untuk itu, kita akan membahas hal-hal berikut:
Global Interpreter LockGlobal Interpreter Lock (GIL) adalah salah satu mata pelajaran paling kontroversial di dunia Python. Dalam CPython, implementasi Python yang paling populer, GIL adalah mutex yang membuat segala sesuatunya aman. GIL memudahkan untuk mengintegrasikan dengan pustaka eksternal yang tidak aman-thread, dan itu membuat kode non-paralel menjadi lebih cepat. Namun, ini membutuhkan biaya. Karena GIL, kami tidak dapat mencapai paralelisme sejati melalui multithreading. Pada dasarnya, dua utas asli yang berbeda dari proses yang sama tidak dapat menjalankan kode Python sekaligus. Hal-hal tidak seburuk itu, dan inilah alasannya: hal-hal yang terjadi di luar wilayah GIL bebas untuk sejajar. Dalam kategori ini jatuh tugas yang berjalan lama seperti I/O dan, untungnya, perpustakaan seperti Threads vs. ProcessesJadi Python tidak benar-benar multithread. Tapi apa itu trhread? Mari kita mundur selangkah dan melihat hal-hal dalam perspektif. Proses adalah abstraksi sistem operasi dasar. Ini adalah program yang sedang dieksekusi—dengan kata lain, kode yang sedang berjalan. Beberapa proses selalu berjalan di komputer, dan mereka mengeksekusi secara paralel. Suatu proses dapat memiliki beberapa utas. Mereka mengeksekusi kode yang sama milik proses induk. Idealnya, mereka berjalan secara paralel, tetapi tidak harus. Alasan mengapa proses tidak cukup adalah karena aplikasi harus responsif dan mendengarkan tindakan pengguna saat memperbarui tampilan dan menyimpan file. Jika itu masih agak tidak jelas, berikut ini cheatsheet:
Tidak ada satu resep pun yang mengakomodasi semuanya. Memilih satu sangat tergantung pada konteks dan tugas yang Anda coba capai. Paralel vs. KonkurensiSekarang kita akan melangkah lebih jauh dan menyelami konkurensi. Concurrency sering disalahpahami dan disalahartikan sebagai paralelisme. Bukan itu masalahnya. Concurrency mengimplikasikan penjadwalan kode independen untuk dieksekusi secara kooperatif. Ambil keuntungan dari fakta bahwa sepotong kode menunggu pada operasi I/O, dan selama waktu itu menjalankan bagian kode yang berbeda tetapi independen. Dengan Python, kita dapat mencapai perilaku konkuren ringan melalui greenlets. Dari perspektif paralelisasi, menggunakan benang atau greenlet setara karena keduanya tidak berjalan paralel. Greenlets bahkan lebih murah untuk dibuat daripada utas. Karena itu, greenlets banyak digunakan untuk melakukan sejumlah besar tugas I/O sederhana, seperti yang biasanya ditemukan di jaringan dan server web. Sekarang kita mengetahui perbedaan antara benang dan proses, paralel dan bersamaan, kita dapat mengilustrasikan bagaimana tugas yang berbeda dilakukan pada dua paradigma. Inilah yang akan kita lakukan: kita akan berlari, beberapa kali, tugas di luar GIL dan satu di dalamnya. Kami menjalankannya secara serial, menggunakan utas dan menggunakan proses. Mari tentukan tugasnya: import os import time import threading import multiprocessing NUM_WORKERS = 4 def only_sleep(): """ Do nothing, wait for a timer to expire """ print("PID: %s, Process Name: %s, Thread Name: %s" % ( os.getpid(), multiprocessing.current_process().name, threading.current_thread().name) ) time.sleep(1) def crunch_numbers(): """ Do some computations """ print("PID: %s, Process Name: %s, Thread Name: %s" % ( os.getpid(), multiprocessing.current_process().name, threading.current_thread().name) ) x = 0 while x < 10000000: x += 1 Kami telah membuat dua tugas. Keduanya lama berjalan, tetapi hanya ## Run tasks serially start_time = time.time() for _ in range(NUM_WORKERS): only_sleep() end_time = time.time() print("Serial time=", end_time - start_time) # Run tasks using threads start_time = time.time() threads = [threading.Thread(target=only_sleep) for _ in range(NUM_WORKERS)] [thread.start() for thread in threads] [thread.join() for thread in threads] end_time = time.time() print("Threads time=", end_time - start_time) # Run tasks using processes start_time = time.time() processes = [multiprocessing.Process(target=only_sleep()) for _ in range(NUM_WORKERS)] [process.start() for process in processes] [process.join() for process in processes] end_time = time.time() print("Parallel time=", end_time - start_time) Inilah keluaran yang saya miliki (milik Anda harus serupa, meskipun PID dan waktu akan sedikit berbeda): PID: 95726, Process Name: MainProcess, Thread Name: MainThread PID: 95726, Process Name: MainProcess, Thread Name: MainThread PID: 95726, Process Name: MainProcess, Thread Name: MainThread PID: 95726, Process Name: MainProcess, Thread Name: MainThread Serial time= 4.018089056015015 PID: 95726, Process Name: MainProcess, Thread Name: Thread-1 PID: 95726, Process Name: MainProcess, Thread Name: Thread-2 PID: 95726, Process Name: MainProcess, Thread Name: Thread-3 PID: 95726, Process Name: MainProcess, Thread Name: Thread-4 Threads time= 1.0047411918640137 PID: 95728, Process Name: Process-1, Thread Name: MainThread PID: 95729, Process Name: Process-2, Thread Name: MainThread PID: 95730, Process Name: Process-3, Thread Name: MainThread PID: 95731, Process Name: Process-4, Thread Name: MainThread Parallel time= 1.014023780822754 Berikut beberapa pengamatan:
Mari kita lakukan rutinitas yang sama tapi kali ini menjalankan tugas start_time = time.time() for _ in range(NUM_WORKERS): crunch_numbers() end_time = time.time() print("Serial time=", end_time - start_time) start_time = time.time() threads = [threading.Thread(target=crunch_numbers) for _ in range(NUM_WORKERS)] [thread.start() for thread in threads] [thread.join() for thread in threads] end_time = time.time() print("Threads time=", end_time - start_time) start_time = time.time() processes = [multiprocessing.Process(target=crunch_numbers) for _ in range(NUM_WORKERS)] [process.start() for process in processes] [process.join() for process in processes] end_time = time.time() print("Parallel time=", end_time - start_time) Inilah output yang saya dapatkan: PID: 96285, Process Name: MainProcess, Thread Name: MainThread PID: 96285, Process Name: MainProcess, Thread Name: MainThread PID: 96285, Process Name: MainProcess, Thread Name: MainThread PID: 96285, Process Name: MainProcess, Thread Name: MainThread Serial time= 2.705625057220459 PID: 96285, Process Name: MainProcess, Thread Name: Thread-1 PID: 96285, Process Name: MainProcess, Thread Name: Thread-2 PID: 96285, Process Name: MainProcess, Thread Name: Thread-3 PID: 96285, Process Name: MainProcess, Thread Name: Thread-4 Threads time= 2.6961309909820557 PID: 96289, Process Name: Process-1, Thread Name: MainThread PID: 96290, Process Name: Process-2, Thread Name: MainThread PID: 96291, Process Name: Process-3, Thread Name: MainThread PID: 96292, Process Name: Process-4, Thread Name: MainThread Parallel time= 0.8014059066772461 Perbedaan utama di sini adalah hasil dari pendekatan multithread. Kali ini performanya sangat mirip dengan pendekatan serial, dan inilah alasannya: karena ia melakukan perhitungan dan Python tidak melakukan paralelisme yang nyata, thread pada dasarnya berjalan satu demi satu, menghasilkan eksekusi satu sama lain sampai semuanya selesai. Ekosistem Pemrograman Paralel/Konkurensi PythonPython memiliki API yang kaya untuk melakukan pemrograman paralel/konkurensi. Dalam tutorial ini kami membahas yang paling populer, tetapi Anda harus tahu bahwa untuk setiap kebutuhan yang Anda miliki di domain ini, mungkin ada sesuatu yang sudah ada di luar sana yang dapat membantu Anda mencapai tujuan Anda. Di bagian selanjutnya, kami akan membangun aplikasi praktis dalam berbagai bentuk, menggunakan semua perpustakaan yang disajikan. Tanpa basa-basi lagi, berikut adalah modul/pustaka yang akan kami bahas:
Membangun Aplikasi PraktisMengetahui teori itu bagus dan bagus, tetapi cara terbaik untuk belajar adalah membangun sesuatu yang praktis, bukan? Pada bagian ini, kita akan membangun jenis aplikasi klasik melalui semua paradigma yang berbeda. Mari membangun aplikasi yang memeriksa uptime situs web. Ada banyak solusi di luar sana, yang paling terkenal mungkin adalah Jetpack Monitor dan Uptime Robot. Tujuan dari aplikasi ini adalah untuk memberi tahu Anda ketika situs web Anda tidak aktif sehingga Anda dapat mengambil tindakan dengan cepat. Begini cara kerjanya:
Inilah mengapa penting untuk mengambil pendekatan paralel/konkurensi terhadap masalah. Ketika daftar situs web tumbuh, melalui daftar secara serial tidak akan menjamin kita bahwa setiap situs web diperiksa setiap lima menit atau lebih. Situs web bisa turun selama berjam-jam, dan pemiliknya tidak akan diberi tahu. Mari kita mulai dengan menulis beberapa utilitas: # utils.py import time import logging import requests class WebsiteDownException(Exception): pass def ping_website(address, timeout=20): """ Check if a website is down. A website is considered down if either the status_code >= 400 or if the timeout expires Throw a WebsiteDownException if any of the website down conditions are met """ try: response = requests.head(address, timeout=timeout) if response.status_code >= 400: logging.warning("Website %s returned status_code=%s" % (address, response.status_code)) raise WebsiteDownException() except requests.exceptions.RequestException: logging.warning("Timeout expired for website %s" % address) raise WebsiteDownException() def notify_owner(address): """ Send the owner of the address a notification that their website is down For now, we're just going to sleep for 0.5 seconds but this is where you would send an email, push notification or text-message """ logging.info("Notifying the owner of %s website" % address) time.sleep(0.5) def check_website(address): """ Utility function: check if a website is down, if so, notify the user """ try: ping_website(address) except WebsiteDownException: notify_owner(address) Kami sebenarnya membutuhkan daftar situs web untuk mencoba sistem kami. Buat daftar Anda sendiri atau gunakan milik saya: # websites.py WEBSITE_LIST = [ 'https://envato.com', 'http://amazon.co.uk', 'http://amazon.com', 'http://facebook.com', 'http://google.com', 'http://google.fr', 'http://google.es', 'http://google.co.uk', 'http://internet.org', 'http://gmail.com', 'http://stackoverflow.com', 'http://github.com', 'http://heroku.com', 'http://really-cool-available-domain.com', 'http://djangoproject.com', 'http://rubyonrails.org', 'http://basecamp.com', 'http://trello.com', 'http://yiiframework.com', 'http://shopify.com', 'http://another-really-interesting-domain.co', 'http://airbnb.com', 'http://instagram.com', 'http://snapchat.com', 'http://youtube.com', 'http://baidu.com', 'http://yahoo.com', 'http://live.com', 'http://linkedin.com', 'http://yandex.ru', 'http://netflix.com', 'http://wordpress.com', 'http://bing.com', ] Biasanya, Anda menyimpan daftar ini dalam database bersama dengan informasi kontak pemilik sehingga Anda dapat menghubungi mereka. Karena ini bukan topik utama dari tutorial ini, dan demi kesederhanaan, kami hanya akan menggunakan daftar Python ini. Jika Anda membayar perhatian yang sangat baik, Anda mungkin telah memperhatikan dua domain yang sangat panjang dalam daftar yang bukan situs web yang valid (saya harap tidak ada yang membelinya pada saat Anda membaca ini untuk membuktikan bahwa saya salah!). Saya menambahkan dua domain ini untuk memastikan bahwa kami memiliki beberapa situs web di setiap run. Juga, beri nama aplikasi kami UptimeSquirrel. Pendekatan SerialPertama, mari coba pendekatan serial dan lihat seberapa buruk kinerjanya. Kami akan mempertimbangkan ini sebagai baseline. # serial_squirrel.py import time start_time = time.time() for address in WEBSITE_LIST: check_website(address) end_time = time.time() print("Time for SerialSquirrel: %ssecs" % (end_time - start_time)) # WARNING:root:Timeout expired for website http://really-cool-available-domain.com # WARNING:root:Timeout expired for website http://another-really-interesting-domain.co # WARNING:root:Website http://bing.com returned status_code=405 # Time for SerialSquirrel: 15.881232261657715secs Pendekatan ThreadingKita akan menjadi sedikit lebih kreatif dengan penerapan pendekatan berulir. Kami menggunakan antrean untuk memasukkan alamat dan membuat threads pekerja agar mereka keluar dari antrean dan memprosesnya. Kami akan menunggu antrean kosong, artinya semua alamat telah diproses oleh pekerja kami. # threaded_squirrel.py import time from queue import Queue from threading import Thread NUM_WORKERS = 4 task_queue = Queue() def worker(): # Constantly check the queue for addresses while True: address = task_queue.get() check_website(address) # Mark the processed task as done task_queue.task_done() start_time = time.time() # Create the worker threads threads = [Thread(target=worker) for _ in range(NUM_WORKERS)] # Add the websites to the task queue [task_queue.put(item) for item in WEBSITE_LIST] # Start all the workers [thread.start() for thread in threads] # Wait for all the tasks in the queue to be processed task_queue.join() end_time = time.time() print("Time for ThreadedSquirrel: %ssecs" % (end_time - start_time)) # WARNING:root:Timeout expired for website http://really-cool-available-domain.com # WARNING:root:Timeout expired for website http://another-really-interesting-domain.co # WARNING:root:Website http://bing.com returned status_code=405 # Time for ThreadedSquirrel: 3.110753059387207secs concurrent.futuresSeperti yang dinyatakan sebelumnya, # future_squirrel.py import time import concurrent.futures NUM_WORKERS = 4 start_time = time.time() with concurrent.futures.ThreadPoolExecutor(max_workers=NUM_WORKERS) as executor: futures = {executor.submit(check_website, address) for address in WEBSITE_LIST} concurrent.futures.wait(futures) end_time = time.time() print("Time for FutureSquirrel: %ssecs" % (end_time - start_time)) # WARNING:root:Timeout expired for website http://really-cool-available-domain.com # WARNING:root:Timeout expired for website http://another-really-interesting-domain.co # WARNING:root:Website http://bing.com returned status_code=405 # Time for FutureSquirrel: 1.812899112701416secs Pendekatan MultiprocessingPustaka # multiprocessing_squirrel.py import time import socket import multiprocessing NUM_WORKERS = 4 start_time = time.time() with multiprocessing.Pool(processes=NUM_WORKERS) as pool: results = pool.map_async(check_website, WEBSITE_LIST) results.wait() end_time = time.time() print("Time for MultiProcessingSquirrel: %ssecs" % (end_time - start_time)) # WARNING:root:Timeout expired for website http://really-cool-available-domain.com # WARNING:root:Timeout expired for website http://another-really-interesting-domain.co # WARNING:root:Website http://bing.com returned status_code=405 # Time for MultiProcessingSquirrel: 2.8224599361419678secs GeventGevent adalah alternatif yang populer untuk mencapai konkurensi besar. Ada beberapa hal yang perlu Anda ketahui sebelum menggunakannya:
Untuk menginstal gevent, jalankan: Berikut adalah
cara untuk menggunakan gevent untuk melakukan tugas kita menggunakan # green_squirrel.py import time from gevent.pool import Pool from gevent import monkey # Note that you can spawn many workers with gevent since the cost of creating and switching is very low NUM_WORKERS = 4 # Monkey-Patch socket module for HTTP requests monkey.patch_socket() start_time = time.time() pool = Pool(NUM_WORKERS) for address in WEBSITE_LIST: pool.spawn(check_website, address) # Wait for stuff to finish pool.join() end_time = time.time() print("Time for GreenSquirrel: %ssecs" % (end_time - start_time)) # Time for GreenSquirrel: 3.8395519256591797secs CeleryCelery adalah pendekatan yang sebagian besar berbeda dari apa yang telah kita lihat sejauh ini. Ini adalah pertempuran yang diuji dalam konteks lingkungan yang sangat kompleks dan berkinerja tinggi. Menyiapkan Celery akan membutuhkan sedikit lebih mengutak-atik daripada semua solusi di atas. Pertama, kita perlu menginstal Celery:
Tugas adalah konsep sentral dalam proyek Celery. Segala sesuatu yang Anda ingin jalankan di dalam Celery perlu menjadi tugas. Celery menawarkan fleksibilitas luar biasa untuk menjalankan tugas: Anda dapat menjalankannya secara sinkron atau asinkron, real-time atau terjadwal, pada mesin yang sama atau pada beberapa mesin, dan menggunakan threads, proses, Eventlet, atau gevent. Pengaturannya akan sedikit lebih rumit. Celery menggunakan layanan lain untuk mengirim dan menerima pesan. Pesan-pesan ini biasanya tugas atau hasil dari tugas. Kami akan menggunakan Redis dalam tutorial ini untuk tujuan ini. Redis adalah pilihan yang bagus karena sangat mudah untuk menginstal dan mengkonfigurasi, dan itu benar-benar mungkin Anda sudah menggunakannya dalam aplikasi Anda untuk keperluan lain, seperti caching dan pub/sub. Anda dapat menginstal Redis dengan mengikuti petunjuk pada halaman Redis Quick Start. Jangan lupa untuk menginstal Mulai
server Redis seperti ini: Untuk mulai membuat barang dengan Celery, pertama-tama kita perlu membuat aplikasi Celery. Setelah itu, Celery perlu mengetahui jenis tugas yang mungkin dilakukan. Untuk mencapai itu, kita perlu mendaftarkan tugas ke aplikasi Seledri. Kami akan melakukan ini menggunakan # celery_squirrel.py import time from utils import check_website from data import WEBSITE_LIST from celery import Celery from celery.result import ResultSet app = Celery('celery_squirrel', broker='redis://localhost:6379/0', backend='redis://localhost:6379/0') @app.task def check_website_task(address): return check_website(address) if __name__ == "__main__": start_time = time.time() # Using `delay` runs the task async rs = ResultSet([check_website_task.delay(address) for address in WEBSITE_LIST]) # Wait for the tasks to finish rs.get() end_time = time.time() print("CelerySquirrel:", end_time - start_time) # CelerySquirrel: 2.4979639053344727 Jangan panik jika tidak ada yang terjadi. Ingat, Celery adalah layanan, dan kita harus menjalankannya. Sampai sekarang, kami hanya menempatkan tugas di Redis tetapi tidak memulai Seledri untuk mengeksekusi mereka. Untuk melakukan itu, kita perlu menjalankan perintah ini di folder tempat kode kita berada:
Sekarang jalankan kembali skrip Python dan lihat apa yang terjadi. Satu hal yang perlu diperhatikan: perhatikan bagaimana kami mengirimkan alamat Redis ke aplikasi Redis kami dua kali. Parameter Selain itu, ketahuilah bahwa log sekarang berada dalam output standar proses Celery, jadi pastikan untuk memeriksanya di terminal yang sesuai. KesimpulanSaya harap ini telah menjadi perjalanan yang menarik bagi Anda dan pengenalan yang baik ke dunia pemrograman paralel/konkurensi dengan Python. Ini adalah akhir dari perjalanan, dan ada beberapa kesimpulan yang bisa kami tarik:
|