برنامه‌نویسی پایتون فهرست مطالب
فصل ۳۷

همروندی: نخ‌ها و asyncio

تا اینجا هر برنامه ما یک کار را در یک لحظه انجام می‌داد: خط دوم منتظر خط اول می‌ماند. در این فصل یاد می‌گیریم چند کار را با هم پیش ببریم، و مهم‌تر از آن، یاد می‌گیریم کدام یک از سه ابزار را انتخاب کنیم. انتخاب اشتباه، تمام پیچیدگی را به شما می‌دهد و هیچ سرعتی را.

در این فصل می‌خوانید
  1. سه ابزار، یک جدول
  2. اولین نخ
  3. نخ برای کار انتظاری
  4. شرایط رقابتی
  5. قفل و رویداد
  6. صف و کارگرها
  7. استخر نخ
  8. نخ و رابط گرافیکی
  9. قفل سراسری و پردازه‌ها
  10. اولین کوروتین
  11. اجرای همزمان با gather
  12. تسک، لغو و مهلت
  13. صف ناهمگام
  14. پروژه: ربات پیام‌رسان

۱. سه ابزار، یک جدول

اگر فقط یک چیز از این فصل یادتان بماند، همین جدول باشد:

برنامه شما وقتش را صرف چه می‌کندابزارچرا
انتظار (شبکه، دیسک، پایگاه داده) و تعداد کارها کم استthreadingقفل سراسری هنگام انتظار آزاد می‌شود
انتظار، ولی صدها یا هزاران کارasyncioیک کوروتین تقریباً هیچ هزینه‌ای ندارد؛ یک نخ دارد
محاسبه (حلقه، ریاضی، پردازش تصویر)multiprocessingهر پردازه قفل خودش را دارد، پس چند هسته واقعاً کار می‌کنند

۲. اولین نخ

نخ یک خط اجرای دوم در همان برنامه است، پس دو تکه کد می‌توانند با هم پیش بروند.

import threading
import time


def worker() -> None:
    '''This function will run in a thread of its own.'''
    name = threading.current_thread().name
    for step in range(3):
        print(f'[{name}] step {step}')
        time.sleep(1)
    print(f'[{name}] finished')


def main() -> None:
    print(f'[{threading.current_thread().name}] creating the thread')

    thread = threading.Thread(target=worker, name='worker')
    thread.start()                 # start() -- never run()

    # The main thread is free while the worker sleeps.
    print('[MainThread] the thread is running; I am not blocked')
    print(f'[MainThread] is it alive? {thread.is_alive()}')

    thread.join()                  # wait for it
    print('[MainThread] done')

اولین نخ: نمونه 01_first_thread/app.py

سه قاعده از همان روز اول

نکته‌ای که همه یک بار گیرش می‌افتند: args=('A',) با ویرگول انتهایی. بدون آن یک رشته است نه تاپل، و پایتون کاراکترهایش را به عنوان آرگومان‌های جدا می‌فرستد.

۳. نخ برای کار انتظاری

نخ کی برنامه را واقعاً سریع‌تر می‌کند؟ وقتی برنامه منتظر است. هنگام دانلود یک فایل یا پرس‌وجو از پایگاه داده، پردازنده بیکار است و نخ دیگری می‌تواند از آن وقت استفاده کند.

SITES = ['kadoos.ir', 'python.org', 'sqlite.org', 'qt.io']


def fake_download(site: str, seconds: float = 1.0) -> None:
    '''Pretend to download a page. `sleep` = waiting for the network.'''
    time.sleep(seconds)
    print(f'  downloaded {site}')


def sequential() -> float:
    start = time.perf_counter()
    for site in SITES:
        fake_download(site)
    return time.perf_counter() - start


def threaded() -> float:
    start = time.perf_counter()
    threads = [threading.Thread(target=fake_download, args=(site,))
               for site in SITES]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()
    return time.perf_counter() - start

چهار ثانیه به یک ثانیه: نمونه 03_threads_for_io/app.py

one after the other (4 sites):
  -> 4.01 seconds

with 4 threads:
  -> 1.00 seconds

4.0 times faster

دقت کنید الگو همیشه یکی است: اول همه نخ‌ها را start کنید، بعد همه را join. اگر هر نخ را بلافاصله بعد از راه‌اندازی join کنید، دوباره پشت سر هم اجرا می‌شوند و هیچ سودی نمی‌برید.

برای اندازه‌گیری زمان از time.perf_counter() استفاده کنید، نه time.time(): فقط جلو می‌رود و دقت بالایی دارد.

۴. شرایط رقابتی

حالا خطرناک‌ترین باگ دنیای همروندی: باگی که فقط گاهی خودش را نشان می‌دهد.

counter = 0                 # shared between every thread


def worker(rounds: int) -> None:
    global counter
    for _ in range(rounds):
        # This is `counter += 1` written out in slow motion.
        temp = counter          # 1. read
        time.sleep(0.000_01)    # <- the interruption, made visible
        temp += 1               # 2. add
        counter = temp          # 3. write


def main() -> None:
    threads = [threading.Thread(target=worker, args=(1000,))
               for _ in range(10)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()

    print(f'expected: 10000')
    print(f'obtained: {counter}')
    print(f'lost:     {10000 - counter} increments')

شرایط رقابتی: نمونه 04_race_condition/app.py

expected: 10000
obtained: 1247
lost:     8753 increments

شرح مثال

۵. قفل و رویداد

class BankAccount:
    '''A shared account. Every change to `balance` goes through the lock.'''

    def __init__(self, balance: int = 0) -> None:
        self.balance = balance
        self.lock = threading.Lock()

    def deposit(self, amount: int) -> None:
        with self.lock:
            # Slow motion again, to prove the lock really protects us.
            new_balance = self.balance + amount
            time.sleep(0.000_01)
            self.balance = new_balance

    def withdraw(self, amount: int) -> bool:
        with self.lock:
            # "Check, then act" MUST be inside the same lock, otherwise the
            # balance can change between the `if` and the subtraction.
            if amount > self.balance:
                return False
            self.balance -= amount
            return True

قفل: نمونه 05_locks/app.py

شرح مثال

ابزاروظیفه آن
Lockهر لحظه فقط یک نخ داخل بخش بحرانی
RLockهمان نخ می‌تواند چند بار بگیردش، برای فراخوانی تودرتو
Eventیک نخ منتظر علامت نخ دیگر می‌ماند: wait() و set()
Semaphoreحداکثر N نخ همزمان، مثلاً برای استخر اتصال
    stop = threading.Event()

    def blinker() -> None:
        while not stop.is_set():
            print('  ...working')
            # wait() returns as soon as the event is set: much better than
            # sleep(), which would always run to the end.
            stop.wait(timeout=0.3)
        print('  worker stopped cleanly')

    worker = threading.Thread(target=blinker)
    worker.start()
    time.sleep(1)
    stop.set()                  # ask it to stop
    worker.join()

رویداد: توقف تمیز یک نخ

بن‌بست

دام بزرگ قفل‌ها بن‌بست است: دو نخ، دو قفل، و ترتیب متفاوت. نخ A قفل یک را دارد و منتظر قفل دو است، نخ B قفل دو را دارد و منتظر قفل یک؛ برنامه برای همیشه می‌ایستد. درمانش یک قاعده ساده است: قفل‌ها را همیشه به یک ترتیب بگیرید.

۶. صف و کارگرها

queue.Queue فهرستی است که خودش از قبل امن است: put و get قفل را برایتان می‌گیرند. برای همین است که توصیه اول هر برنامه‌نویس باتجربه این است: «داده را از راه صف رد و بدل کنید، نه از راه متغیر مشترک».

def worker(tasks: queue.Queue, results: queue.Queue) -> None:
    '''Take numbers from the queue until the sentinel arrives.'''
    name = threading.current_thread().name
    while True:
        number = tasks.get()
        try:
            if number is None:          # the sentinel: "no more work"
                return
            time.sleep(0.2)             # pretend the job takes time
            results.put((name, number, number * number))
        finally:
            # In a `finally`, so an exception cannot make queue.join() hang
            # for ever waiting for a task_done() that never comes.
            tasks.task_done()


def main() -> None:
    tasks: queue.Queue = queue.Queue()
    results: queue.Queue = queue.Queue()

    workers = [threading.Thread(target=worker, args=(tasks, results),
                                name=f'worker-{index}', daemon=True)
               for index in range(3)]
    for thread in workers:
        thread.start()

    for number in range(1, 11):
        tasks.put(number)

    # One sentinel per worker, so each of them sees exactly one.
    for _ in workers:
        tasks.put(None)

    tasks.join()                    # wait until everything is handled
    for thread in workers:
        thread.join()

صف و کارگرها: نمونه 06_queue_and_workers/app.py

شرح مثال

۷. استخر نخ

concurrent.futures راه کوتاه و امروزی همه کارهای بالاست: استخری از نخ‌های آماده که راه‌اندازی و انتظار و جمع‌آوری نتیجه را خودش انجام می‌دهد.

from concurrent.futures import ThreadPoolExecutor, as_completed

    # -- map: simple, ordered, and it stops at the first exception ---------
    with ThreadPoolExecutor(max_workers=4) as pool:
        good_sites = [site for site in SITES if site != 'flask.dev']
        for site, size in zip(good_sites, pool.map(fetch, good_sites)):
            print(f'  {site}: {size} bytes')

    # -- submit + as_completed: answers as soon as they are ready ----------
    with ThreadPoolExecutor(max_workers=4) as pool:
        # A dict Future -> site, to know which job an answer belongs to.
        futures = {pool.submit(fetch, site): site for site in SITES}

        for future in as_completed(futures):
            site = futures[future]
            try:
                size = future.result()      # re-raises the worker's exception
            except ConnectionError as error:
                print(f'  {site}: FAILED ({error})')
            else:
                print(f'  {site}: {size} bytes')

استخر نخ: نمونه 07_thread_pool/app.py

mapsubmit
یک تابع روی یک فهرستهر کار جداگانه، با یک Future
نتیجه‌ها به ترتیب ورودیبا as_completed به ترتیب آماده شدن
ساده‌ترانعطاف بیشتر، مدیریت خطای هر کار جدا
خطایی که هرگز نمی‌بینید

استثنایی که داخل یک کارگر رخ می‌دهد، برنامه را نمی‌ترکاند و هیچ چیزی چاپ نمی‌کند. در Future ذخیره می‌شود و فقط وقتی .result() را صدا بزنید دوباره پرتاب می‌شود. یعنی submit()ی که نتیجه‌اش را هرگز نمی‌خوانید، خطایی است که هیچ وقت نخواهید دید.

بلوک with پیش از پایان، منتظر همه کارها می‌ماند. پس دیگر حلقه join ضرورتی ندارد.

۸. نخ و رابط گرافیکی

اینجا دلیل واقعی نیاز شما به نخ است. فصل سی و پنجم گفت کار طولانی داخل تابع پاسخ، پنجره را منجمد می‌کند. after() برای یک زمان‌سنج جواب داد، ولی برای کار واقعی نه: یک دانلود را نمی‌شود به تکه‌های صدمیلی‌ثانیه‌ای برید.

قانون طلایی

هرگز از نخ دیگری به ویجت دست نزنید. نه tkinter و نه Qt و نه تقریباً هیچ کتابخانه گرافیکی دیگری امن-نخ نیست. صدا زدن label.config(...) از داخل کارگر، به شما خرابی‌های تصادفی، پنجره منجمد، یا هیچ می‌دهد؛ و فقط روی بعضی کامپیوترها.

الگوی امن این است:

worker thread  --put-->  queue.Queue  --get-->  main thread (after())
    # -- the worker thread -------------------------------------------------
    def download(self) -> None:
        '''Runs in a THREAD. It must not touch a single widget.'''
        for percent in range(0, 101, 5):
            if self.stop_event.is_set():
                self.messages.put(('done', 'cancelled'))
                return
            time.sleep(0.2)                     # the waiting work
            self.messages.put(('progress', percent))
        self.messages.put(('done', 'finished'))

    # -- the GUI thread ----------------------------------------------------
    def poll_queue(self) -> None:
        '''Runs in the MAIN thread: here, and only here, we touch widgets.'''
        try:
            while True:
                # get_nowait raises Empty instead of blocking the interface.
                kind, value = self.messages.get_nowait()
                if kind == 'progress':
                    self.bar['value'] = value
                    self.status.config(text=f'downloading... {value} %')
                elif kind == 'done':
                    self.status.config(text=str(value))
                    self.start_button.config(state='normal')
        except queue.Empty:
            pass
        # Ask to be called again. This line must never be skipped.
        self.root.after(self.POLL_MS, self.poll_queue)

    def cancel(self) -> None:
        '''You cannot kill a thread; you ASK it to stop.'''
        self.stop_event.set()
        self.status.config(text='cancelling...')

نخ و tkinter: نمونه 08_thread_with_gui/app.py

شرح مثال

۹. قفل سراسری و پردازه‌ها

GIL یا قفل سراسری مفسر، قفلی است داخل خود CPython: در هر لحظه فقط یک نخ بایت‌کد پایتون اجرا می‌کند. نتیجه‌اش همان جدول بخش اول است.

from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor


def count_primes(limit: int) -> int:
    '''Pure calculation, no waiting at all: the GIL's worst case.'''
    total = 0
    for number in range(2, limit):
        for divisor in range(2, int(number ** 0.5) + 1):
            if number % divisor == 0:
                break
        else:
            total += 1
    return total

محاسبه خالص: نمونه 09_gil_and_processes/app.py

این برنامه را اجرا کنید: نسخه نخ‌دار یک محاسبه خالص، به همان کندی نسخه ترتیبی است و گاهی کندتر، به خاطر هزینه جابه‌جایی بین نخ‌ها. پاسخ درست برای محاسبه، ProcessPoolExecutor است: چند پردازه واقعی سیستم‌عامل، هر کدام با مفسر و قفل خودش، روی چند هسته.

بهای یک پردازه

آینده GIL

نسخه‌های آزاد-نخ پایتون ۳.۱۳ به بعد می‌توانند بدون GIL اجرا شوند. تا وقتی این حالت همه‌جا عادی نشده است، قاعده بالا همان چیزی است که با آن برنامه‌ریزی می‌کنید.

۱۰. اولین کوروتین

یک نخ گران است: سیستم‌عامل می‌سازدش، به آن سوئیچ می‌کند، و هر کدام حافظه می‌خورند. هزار نخ یک مشکل است؛ هزار کوروتین هیچ چیز نیست. asyncio همروندی با یک نخ است.

واژهیعنی
async defتعریف یک کوروتین: تابعی که می‌تواند مکث کند
awaitاینجا مکث کن تا این آماده شود، و بگذار در این فاصله چیز دیگری اجرا شود
asyncio.run()حلقه رویداد را راه می‌اندازد و یک کوروتین را تا آخر اجرا می‌کند
import asyncio
import time


async def worker(name: str, seconds: float) -> str:
    '''A coroutine: it can pause at every `await`.'''
    print(f'{name}: starting')
    await asyncio.sleep(seconds)        # NOT time.sleep()
    print(f'{name}: finished after {seconds} s')
    return f'{name} is done'


async def main_async() -> None:
    # `await` one after the other = sequential. Two seconds in total.
    first = await worker('A', 1)
    second = await worker('B', 1)

    # What happens if you forget the await:
    coroutine = worker('C', 1)          # nothing runs here
    print(f'without await, worker() gives: {type(coroutine).__name__}')
    print(await coroutine)              # NOW it runs


def main() -> None:
    # asyncio.run creates the event loop, runs the coroutine, closes the loop.
    # Call it ONCE, at the very top of your program.
    asyncio.run(main_async())

اولین کوروتین: نمونه 10_first_coroutine/app.py

دو قاعده‌ای که همه رویشان می‌لغزند

هرگز حلقه رویداد را مسدود نکنید

time.sleep(1) داخل یک کوروتین، کل حلقه رویداد را یک ثانیه متوقف می‌کند، شامل هر کوروتین دیگری. نسخه ناهمگامش await asyncio.sleep(1) است. همین درباره requests (مسدودکننده) در برابر httpx.AsyncClient (ناهمگام) هم صادق است. برای فراخوانی مسدودکننده‌ای که چاره‌ای جز آن ندارید: await asyncio.to_thread(function, arg).

۱۱. اجرای همزمان با gather

async def main_async() -> None:
    sites = [('kadoos.ir', 1.0), ('python.org', 0.4),
             ('sqlite.org', 0.7), ('qt.io', 0.2)]

    print('sequential:')
    for site, seconds in sites:
        await fetch(site, seconds)

    print('with gather:')
    sizes = await asyncio.gather(*(fetch(site, seconds)
                                   for site, seconds in sites))
    print(f'   results in the ORDER OF THE ARGUMENTS: {sizes}')

    print('with a failure and return_exceptions=True:')
    results = await asyncio.gather(
        fetch('kadoos.ir', 0.3),
        fetch('broken.ir', 0.1),
        fetch('qt.io', 0.2),
        return_exceptions=True,
    )
    for result in results:
        if isinstance(result, Exception):
            print(f'   FAILED: {result}')
        else:
            print(f'   ok: {result} bytes')

gather: نمونه 11_gather/app.py

شرح مثال

۱۲. تسک، لغو و مهلت

async def long_job(name: str, seconds: float) -> str:
    '''A job that cleans up properly when it is cancelled.'''
    try:
        for second in range(int(seconds)):
            print(f'  {name}: working ({second + 1}/{int(seconds)})')
            await asyncio.sleep(1)
        return f'{name} finished'
    except asyncio.CancelledError:
        print(f'  {name}: cancelled -- closing the file, the socket...')
        raise            # ALWAYS re-raise: the caller must know


async def main_async() -> None:
    # -- create_task: the work starts, we do something else meanwhile -------
    task = asyncio.create_task(long_job('background', 3))
    print('  the main coroutine is free')
    await asyncio.sleep(1)
    print(f'  is it done? {task.done()}')
    print(f'  result: {await task}')

    # -- cancelling --------------------------------------------------------
    task = asyncio.create_task(long_job('endless', 10))
    await asyncio.sleep(2.5)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print('  the caller sees CancelledError')

    # -- timeout (Python 3.11+) --------------------------------------------
    try:
        async with asyncio.timeout(2):
            await long_job('slow', 10)
    except TimeoutError:
        print('  too slow: the job was cancelled for us')

تسک‌ها: نمونه 12_tasks_cancel_timeout/app.py

شرح مثال

۱۳. صف ناهمگام

همان ایده بخش ششم، ولی با کوروتین به جای نخ. و چون همه چیز در یک نخ اجرا می‌شود، اصلاً شرایط رقابتی‌ای وجود ندارد که از آن محافظت کنیم.

async def producer(queue: asyncio.Queue, count: int) -> None:
    '''Puts `count` jobs into the queue.'''
    for number in range(1, count + 1):
        await asyncio.sleep(random.uniform(0.05, 0.15))
        await queue.put(number)
        print(f'produced  {number:>2}   (queue: {queue.qsize()})')
    print('the producer has finished')


async def consumer(name: str, queue: asyncio.Queue) -> None:
    '''Takes jobs for ever, until it is cancelled.'''
    while True:
        number = await queue.get()
        try:
            await asyncio.sleep(random.uniform(0.1, 0.3))
            print(f'  {name} handled {number:>2} -> {number ** 2}')
        finally:
            # In a `finally`, so a failure cannot make join() wait for ever.
            queue.task_done()

صف ناهمگام: نمونه 14_async_queue/app.py

maxsize به شما فشار برگشتی می‌دهد: وقتی مصرف‌کننده‌ها عقب می‌مانند، صف پر می‌شود و put شروع به انتظار می‌کند، پس تولیدکننده کند می‌شود به جای اینکه همه حافظه را بخورد. تمام ترفند یک خط لوله سالم همین است.

۱۴. پروژه: ربات پیام‌رسان

ربات طبیعی‌ترین پروژه asyncio است: تقریباً همیشه منتظر شبکه است و همزمان به آدم‌های زیادی خدمت می‌کند؛ دقیقاً همان شکلی که asyncio برایش ساخته شد. کتابخانه python-telegram-bot از بالا تا پایین روی asyncio ساخته شده و برای همین هر پاسخگر یک async def است.

async def hello(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    '''Answers /hello.'''
    # update.message CAN be None (an edited message, a channel post...), so
    # check instead of silencing the type checker with a `# type: ignore`.
    if update.message is None or update.effective_user is None:
        return
    name = update.effective_user.first_name
    await update.message.reply_text(f'Hello {name}! Welcome to Kadoos.')


def main() -> None:
    # `.base_url(...)` is what makes the same library talk to Bale.
    application = (Application.builder()
                   .token(get_token())
                   .base_url(BASE_URL)
                   .build())

    application.add_handler(CommandHandler('hello', hello))
    application.add_handler(CommandHandler('dice', dice))
    application.add_handler(CommandHandler('help', help_command))

    print('The bot is running. Press Ctrl-C to stop it.')
    application.run_polling()


# The main guard matters here: without it, importing this file would start the
# bot. The first version of this course built the application at module level.
if __name__ == '__main__':
    main()

ربات: نمونه 15_messenger_bot/01_hello_bot.py

توکن یک رمز عبور است

نسخه اول این دوره، توکن زنده ربات را داخل constants.py نوشته بود و آن فایل همراه پروژه سفر می‌کرد. هر کسی که پروژه را می‌گرفت، مالک ربات می‌شد. سه قاعده: توکن و رمز و کلید API در متغیر محیطی یا فایل .env می‌نشینند، نه در کد؛ فایل .env از روز اول در .gitignore می‌رود؛ و اگر توکنی یک بار کامیت شده باشد، باید باطلش کنید، چون پاک کردن آن خط کافی نیست: توکن هنوز در تاریخچه مخزن هست.

def get_token() -> str:
    '''Read the token, or explain clearly what is missing.'''
    token = os.environ.get('BOT_TOKEN')
    if not token:
        raise SystemExit(
            'BOT_TOKEN is not set.\n\n'
            '  Windows : set BOT_TOKEN=your-token-here\n'
            '  Linux   : export BOT_TOKEN="your-token-here"\n\n'
            'Get a token from @BotFather (Telegram) or @BotFather (Bale).')
    return token

خواندن توکن از محیط: نمونه 15_messenger_bot/config.py

نکته امنیتی دوم در بخش دریافت فایل است. نام فایل را فرستنده انتخاب می‌کند، پس نامی مثل ../../passwords.txt بیرون از پوشه دانلود شما نوشته می‌شود. به این حمله پیمایش مسیر می‌گویند و دفاعش این است که فقط آخرین بخش نام را نگه دارید و بعد بررسی کنید مسیر نهایی واقعاً داخل پوشه مورد نظر است.

def safe_filename(name: str) -> str:
    '''Turn a name chosen by a stranger into a name we can safely write.'''
    # Path(...).name drops every folder: '../../etc/passwd' -> 'passwd'
    name = Path(name).name
    # Then keep only harmless characters.
    name = re.sub(r'[^A-Za-z0-9._-]', '_', name)
    # '.' and '..' survive both steps above and are not file names at all.
    if name in {'', '.', '..'}:
        return 'file.bin'
    return name

نام فایل امن: نمونه 15_messenger_bot/04_files_bot.py

فایل‌های تمرینی این فصل

پوشه S37-concurrency-threading-asyncio شامل پانزده پوشه است. نمونه‌های ۰۱ تا ۱۲ و ۱۴ فقط به کتابخانه استاندارد نیاز دارند. نمونه ۱۳ به httpx و نمونه ۱۵ به python-telegram-bot و یک توکن. نمونه‌های ۰۲ و ۰۴ را چند بار اجرا کنید: خروجی هر بار فرق می‌کند و تمام درس همین است.

خلاصه فصل

ابزاروظیفه آن
Thread(target=, args=)ساختن نخ. ویرگول تاپل را فراموش نکنید
start() و join()راه‌اندازی و انتظار. هرگز run()
Lock و with lock:محافظت از متغیر مشترک
Eventدرخواست توقف تمیز از یک نخ
queue.Queueتبادل امن داده، بدون قفل
نگهبان Noneپایان دادن به کارگرهای حلقه‌ای
ThreadPoolExecutorاستخر نخ. map یا submit
as_completedنتیجه‌ها به ترتیب آماده شدن
future.result()خطای کارگر را دوباره پرتاب می‌کند. حتماً بخوانیدش
GILیک نخ در هر لحظه. برای محاسبه، نخ بی‌فایده است
ProcessPoolExecutorچند هسته واقعی، برای محاسبه
async def و awaitکوروتین و مکث
asyncio.run()یک بار، در بالاترین سطح برنامه
asyncio.gather()اجرای همزمان، نتیجه به ترتیب ورودی
create_task و cancelکار پس‌زمینه و لغوش
asyncio.timeout()مهلت، با لغو خودکار
asyncio.Queueتولیدکننده و مصرف‌کننده، با فشار برگشتی
قانون رابط گرافیکیهرگز از نخ دیگر به ویجت دست نزنید
قانون asyncioهرگز تابع مسدودکننده در کوروتین صدا نزنید

تمرین‌های فصل سی و هفت

  1. برنامه‌ای بنویسید که ده صفحه را هر کدام با یک نخ «دانلود» کند و زمان کل را چاپ کند. با نسخه ترتیبی مقایسه کنید.
  2. از نمونه شرایط رقابتی، time.sleep را بردارید و بیست بار اجرایش کنید. آیا نتیجه غلط باز هم ظاهر می‌شود؟ این درباره باگ‌های غیرقابل بازتولید چه به شما می‌آموزد؟
  3. همان نمونه را با Lock درست کنید و بسنجید چقدر کندتر شد. بعد قفل را دور کل حلقه ببرید: چه بر سر همروندی آمد؟
  4. نخی بنویسید که از ده تا صفر بشمارد و در هر لحظه با یک Event از نخ اصلی متوقف شود.
  5. نمونه صف و کارگرها را با ThreadPoolExecutor بازنویسی کنید. چند خط حذف کردید؟
  6. با ThreadPoolExecutor ده فایل متنی را همزمان بخوانید و تعداد کلمات هر کدام را گزارش دهید. نبود یک فایل نباید بقیه را متوقف کند.
  7. در نمونه GIL اندازه کار را آنقدر کم کنید تا پردازه‌ها دیگر سریع‌تر از نسخه ترتیبی نباشند. چرا اندازه‌ای هست که پایین‌تر از آن پردازه‌ها می‌بازند؟
  8. در سه خط و با زبان خودتان توضیح دهید چرا نخ یک دانلود را سریع می‌کند و یک محاسبه را نه.
  9. تمرین اول را با asyncio.gather بازنویسی کنید.
  10. کوروتین countdown(name, seconds) بنویسید و سه تای آن را با مدت‌های متفاوت با هم اجرا کنید.
  11. به یکی از آن سه، مهلت سه ثانیه‌ای بدهید و مطمئن شوید دو تای دیگر تمام می‌شوند.
  12. در نمونه صف ناهمگام، maxsize را یک و بعد صد بگذارید. چه چیزی در خروجی عوض می‌شود و چرا؟
  13. کوروتینی بنویسید که CancelledError را بگیرد و دوباره پرتاب نکند. چه بر سر asyncio.timeout می‌آید؟
  14. به ربات دستور /weather <city> اضافه کنید که یک REST API عمومی را با httpx.AsyncClient صدا بزند. چرا آنجا نباید از requests استفاده کنید؟
  15. برنامه شما باید پانصد عکس را تغییر اندازه دهد. نخ، پردازه یا asyncio؟ و اگر باید همزمان به پنج هزار کاربر چت جواب بدهد چه؟
فصل قبل: PySide6 و ساخت فایل اجرایی فصل بعد: وب با Flask