Pythonia kritisoivissa keskusteluissa puhutaan usein siitä, kuinka Pythonia on vaikea käyttää monisäikeisessä työssä osoittamalla sormilla ns. Maailmanlaajuista tulkkilukkoa (jota kutsutaan hellästi nimellä GIL ), joka estää useita Python-koodin ketjuja toimimasta samanaikaisesti. Tästä johtuen Python-monisäikeinen moduuli ei toimi aivan kuten odotat, jos et ole Python-kehittäjä ja olet kotoisin muilta kieliltä, kuten C ++ tai Java. On tehtävä selväksi, että Pythoniin voidaan silti kirjoittaa samanaikaisesti tai rinnakkain kulkevaa koodia ja tehdä huomattavia eroja tuloksissa, kunhan tietyt asiat otetaan huomioon. Jos et ole vielä lukenut sitä, suosittelen, että katsot Eqbal Quranin artikkeli samanaikaisuudesta ja rinnakkaisuudesta Ruby-lehdessä täällä ApeeScape Engineering -blogissa.
Tässä Python-samanaikaisuuden opetusohjelmassa kirjoitamme pienen Python-komentosarjan suosituimpien kuvien lataamiseksi Imgurista. Aloitamme versiosta, joka lataa kuvia peräkkäin tai yksi kerrallaan. Edellytyksenä sinun on rekisteröidyttävä sovellus Imguriin . Jos sinulla ei vielä ole Imgur-tiliä, luo se ensin.
Näiden ketjuesimerkkien skriptit on testattu Python 3.6.4: lla. Joidenkin muutosten myötä niiden tulisi myös toimia Python 2: n kanssa - urllib on se, mikä on muuttunut eniten näiden kahden Python-version välillä.
Aloitetaan luomalla Python-moduuli nimeltä download.py
. Tämä tiedosto sisältää kaikki tarvittavat toiminnot kuvaluettelon noutamiseksi ja lataamiseksi. Jaamme nämä toiminnot kolmeen erilliseen toimintoon:
get_links
download_link
setup_download_dir
Kolmas funktio, setup_download_dir
, käytetään luomaan lataushakemistohakemisto, jos sitä ei vielä ole olemassa.
Imgurin sovellusliittymä edellyttää, että HTTP-pyynnöt sisältävät Authorization
otsikko asiakastunnuksella. Löydät tämän asiakastunnuksen Imguriin rekisteröimäsi sovelluksen hallintapaneelista, ja vastaus on JSON-koodattu. Voimme käyttää Pythonin tavanomaista JSON-kirjastoa sen dekoodaamiseen. Kuvan lataaminen on vielä yksinkertaisempi tehtävä, koska sinun tarvitsee vain hakea kuva sen URL-osoitteen perusteella ja kirjoittaa se tiedostoon.
Näin käsikirjoitus näyttää:
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
Seuraavaksi meidän on kirjoitettava moduuli, joka käyttää näitä toimintoja kuvien lataamiseen yksi kerrallaan. Nimeämme tämän single.py
. Tämä sisältää Imgur-kuvalataimen ensimmäisen, naiivin version päätoiminnon. Moduuli hakee Imgur-asiakastunnuksen ympäristömuuttujaan IMGUR_CLIENT_ID
Se käyttää setup_download_dir
luoda ladattavan kohdehakemiston. Lopuksi se hakee luettelon kuvista käyttämällä get_links
-toiminto, suodata kaikki GIF- ja albumien URL-osoitteet ja käytä sitten download_link
ladata ja tallentaa kukin näistä kuvista levylle. Tässä on mitä single.py
näyttää:
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()
Kannettavalla tietokoneellani tämä skripti kesti 19,4 sekuntia 91 kuvan lataamiseen. Huomaa, että nämä numerot voivat vaihdella verkon mukaan. 19,4 sekuntia ei ole kovin pitkä, mutta entä jos haluaisimme ladata lisää kuvia? Ehkä 900 kuvaa 90 sijasta. Kun keskimäärin 0,2 sekuntia kuvaa kohden, 900 kuvaa kestää noin 3 minuuttia. 9000 kuvalle kestää 30 minuuttia. Hyvä uutinen on, että ottamalla käyttöön samanaikaisuuden tai rinnakkaisuuden voimme nopeuttaa tätä dramaattisesti.
Kaikki seuraavat koodiesimerkit näyttävät vain tuontilausekkeet, jotka ovat uusia ja erityisiä näille esimerkeille. Mukavuuden vuoksi kaikki nämä Python-komentosarjat löytyvät tämä GitHub-arkisto .
Langoittaminen on yksi tunnetuimmista lähestymistavoista saavuttaa Python-samanaikaisuus ja rinnakkaisuus. Langoitus on ominaisuus, jonka käyttöjärjestelmä yleensä tarjoaa. Kierteet ovat prosessia kevyempiä, ja niillä on sama muistitila.
Tässä Python-ketjutuksen esimerkissä kirjoitamme uuden moduulin korvaamaan single.py
Tämä moduuli luo kahdeksan säiettä, jotka muodostavat yhteensä yhdeksän säiettä päälanka mukaan lukien. Valitsin kahdeksan työntekijälankaa, koska tietokoneessani on kahdeksan CPU-ydintä ja yksi työlanka ydintä kohden näytti hyvältä lukumäärältä kuinka monta säiettä suoritetaan kerralla. Käytännössä tämä numero valitaan paljon huolellisemmin muiden tekijöiden, kuten muiden samalla koneella toimivien sovellusten ja palveluiden perusteella.
Tämä on melkein sama kuin edellinen, paitsi että meillä on nyt uusi luokka DownloadWorker
, joka on Pythonin jälkeläinen Thread
luokassa. Suoritustapa on ohitettu, joka ajaa ääretöntä silmukkaa. Jokaisella iteraatiolla se kutsuu self.queue.get()
yrittää hakea URL-osoite langattomasta jonosta. Se estää, kunnes jonossa on kohde, jota työntekijä voi käsitellä. Kun työntekijä saa kohteen jonosta, se soittaa samalle download_link
menetelmä, jota käytettiin edellisessä komentosarjassa kuvan lataamiseksi kuvahakemistoon. Kun lataus on valmis, työntekijä ilmoittaa jonolle, että tehtävä on suoritettu. Tämä on erittäin tärkeää, koska jono seuraa kuinka monta tehtävää on tehty. Kutsu queue.join()
estäisi pääkierteen ikuisesti, jos työntekijät eivät ilmoita suorittaneensa tehtävän.
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()
Tämän Python-ketjutuksen esimerkkikoodin suorittaminen samalla koneella, jota käytettiin aiemmin, johtaa 4,1 sekunnin latausaikaan! Se on 4,7 kertaa nopeampi kuin edellinen esimerkki. Vaikka tämä on paljon nopeampi, on syytä mainita, että vain yksi ketju suoritettiin kerrallaan koko prosessin ajan GIL: n vuoksi. Siksi tämä koodi on samanaikainen, mutta ei rinnakkainen. Syy on edelleen nopeampi, koska tämä on IO-sidottu tehtävä. Suoritin tuskin hikoilee ladatessaan näitä kuvia, ja suurin osa ajasta kuluu verkon odottamiseen. Siksi Pythonin monisäikeisyys voi lisätä suurta nopeutta. Suoritin voi vaihtaa ketjujen välillä, kun jokin niistä on valmis tekemään töitä. Lankamoduulin käyttö Pythonissa tai muussa tulkitussa kielessä GIL: n kanssa voi johtaa suorituskyvyn heikkenemiseen. Jos koodisi suorittaa suorittimeen sidottua tehtävää, kuten purkaa gzip-tiedostoja, käytä threading
moduuli johtaa hitaampaan suoritusaikaan. Suoritinohjattuihin tehtäviin ja todella rinnakkaiseen suoritukseen voimme käyttää moniprosessointimoduulia.
Samalla kun de facto viitepython-toteutus - CPythonilla - on GIL, tämä ei päde kaikkiin Python-toteutuksiin. Esimerkiksi IronPythonilla, joka on .NET-kehystä käyttävä Python-toteutus, ei ole GIL: ää eikä Jythonilla, Java-pohjainen toteutus. Löydät luettelon toimivista Python-toteutuksista tässä .
Liittyvät: ApeeScape-kehittäjien Pythonin parhaat käytännöt ja vinkitMoniprosessointimoduuli on helpompi pudottaa sisään kuin ketjutusmoduuli, koska meidän ei tarvitse lisätä luokkaa, kuten Python-ketjutuksen esimerkki. Ainoat muutokset, jotka meidän on tehtävä, ovat päätoiminnossa.
Useiden prosessien käyttämiseksi luomme moniprosessoinnin Pool
. Sen tarjoamalla karttamenetelmällä välitämme URL-osoitteiden luettelon altaalle, joka puolestaan synnyttää kahdeksan uutta prosessia ja käyttää niitä kaikkia lataamaan kuvat rinnakkain. Tämä on todellista rinnakkaisuutta, mutta siitä aiheutuu kustannuksia. Komentosarjan koko muisti kopioidaan kuhunkin synnyttämään aliprosessiin. Tässä yksinkertaisessa esimerkissä se ei ole iso juttu, mutta siitä voi helposti tulla vakava kustannus ei-triviaalille ohjelmille.
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()
Vaikka ketjutus- ja moniprosessointimoduulit sopivat erinomaisesti tietokoneellasi toimiville komentosarjoille, mitä sinun pitäisi tehdä, jos haluat työn tekemisen eri koneella tai sinun on skaalattava enemmän kuin yhden koneen CPU voi kahva? Erinomainen käyttötapaus tähän on pitkäaikainen web-sovellusten taustatehtävä. Jos sinulla on pitkäaikaisia tehtäviä, et halua kehittää joukkoa aliprosesseja tai ketjuja samalla koneella, joiden on suoritettava loput sovelluskoodistasi. Tämä heikentää sovelluksesi suorituskykyä kaikille käyttäjillesi. Hienoa on pystyä ajamaan näitä töitä toisella koneella tai monilla muilla koneilla.
Suuri Python-kirjasto tätä tehtävää varten on RQ , hyvin yksinkertainen mutta tehokas kirjasto. Kuvaa ensin funktio ja sen argumentit kirjaston avulla. Tämä suolakurkkua funktiokutsuesitys, joka liitetään sitten a Redis lista. Työn tarjoaminen on ensimmäinen askel, mutta se ei vielä tee mitään. Tarvitsemme myös vähintään yhden työntekijän kuuntelemaan kyseistä jonoa.
Ensimmäinen askel on asentaa ja suorittaa Redis-palvelin tietokoneellesi tai käyttää käynnissä olevaa Redis-palvelinta. Sen jälkeen olemassa olevaan koodiin on tehty vain muutama pieni muutos. Luomme ensin RQ-jonon esiintymän ja välitämme sille Redis-palvelimen ilmentymän redis-py-kirjasto . Sitten sen sijaan, että soittaisit vain download_link
-menetelmää kutsutaan q.enqueue(download_link, download_dir, link)
. Enqueue-menetelmä ottaa funktion ensimmäisenä argumenttina, minkä jälkeen kaikki muut argumentit tai avainsana-argumentit välitetään kyseiselle funktiolle, kun työ todella suoritetaan.
Viimeinen askel, joka meidän on tehtävä, on perustaa työntekijöitä. RQ tarjoaa kätevän komentosarjan työntekijöiden ajamiseksi oletusjonossa. Aja vain rqworker
pääteikkunassa ja se aloittaa työntekijän kuuntelemisen oletusjonossa. Varmista, että nykyinen työkirjasi on sama kuin komentosarjat. Jos haluat kuunnella eri jonoa, voit suorittaa rqworker queue_name
ja se kuuntelee nimettyä jonoa. RQ: n hieno asia on, että niin kauan kuin voit muodostaa yhteyden Redisiin, voit ajaa niin monta työntekijää kuin haluat niin monella eri koneella kuin haluat; Siksi on erittäin helppo laajentaa sovelluksen kasvaessa. Tässä on RQ-version lähde:
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()
RQ ei kuitenkaan ole ainoa Python-työjonojärjestelmä. RQ on helppokäyttöinen ja kattaa yksinkertaiset käyttötapaukset erittäin hyvin, mutta jos tarvitaan edistyneempiä vaihtoehtoja, muut Python 3 -jonojärjestelmät (kuten Selleri ) voidaan käyttää.
Jos koodi on IO-sidottu, sekä moniprosessointi että monisäikeisyys Pythonissa toimivat sinulle. Moniprosessointi on helpompi vain pudottaa kuin langoittaminen, mutta sillä on suurempi muisti. Jos koodisi on sidottu suorittimeen, moniprosessointi on todennäköisesti parempi valinta - varsinkin jos kohdekoneessa on useita ytimiä tai suorittimia. Verkkosovelluksissa ja kun sinun on skaalattava työ useille koneille, RQ tulee olemaan sinulle parempi.
Liittyvät: Tulee edistyneemmäksi: Vältä 10 yleisintä virhettä, joita Python-ohjelmoijat tekevätconcurrent.futures
Jotain uutta Python 3.2: n jälkeen, jota alkuperäisessä artikkelissa ei käsitelty, on concurrent.futures
paketti. Tämä paketti tarjoaa vielä yhden tavan käyttää samanaikaisuutta ja rinnakkaisuutta Pythonin kanssa.
Alkuperäisessä artikkelissa mainitsin, että Pythonin moniprosessointimoduuli olisi helpompi pudottaa olemassa olevaan koodiin kuin ketjutusmoduuli. Tämä johtui siitä, että Python 3 -säikeistysmoduuli vaati Thread
-alaluokituksen luokka ja luodaan myös Queue
langat valvovat työtä.
Käyttää concurrent.futures.ThreadPoolExecutor tekee Python-ketjutuksen esimerkkikoodista lähes identtisen moniprosessointimoduulin kanssa.
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()
Nyt kun kaikki nämä kuvat on ladattu Python ThreadPoolExecutor
-sovelluksellamme, voimme käyttää niitä testaamaan prosessoriin sidottua tehtävää. Voimme luoda pikkukuvan versiot kaikista kuvista sekä yksisäikeisessä, yksivaiheisessa komentosarjassa että testata sitten moniprosessointipohjaisen ratkaisun.
Aiomme käyttää Tyyny kirjasto käsittelemään kuvien kokoa.
Tässä on alkuperäinen käsikirjoituksemme.
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. E.g.: >>> 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()
Tämä komentosarja toistaa images
-polut kansion ja kullekin polulle se suorittaa create_thumbnail-toiminnon. Tämä toiminto käyttää tyynyä kuvan avaamiseen, pikkukuvan luomiseen ja uuden, pienemmän kuvan tallentamiseen samalla nimellä kuin alkuperäinen, mutta _thumbnail
liitetty nimeen.
Tämän komentosarjan suorittaminen 160 kuvalla, yhteensä 36 miljoonalla, kestää 2,32 sekuntia. Katsotaan, voimmeko nopeuttaa tätä käyttämällä a 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. E.g.: >>> 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
menetelmä on identtinen viimeisen komentosarjan kanssa. Suurin ero on ProcessPoolExecutor
: n luominen. Toteuttajan kartta menetelmää käytetään pikkukuvien luomiseen rinnakkain. Oletuksena ProcessPoolExecutor
luo yhden aliprosessin prosessoria kohden. Tämän komentosarjan suorittaminen samalla 160 kuvalla vei 1,05 sekuntia - 2,2 kertaa nopeammin!
Yksi alkuperäisen artikkelin kommenteissa pyydetyistä kohteista oli esimerkki Python 3: n käytöstä asyncio moduuli. Muihin esimerkkeihin verrattuna on joitain uusia Python-syntaksia, jotka saattavat olla uusia useimmille ihmisille, ja myös joitain uusia käsitteitä. Valitettavasti monimutkaisuuden lisää valitettavasti Pythonin sisäänrakennettu urllib
moduuli ei ole asynkroninen. Meidän on käytettävä asynkronoitua HTTP-kirjastoa saadaksesi täyden hyödyn asynciosta. Tätä varten käytämme aiohttp .
Hypätään suoraan koodiin ja tarkempi selitys seuraa.
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)
Täällä on melko vähän purettavissa. Aloitetaan ohjelman pääsisäänkäynnistä. Ensimmäinen uusi asia, jonka teemme asyncio-moduulin kanssa, on hankkia tapahtumasilmukka. Tapahtumasilmukka käsittelee kaiken asynkronisen koodin. Sitten silmukkaa ajetaan loppuun saakka ja se ohittaa main
toiminto. Main-määritelmässä on pala uutta syntaksia: async def
. Huomaat myös await
ja with async
.
Asynkroni / odota-syntakse otettiin käyttöön PEP492 . async def
syntaksilla merkitään funktio a: ksi korutiini . Sisäisesti korutiinit perustuvat Python-generaattoreihin, mutta eivät ole aivan sama asia. Korutiinit palauttavat korutiiniobjektin, joka on samanlainen kuin generaattorit palauttavat generaattorikohteen. Kun sinulla on korutiini, saat sen tulokset await
ilmaisu. Kun korutiini kutsuu await
, korutiinin toteutus keskeytetään, kunnes odotettavissa oleva on valmis. Tämän jousituksen avulla muut työt voidaan suorittaa loppuun samalla kun korutiini on keskeytetty 'odottamassa' jonkinlaista tulosta. Yleensä tämä tulos on jonkinlainen I / O, kuten tietokantapyyntö tai tapauksessamme HTTP-pyyntö.
download_link
toimintoa oli muutettava melko merkittävästi. Aikaisemmin luotimme urllib
tehdä suurin osa kuvan lukemisesta meille. Nyt, jotta menetelmämme toimisi oikein asynkronisen ohjelmointiparadigman kanssa, olemme ottaneet käyttöön while
silmukka, joka lukee kuvan palat kerrallaan ja keskeyttää suorituksen odottaessaan I / O: n päättymistä. Tämä antaa tapahtumasilmukan edetä lataamalla eri kuvia, koska jokaisella on uutta tietoa saatavana latauksen aikana.
Samalla kun zen Pythonista kertoo meille, että pitäisi olla yksi ilmeinen tapa tehdä jotain, Pythonissa on monia tapoja lisätä samanaikaisuus ohjelmiin. Paras menetelmä valita riippuu käyttötapauksestasi. Asynkroninen paradigma skaalautuu paremmin korkean samanaikaisuuden kuormituksiin (kuten verkkopalvelin) ketjutukseen tai moniprosessointiin verrattuna, mutta se vaatii, että koodisi (ja riippuvuutesi) ovat asynkronoituja, jotta niistä saat täyden hyödyn.
Toivottavasti tämän artikkelin Python-säikeiden esimerkit - ja päivitys - osoittavat sinut oikeaan suuntaan, joten sinulla on idea siitä, mistä etsiä Python-standardikirjastossa, jos sinun on lisättävä samanaikaisuutta ohjelmiin.
Lanka on kevyt prosessi tai tehtävä. Lanka on yksi tapa lisätä samanaikaisuutta ohjelmiin. Jos Python-sovelluksesi käyttää useita ketjuja ja katsot käyttöjärjestelmässäsi käynnissä olevia prosesseja, näet vain yhden merkinnän skriptille, vaikka siinä olisi useita ketjuja.
Monisäikeinen (joskus yksinkertaisesti 'ketjutus') on silloin, kun ohjelma luo useita ketjuja, joiden joukossa on suoritussykli, joten yksi pidempään suoritettava tehtävä ei estä kaikkia muita. Tämä toimii hyvin tehtävissä, jotka voidaan jakaa pienempiin alitehtäviin, jotka kukin voidaan sitten antaa suoritettavalle säikeelle.
Langoituksella samanaikaisuus saavutetaan käyttämällä useita ketjuja, mutta GIL: n vuoksi vain yksi ketju voi olla käynnissä kerrallaan. Moniprosessoinnissa alkuperäinen prosessi haarautuu prosessiksi useiksi aliprosesseiksi ohittaen GIL. Jokaisella aliprosessilla on kopio koko ohjelman muistista.
Sekä monisäikeinen että moniprosessointi sallivat Python-koodin suorittamisen samanaikaisesti. Vain moniprosessointi sallii koodisi olla todella rinnakkainen. Jos koodisi on kuitenkin IO-raskas (kuten HTTP-pyynnöt), monisäikeisyys todennäköisesti silti nopeuttaa koodiasi.