Python Çoklu Okuma ve Çoklu İşlem Eğitimi
Yayınlanan: 2022-03-11Python'u eleştiren tartışmalar, genellikle Python kodunun birden çok iş parçacığının aynı anda çalışmasını engelleyen genel yorumlayıcı kilidi (sevgiyle GIL olarak anılır) olarak bilinen şeyi işaret ederek, Python'u çok iş parçacıklı çalışma için kullanmanın ne kadar zor olduğu hakkında konuşur. Bu nedenle, Python geliştiricisi değilseniz ve C++ veya Java gibi diğer dillerden geliyorsanız, Python çoklu okuma modülü beklediğiniz gibi davranmaz. Açıkça belirtilmelidir ki, Python'da aynı anda veya paralel olarak çalışan ve belirli şeyler göz önünde bulundurulduğu sürece sonuçta ortaya çıkan performansta büyük bir fark yaratan kod yazılabilir. Henüz okumadıysanız, Toptal Engineering Blog'da Eqbal Kuran'ın Ruby'deki eşzamanlılık ve paralellik hakkındaki makalesine bir göz atmanızı öneririm.
Bu Python eşzamanlılık eğitiminde, Imgur'dan en popüler görüntüleri indirmek için küçük bir Python betiği yazacağız. Görüntüleri sırayla veya birer birer indiren bir sürümle başlayacağız. Ön koşul olarak, Imgur'a bir başvuru kaydetmeniz gerekecektir. Halihazırda bir Imgur hesabınız yoksa, lütfen önce bir tane oluşturun.
Bu iş parçacığı örneklerindeki komut dosyaları Python 3.6.4 ile test edilmiştir. Bazı değişikliklerle birlikte, Python 2 ile de çalıştırılmalıdırlar—urllib, Python'un bu iki sürümü arasında en çok değişen şeydir.
Python Çoklu İş Parçacığına Başlarken
download.py
adında bir Python modülü oluşturarak başlayalım. Bu dosya, görüntülerin listesini almak ve indirmek için gerekli tüm işlevleri içerecektir. Bu işlevleri üç ayrı işleve ayıracağız:
-
get_links
-
download_link
-
setup_download_dir
Üçüncü işlev, setup_download_dir
, zaten mevcut değilse, bir indirme hedef dizini oluşturmak için kullanılacaktır.
Imgur'un API'si, istemci kimliğiyle Authorization
başlığını taşımak için HTTP isteklerini gerektirir. Bu müşteri kimliğini Imgur'a kaydettiğiniz uygulamanın panosundan bulabilirsiniz ve yanıt JSON kodlu olacaktır. Kodunu çözmek için Python'un standart JSON kitaplığını kullanabiliriz. Görüntüyü indirmek daha da basit bir iştir, çünkü tek yapmanız gereken görüntüyü URL'sinden alıp bir dosyaya yazmaktır.
Komut dosyası şöyle görünür:
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
Ardından, görüntüleri tek tek indirmek için bu işlevleri kullanacak bir modül yazmamız gerekecek. Bu single.py
adını vereceğiz. Bu, Imgur resim indiricisinin ilk, saf versiyonumuzun ana işlevini içerecektir. Modül, IMGUR_CLIENT_ID
ortam değişkenindeki Imgur istemci kimliğini alacaktır. İndirme hedef dizini oluşturmak için setup_download_dir
çağırır. Son olarak, get_links
işlevini kullanarak bir görüntü listesi getirecek, tüm GIF ve albüm URL'lerini filtreleyecek ve ardından bu görüntülerin her birini indirip diske kaydetmek için download_link
kullanacak. single.py
şöyle görünür:
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()
Dizüstü bilgisayarımda bu komut dosyasının 91 görüntüyü indirmesi 19.4 saniye sürdü. Lütfen bu numaraların bulunduğunuz ağa göre değişebileceğini unutmayın. 19.4 saniye çok uzun değil, ama ya daha fazla resim indirmek istersek? Belki 90 yerine 900 resim. Resim başına ortalama 0,2 saniye ile 900 resim yaklaşık 3 dakika sürer. 9000 fotoğraf için 30 dakika sürer. İyi haber şu ki, eşzamanlılık veya paralellik getirerek bunu önemli ölçüde hızlandırabiliriz.
Sonraki tüm kod örnekleri, yalnızca yeni ve bu örneklere özgü içe aktarma ifadelerini gösterecektir. Kolaylık sağlamak için bu Python komut dosyalarının tümü bu GitHub deposunda bulunabilir.
Python'da Eşzamanlılık ve Paralellik: İş Parçacığı Örneği
İş parçacığı oluşturma, Python eşzamanlılığına ve paralelliğine ulaşmak için en iyi bilinen yaklaşımlardan biridir. İş parçacığı oluşturma, genellikle işletim sistemi tarafından sağlanan bir özelliktir. İş parçacıkları işlemlerden daha hafiftir ve aynı bellek alanını paylaşır.
Bu Python iş parçacığı örneğinde, single.py
yerine yeni bir modül yazacağız. Bu modül, ana iş parçacığı dahil olmak üzere toplam dokuz iş parçacığı oluşturan sekiz iş parçacığından oluşan bir havuz oluşturacaktır. Sekiz çalışan iş parçacığı seçtim çünkü bilgisayarımda sekiz CPU çekirdeği var ve çekirdek başına bir çalışan iş parçacığı, aynı anda kaç iş parçacığının çalıştırılacağı konusunda iyi bir sayı gibi görünüyordu. Pratikte bu sayı, aynı makinede çalışan diğer uygulamalar ve hizmetler gibi diğer faktörlere bağlı olarak çok daha dikkatli seçilir.
Bu, Python Thread
sınıfının bir alt öğesi olan DownloadWorker
adlı yeni bir sınıfımız olması dışında, neredeyse öncekiyle aynıdır. Sonsuz bir döngü çalıştıran run yöntemi geçersiz kılındı. Her yinelemede, iş parçacığı güvenli bir kuyruğa bir URL getirmeye çalışmak için self.queue.get()
çağırır. Çalışanın işlemesi için kuyrukta bir öğe olana kadar engeller. Çalışan kuyruktan bir öğe aldığında, görüntüyü görüntüler dizinine indirmek için önceki komut dosyasında kullanılan aynı download_link
yöntemini çağırır. İndirme işlemi tamamlandıktan sonra çalışan, görevin tamamlandığını kuyruğa bildirir. Bu çok önemlidir, çünkü Kuyruk kaç görevin sıraya konduğunu takip eder. queue.join()
çağrısı, işçiler bir görevi tamamladıklarını bildirmediyse, ana iş parçacığını sonsuza kadar engeller.
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()
Bu Python iş parçacığı örneği komut dosyasını daha önce kullanılan aynı makinede çalıştırmak, 4,1 saniyelik bir indirme süresiyle sonuçlanır! Bu, önceki örneğe göre 4,7 kat daha hızlı. Bu çok daha hızlı olsa da, GIL nedeniyle bu süreç boyunca aynı anda yalnızca bir iş parçacığının yürütülmekte olduğunu belirtmekte fayda var. Bu nedenle, bu kod eşzamanlıdır ancak paralel değildir. Hala daha hızlı olmasının nedeni, bunun IO'ya bağlı bir görev olmasıdır. İşlemci bu görüntüleri indirirken neredeyse hiç ter atmıyor ve zamanın çoğu ağı beklemekle geçiyor. Bu nedenle Python çoklu okuma, büyük bir hız artışı sağlayabilir. İşlemci, bir iş yapmaya hazır olduğunda iş parçacıkları arasında geçiş yapabilir. Python'da veya GIL ile yorumlanan başka bir dilde iş parçacığı modülünü kullanmak, aslında performansın düşmesine neden olabilir. Kodunuz gzip dosyalarının sıkıştırılması gibi CPU'ya bağlı bir görev gerçekleştiriyorsa, threading
modülünü kullanmak daha yavaş yürütme süresine neden olur. CPU'ya bağlı görevler ve gerçekten paralel yürütme için çoklu işlem modülünü kullanabiliriz.
Fiili referans Python uygulaması (CPython) bir GIL'ye sahip olsa da, bu tüm Python uygulamaları için geçerli değildir. Örneğin, .NET çerçevesini kullanan bir Python uygulaması olan IronPython'da bir GIL yoktur ve Java tabanlı uygulama olan Jython da yoktur. Çalışan Python uygulamalarının bir listesini burada bulabilirsiniz.
Python'da Eşzamanlılık ve Paralellik Örnek 2: Birden Çok Süreç Oluşturma
Python iş parçacığı örneği gibi bir sınıf eklememiz gerekmediğinden, çoklu işlem modülünü eklemek iş parçacığı modülünden daha kolaydır. Yapmamız gereken tek değişiklik ana fonksiyonda.
Birden çok işlemi kullanmak için çok işlemli bir Pool
oluşturuyoruz. Sağladığı harita yöntemiyle, URL'lerin listesini havuza ileteceğiz, bu da sırayla sekiz yeni süreç oluşturacak ve her birini görüntüleri paralel olarak indirmek için kullanacak. Bu gerçek bir paralelliktir, ancak bir bedeli vardır. Komut dosyasının tüm belleği, oluşturulan her alt işleme kopyalanır. Bu basit örnekte, bu çok önemli değil, ancak önemsiz olmayan programlar için kolayca ciddi bir ek yük haline gelebilir.
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()
Python'da Eşzamanlılık ve Paralellik Örnek 3: Birden Çok Çalışana Dağıtma
İş parçacığı ve çoklu işlem modülleri, kişisel bilgisayarınızda çalışan komut dosyaları için harika olsa da, işin farklı bir makinede yapılmasını istiyorsanız veya bir makinedeki CPU'dan daha fazlasına kadar ölçeklendirmeniz gerekiyorsa ne yapmalısınız? üstesinden gelmek? Bunun için harika bir kullanım örneği, web uygulamaları için uzun süredir devam eden arka uç görevleridir. Uzun süredir devam eden bazı görevleriniz varsa, aynı makinede uygulama kodunuzun geri kalanını çalıştırması gereken bir grup alt işlemi veya iş parçacığını döndürmek istemezsiniz. Bu, tüm kullanıcılarınız için uygulamanızın performansını düşürür. Harika olan, bu işleri başka bir makinede veya başka birçok makinede çalıştırabilmektir.
Bu görev için harika bir Python kitaplığı, çok basit ama güçlü bir kitaplık olan RQ'dur. Önce kitaplığı kullanarak bir işlevi ve argümanlarını kuyruğa alırsınız. Bu, daha sonra bir Redis listesine eklenen işlev çağrısı temsilini seçer. İşi kuyruğa almak ilk adımdır, ancak henüz hiçbir şey yapmayacaktır. Ayrıca o iş kuyruğunu dinlemek için en az bir çalışana ihtiyacımız var.
İlk adım, bilgisayarınıza bir Redis sunucusu kurup çalıştırmak veya çalışan bir Redis sunucusuna erişim sağlamaktır. Bundan sonra, mevcut kodda yapılan sadece birkaç küçük değişiklik var. Önce bir RQ Kuyruğu örneği oluşturuyoruz ve ona redis-py kitaplığından bir Redis sunucusu örneği iletiyoruz. Ardından, download_link
yöntemimizi çağırmak yerine, q.enqueue(download_link, download_dir, link)
. Sıraya alma yöntemi, ilk argümanı olarak bir işlevi alır, ardından iş gerçekten yürütüldüğünde, diğer argümanlar veya anahtar kelime argümanları bu fonksiyona iletilir.

Yapmamız gereken son bir adım, bazı işçileri başlatmak. RQ, çalışanları varsayılan kuyrukta çalıştırmak için kullanışlı bir komut dosyası sağlar. Sadece bir terminal penceresinde rqworker
çalıştırın ve varsayılan kuyruğu dinleyen bir işçi başlatacaktır. Lütfen mevcut çalışma dizininizin betiklerin bulunduğu yerle aynı olduğundan emin olun. Farklı bir kuyruk dinlemek istiyorsanız, rqworker queue_name
çalıştırabilirsiniz ve o adlandırılmış kuyruğu dinleyecektir. RQ'nun harika yanı, Redis'e bağlanabildiğiniz sürece, istediğiniz kadar farklı makinede istediğiniz kadar işçi çalıştırabilmenizdir; bu nedenle, uygulamanız büyüdükçe ölçeği büyütmek çok kolaydır. İşte RQ sürümünün kaynağı:
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()
Ancak, RQ tek Python iş kuyruğu çözümü değildir. RQ'nun kullanımı kolaydır ve basit kullanım durumlarını son derece iyi kapsar, ancak daha gelişmiş seçenekler gerekiyorsa, diğer Python 3 kuyruk çözümleri (Kereviz gibi) kullanılabilir.
Python Çoklu İş Parçacığı ve Çoklu İşleme
Kodunuz IO'ya bağlıysa, Python'da hem çoklu işlem hem de çoklu kullanım sizin için çalışacaktır. Çoklu işlem, iş parçacığı oluşturmaktan daha kolaydır, ancak daha yüksek bir bellek yüküne sahiptir. Kodunuz CPU'ya bağlıysa, özellikle hedef makinenin birden fazla çekirdeği veya CPU'su varsa, çoklu işlem büyük olasılıkla daha iyi bir seçim olacaktır. Web uygulamaları için ve işi birden fazla makinede ölçeklendirmeniz gerektiğinde, RQ sizin için daha iyi olacaktır.
Güncelleme
Python concurrent.futures
. vadeli işlemler
Python 3.2'den bu yana orijinal makalede değinilmeyen yeni bir şey concurrent.futures
paketidir. Bu paket, Python ile eşzamanlılık ve paralellik kullanmanın başka bir yolunu sunar.
Orijinal makalede Python'un çoklu işlem modülünün mevcut koda atılmasının iş parçacığı modülünden daha kolay olacağını belirtmiştim. Bunun nedeni, Python 3 iş parçacığı modülünün, Thread
sınıfının alt sınıflanmasını ve aynı zamanda iş için izleyecek iş parçacıkları için bir Queue
oluşturmayı gerektirmesiydi.
Bir concurrent.futures.ThreadPoolExecutor kullanmak, Python iş parçacığı oluşturma örnek kodunu çoklu işlem modülüyle neredeyse aynı hale getirir.
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()
Python ThreadPoolExecutor
ile tüm bu görüntüleri indirdiğimize göre, bunları CPU'ya bağlı bir görevi test etmek için kullanabiliriz. Hem tek iş parçacıklı, tek işlemli bir komut dosyasında tüm görüntülerin küçük resim sürümlerini oluşturabilir ve ardından çok işlem tabanlı bir çözümü test edebiliriz.
Görüntülerin yeniden boyutlandırılmasını işlemek için Pillow kitaplığını kullanacağız.
İşte ilk senaryomuz.
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()
Bu komut dosyası, images
klasöründeki yollar üzerinde yinelenir ve her yol için create_thumbnail işlevini çalıştırır. Bu işlev, resmi açmak, bir küçük resim oluşturmak ve orijinal ile aynı ada sahip ancak adına _thumbnail
eklenmiş yeni, daha küçük resmi kaydetmek için Yastık kullanır.
Bu betiği toplam 36 milyon 160 görüntü üzerinde çalıştırmak 2.32 saniye sürüyor. Bakalım ProcessPoolExecutor kullanarak bunu hızlandırabilecek miyiz.
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()
create_thumbnail
yöntemi, son komut dosyasıyla aynıdır. Temel fark, bir ProcessPoolExecutor
oluşturulmasıdır. Yürütücünün harita yöntemi, küçük resimleri paralel olarak oluşturmak için kullanılır. Varsayılan olarak ProcessPoolExecutor
, CPU başına bir alt süreç oluşturur. Bu komut dosyasını aynı 160 görüntü üzerinde çalıştırmak 1,05 saniye sürdü - 2,2 kat daha hızlı!
Zaman uyumsuz/Bekleme (yalnızca Python 3.5+)
Orijinal makaledeki yorumlarda en çok istenen öğelerden biri, Python 3'ün asyncio modülünü kullanan bir örnekti. Diğer örneklerle karşılaştırıldığında, çoğu insan için yeni olabilecek bazı yeni Python sözdizimi ve ayrıca bazı yeni kavramlar var. Talihsiz bir ek karmaşıklık katmanı, Python'un yerleşik urllib
modülünün eşzamansız olmamasından kaynaklanır. Asyncio'nun tüm avantajlarından yararlanmak için bir zaman uyumsuz HTTP kitaplığı kullanmamız gerekecek. Bunun için aiohttp kullanacağız.
Hemen koda geçelim ve daha ayrıntılı bir açıklama takip edecek.
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)
Burada açmak için biraz var. Programın ana giriş noktasıyla başlayalım. asyncio modülü ile yaptığımız ilk yeni şey olay döngüsünü elde etmektir. Olay döngüsü, tüm zaman uyumsuz kodu işler. Ardından döngü tamamlanana ve main
işlevi geçene kadar çalıştırılır. main: async def
tanımında bir parça yeni sözdizimi var. Ayrıca await
ve with async
fark edeceksiniz.
Zaman uyumsuz/bekleme sözdizimi PEP492'de tanıtıldı. Zaman async def
sözdizimi, bir işlevi eşyordam olarak işaretler. Dahili olarak, eşyordamlar Python oluşturuculara dayanır, ancak tam olarak aynı şey değildir. Eşyordamlar, oluşturucuların bir oluşturucu nesnesi döndürmesine benzer bir eşyordam nesnesi döndürür. Bir eşyordamınız olduğunda, sonuçlarını await
ifadesi ile elde edersiniz. Bir eşyordam wait çağırdığında, await
yürütülmesi beklenen tamamlanana kadar askıya alınır. Bu askıya alma, eşyordam bazı sonuçları “beklerken” askıya alınırken diğer işlerin tamamlanmasına izin verir. Genel olarak, bu sonuç, bir veritabanı talebi veya bizim durumumuzda bir HTTP talebi gibi bir tür G/Ç olacaktır.
download_link
işlevinin oldukça önemli ölçüde değiştirilmesi gerekiyordu. Önceden, resmi okuma işinin yükünü bizim için yapması için urllib
güveniyorduk. Şimdi, yöntemimizin zaman uyumsuz programlama paradigmasıyla düzgün çalışmasına izin vermek için, bir seferde görüntünün parçalarını okuyan ve G/Ç'nin tamamlanmasını beklerken yürütmeyi askıya alan bir while
döngüsü ekledik. Bu, olay döngüsünün, indirme sırasında her birinin yeni verisi olduğundan farklı görüntülerin indirilmesi arasında döngü yapmasına izin verir.
Bunu Yapmanın Tek Bir - Tercihen Tek Bir - Açık Yolu Olmalı
Python zen'i bize bir şeyi yapmanın tek bir açık yolu olması gerektiğini söylese de, Python'da programlarımıza eşzamanlılık getirmenin birçok yolu vardır. Seçilecek en iyi yöntem, özel kullanım durumunuza bağlı olacaktır. Eşzamansız paradigma, iş parçacığı oluşturma veya çoklu işleme ile karşılaştırıldığında yüksek eşzamanlılıklı iş yüklerine (bir web sunucusu gibi) daha iyi ölçeklenir, ancak tam olarak yararlanmak için kodunuzun (ve bağımlılıklarınızın) eşzamansız olmasını gerektirir.
Umarım bu makaledeki ve güncellemedeki Python iş parçacığı örnekleri sizi doğru yöne yönlendirir, böylece programlarınıza eşzamanlılık eklemeniz gerekiyorsa Python standart kitaplığında nereye bakacağınız konusunda bir fikriniz olur.