وقتی تعداد درخواست‌ها از ظرفیت سرویس بیشتر می‌شود

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

پارامتر buffersize از Python 3.14 به Executor.map اضافه شده است. با تعیین این مقدار، تولیدکننده وقتی ظرفیت نتایج ارسال‌شده اما تحویل‌داده‌نشده پر می‌شود، برای ادامهٔ مصرف ورودی منتظر می‌ماند. خروجی map همچنان به ترتیب ورودی تحویل داده می‌شود؛ بنابراین یک کار کند در ابتدای صف می‌تواند تحویل چند نتیجهٔ آماده را عقب بیندازد. این ویژگی را با تضمین سرعت بیشتر یا پایان هم‌زمان همهٔ درخواست‌ها اشتباه نگیرید.

مرز صف، ترتیب نتیجه و رفتار خطا

برای پروژه‌ای که هنوز روی نسخه‌های قبلی پایتون اجرا می‌شود، نمونهٔ bounded_map همین قرارداد اصلی را به‌صورت روشن پیاده می‌کند. تنها به‌اندازهٔ pending کار ارسال می‌شود و بعد از مصرف هر نتیجه، ورودی بعدی خوانده می‌شود. این طراحی برای ورودی مولد مناسب است و نیاز ندارد کل فهرست از ابتدا در حافظه باشد. مقدار workers ظرفیت اجرای هم‌زمان است و pending سقف کارهای ارسال‌شدهٔ این پنجره را تعیین می‌کند.

کنترل صف جای timeout شبکه را نمی‌گیرد. هر تماس بیرونی باید زمان انتظار مشخص داشته باشد و خطای قابل پیگیری تولید کند. لغو یک Future که از قبل در حال اجرا است، نخ را به‌اجبار متوقف نمی‌کند؛ خروج از محدودهٔ executor نیز ممکن است منتظر همان کار بماند. در نمونهٔ آموزشی، خطا به فراخواننده برمی‌گردد و کارهای صف‌شده‌ای که امکان لغو دارند لغو می‌شوند، اما برای سرویس واقعی همچنان به سیاست زمان انتظار نیاز دارید.

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

فایل‌های کامل، روش اجرا و آزمون

Python 3.11+ و فقط کتابخانهٔ استاندارد؛ فایل API جدید به Python 3.14+ نیاز دارد. چهار فایل زیر را با همین نام‌ها در یک پوشه ذخیره کنید؛ هیچ بخش کد با سه‌نقطه جایگزین نشده است.

1. صف محدود و مرتب

نام فایل: bounded_workers.py؛ این فایل را کامل و بدون حذف سطرها ذخیره کنید.

from collections import deque
from concurrent.futures import ThreadPoolExecutor
from itertools import islice


def bounded_map(function, values, *, workers=4, pending=8):
    """Yield ordered results while retaining at most pending submitted jobs."""
    if workers < 1 or pending < 1:
        raise ValueError('workers and pending must be positive')
    source = iter(values)
    with ThreadPoolExecutor(max_workers=workers) as pool:
        jobs = deque(pool.submit(function, value)
                     for value in islice(source, pending))
        try:
            while jobs:
                yield jobs.popleft().result()
                try:
                    value = next(source)
                except StopIteration:
                    continue
                jobs.append(pool.submit(function, value))
        finally:
            for job in jobs:
                job.cancel()

این فایل همهٔ وابستگی‌های خود را وارد می‌کند و bounded_map را صادر می‌کند. سقف pending هنگام ارسال رعایت می‌شود؛ پایان executor به معنای توقف اجباری نخ فعال نیست.

2. API جدید در Python 3.14 و بالاتر

نام فایل: python314_map.py؛ این فایل را کامل و بدون حذف سطرها ذخیره کنید.

import inspect
from concurrent.futures import ThreadPoolExecutor


def main():
    if 'buffersize' not in inspect.signature(ThreadPoolExecutor.map).parameters:
        print('SKIP: this example requires Python 3.14 or newer')
        return
    with ThreadPoolExecutor(max_workers=4) as pool:
        results = list(pool.map(lambda n: n * n, range(100), buffersize=8))
    assert results == [n * n for n in range(100)]
    print(sum(results))  # 328350


if __name__ == '__main__':
    main()

با python python314_map.py اجرا کنید. نتیجهٔ نسخهٔ پشتیبانی‌شده 328350 است؛ در نسخهٔ قدیمی‌تر به‌روشنی SKIP چاپ می‌شود.

3. آزمون ظرفیت، ترتیب و خطا

نام فایل: test_bounded_workers.py؛ این فایل را کامل و بدون حذف سطرها ذخیره کنید.

import unittest
from bounded_workers import bounded_map


class BoundedMapTests(unittest.TestCase):
    def test_order_and_bounded_input_consumption(self):
        consumed = []
        def source():
            for value in range(20):
                consumed.append(value)
                yield value
        stream = bounded_map(lambda n: n * n, source(), workers=2, pending=4)
        self.assertEqual(next(stream), 0)
        self.assertEqual(len(consumed), 4)
        self.assertEqual(next(stream), 1)
        self.assertEqual(len(consumed), 5)
        self.assertEqual(list(stream), [n * n for n in range(2, 20)])

    def test_worker_error_is_not_silently_dropped(self):
        def fail(value):
            if value == 2:
                raise RuntimeError('invalid item')
            return value
        with self.assertRaisesRegex(RuntimeError, 'invalid item'):
            list(bounded_map(fail, range(10), pending=3))

    def test_invalid_capacity(self):
        for options in ({'workers': 0}, {'pending': 0}):
            with self.assertRaises(ValueError):
                list(bounded_map(str, [1], **options))


if __name__ == '__main__':
    unittest.main()

با python -m unittest test_bounded_workers.py هر سه آزمون را اجرا کنید. تعداد ورودی‌های خوانده‌شده نیز سنجیده می‌شود تا محدودبودن صف فقط یک ادعا نباشد.

4. اجرای خط فرمان با تنظیم ظرفیت

نام فایل: run_workers.py؛ این فایل را کامل و بدون حذف سطرها ذخیره کنید.

import argparse
import json
from bounded_workers import bounded_map


parser = argparse.ArgumentParser()
parser.add_argument('--count', type=int, default=100)
parser.add_argument('--workers', type=int, default=4)
parser.add_argument('--pending', type=int, default=8)
args = parser.parse_args()
if args.count < 0:
    parser.error('--count must not be negative')
values = list(bounded_map(lambda n: n * n, range(args.count),
                          workers=args.workers, pending=args.pending))
print(json.dumps({'count': len(values), 'sum': sum(values)}))

اجرای python run_workers.py --workers 2 --pending 4 باید count برابر 100 و sum برابر 328350 بدهد. ظرفیت صفر و تعداد منفی پذیرفته نمی‌شود.

روش اجرای مثال‌ها و انتخاب ظرفیت واقعی

چهار فایل این آموزش را کنار هم ذخیره کنید و با python -m unittest test_bounded_workers.py اجرا بگیرید. فایل run_workers.py با ورودی پیش‌فرض صد نتیجه و مجموع 328350 می‌دهد. فایل python314_map.py روی Python 3.14 یا جدیدتر همان نتیجه را با API جدید تولید می‌کند و روی نسخهٔ قدیمی‌تر پیام SKIP دارد. برای استقرار، ابتدا ظرفیت کم انتخاب کنید، تأخیر و حافظه را اندازه بگیرید و اعداد را با سقف سرویس مقصد هماهنگ کنید.

چک‌لیست اجرای عملی

  • آزمون مصرف محدود ورودی، ترتیب خروجی و انتشار خطا را اجرا کنید.
  • برای هر تماس شبکه timeout واقعی تعیین کنید.
  • pending و workers را دو تنظیم جداگانه نگه دارید.
  • در Python 3.14+ نمونهٔ buffersize را نیز اجرا کنید.

منابع

تدوین و راهنمای عملی: دانشنامه لیان.