FA-TOOLS — Header Component
آموزش concurrent.futures در پایتون

آموزش concurrent.futures در پایتون: اجرای همزمان برنامه‌ها مثل یک حرفه‌ای

تا حالا شده کدی بنویسید که اجرای آن به خاطر دانلود صدها فایل یا انجام محاسبات ریاضی سنگین، دقیقه‌ها یا حتی ساعت‌ها طول بکشد؟ در دنیای مدرن برنامه‌نویسی، زمان طلاست. پایتون برای حل این مشکل ابزار قدرتمندی به نام ماژول concurrent.futures را در اختیار ما قرار داده است. این کتابخانه به شما اجازه می‌دهد تا کارهای خود را به صورت همزمان (Concurrency) یا موازی (Parallelism) اجرا کنید و سرعت برنامه‌های خود را به شدت افزایش دهید. در این مقاله به زبان ساده، کاربردی و عمیق یاد می‌گیرید که چطور از این قابلیت برای بهینه‌سازی پروژه‌های خود استفاده کنید.

نقشه راه انتخاب ابزار در concurrent.futures

🧵 ThreadPoolExecutor

مناسب برای کارهای وابسته به شبکه و دیسک (I/O-Bound)

  • دانلود تصاویر و صفحات وب
  • خواندن و نوشتن فایل‌های حجیم
  • ارتباط با دیتابیس و APIها

⚡ ProcessPoolExecutor

مناسب برای پردازش‌های سنگین پردازنده (CPU-Bound)

  • پردازش تصویر و ویدیو
  • محاسبات سنگین ریاضی و هوش مصنوعی
  • فشرده‌سازی اطلاعات و رمزنگاری

۱. چرا به concurrent.futures نیاز داریم؟

آموزش concurrent.futures در پایتون — تصویر 1

به طور پیش‌فرض، پایتون کدهای شما را به صورت تک‌رشته‌ای (Single-threaded) اجرا می‌کند. این یعنی خط به خط برنامه شما منتظر می‌ماند تا خط قبلی به طور کامل تمام شود. حالا فرض کنید می‌خواهید ۲۰ سایت مختلف را بررسی کنید و وضعیت پاسخ آن‌ها را بگیرید. اگر بررسی هر سایت ۲ ثانیه طول بکشد، کل برنامه شما ۴۰ ثانیه معطل خواهد شد!

برای رهایی از این محدودیت، پایتون راه‌های مختلفی مثل threading و multiprocessing دارد. اما کار با این کتابخانه‌ها در سطح پایین (Low-level) بسیار پیچیده است و نیاز به قفل‌گذاری (Locks)، مدیریت صف‌ها و کنترل سخت خطاها دارد. اینجاست که ماژول concurrent.futures به عنوان یک رابط کاربری سطح بالا و بسیار ساده وارد میدان می‌شود تا با چند خط کد، تمام این هماهنگی‌ها را پشت صحنه انجام دهد. با استفاده از این ابزار، نوشتن کدهای برنامه‌نوسی همزمان در پایتون به یک کار لذت‌بخش تبدیل می‌شود.

۲. آشنایی با ساختار و مفاهیم اصلی ماژول

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

  • ThreadPoolExecutor: مدیریت ریسمان‌ها (Threads) را بر عهده دارد. برای فرآیندهایی که بیشتر وقت خود را منتظر پاسخ سخت‌افزار خارجی، شبکه یا کاربر می‌گذرانند عالی است.
  • ProcessPoolExecutor: وظایف را به پروسس‌های مجزا می‌سپارد. از آنجا که هر پروسس مفسر پایتون و حافظه اختصاصی خودش را دارد، می‌تواند قفل فریم‌ورک پایتون (GIL) را دور بزند و از تمام هسته‌های پردازنده شما استفاده کند.

💡 نکته طلایی برای سئو و پرفورمنس:

اگر پروژه شما به اینترنت، دیتابیس یا فایل سیستم متصل است از ThreadPoolExecutor و اگر محاسبات سنگین ریاضی دارید از ProcessPoolExecutor استفاده کنید.

۳. کار با ThreadPoolExecutor (بهینه‌سازی I/O)

آموزش concurrent.futures در پایتون — تصویر 3

بیایید با یک مثال واقعی شروع کنیم. فرض کنید می‌خواهیم زمان تاخیر چند وب‌سایت مختلف را اندازه بگیریم. ابتدا کد را به روش معمولی می‌نویسیم و سپس با استفاده از ThreadPoolExecutor آن را چند برابر سریع‌تر می‌کنیم.

در ادامه نمونه کدی را مشاهده می‌کنید که وظیفه شبیه‌سازی دانلود اطلاعات از اینترنت را دارد:

import concurrent.futures
import time
import requests

URLS = [
    'https://httpbin.org/delay/2',
    'https://httpbin.org/delay/1',
    'https://httpbin.org/delay/3',
    'https://httpbin.org/delay/1'
]

def download_site(url):
    print(f"شروع دانلود از: {url}")
    response = requests.get(url)
    print(f"پایان دانلود: {url} با کد وضعیت {response.status_code}")
    return response.status_code

# استفاده از ThreadPoolExecutor برای مدیریت ریسمان‌ها
start_time = time.time()

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
    # متد map کارهای تکراری را به صورت همزمان روی آیتم‌های لیست اجرا می‌کند
    results = executor.map(download_site, URLS)

print("nنتایج دریافت شده:")
for url, status in zip(URLS, results):
    print(f"{url} -> {status}")

end_time = time.time()
print(f"کل زمان اجرا: {end_time - start_time:.2f} ثانیه")

تحلیل کد: در حالت عادی، اجرای این کد حدود ۷ ثانیه زمان می‌برد (۲+۱+۳+۱ ثانیه). اما به لطف استفاده از ThreadPoolExecutor و اجرای همزمان، کل فرآیند تنها در حدود ۳ ثانیه (به اندازه طولانی‌ترین درخواست) به پایان می‌رسد! ویژگی max_workers مشخص می‌کند که حداکثر چند کارگر یا نخ به طور همزمان حق فعالیت دارند.

۴. کار با ProcessPoolExecutor (بهینه‌سازی محاسبات سنگین)

اگر با محاسبات سنگین ریاضی مثل پیدا کردن اعداد اول یا پردازش ماتریس‌ها سر و کار دارید، Threadها به کارتان نمی‌آیند زیرا به دلیل وجود مکانیزم GIL در پایتون، ریسمان‌ها نمی‌توانند به طور واقعی همزمان روی چند هسته پردازنده اجرا شوند. برای عبور از این سد، باید به سراغ ProcessPoolExecutor برویم. این مجری، وظایف را به پروسس‌های جداگانه در سیستم‌عامل هدایت می‌کند.

import concurrent.futures
import time

def cpu_bound_task(number):
    print(f"شروع پردازش عدد {number}")
    # شبیه‌سازی یک کار محاسباتی بسیار سنگین
    count = 0
    for i in range(10_000_000):
        count += i
    print(f"اتمام پردازش عدد {number}")
    return count

if __name__ == '__main__':
    numbers = [50, 60, 70, 80]
    start = time.time()

    # استفاده از تمام ظرفیت پردازنده با ساخت پروسس‌های موازی
    with concurrent.futures.ProcessPoolExecutor() as executor:
        results = executor.map(cpu_bound_task, numbers)
        
    print(list(results))
    print(f"کل زمان محاسبات: {time.time() - start:.2f} ثانیه")

توجه داشته باشید که کدهای مربوط به پردازش موازی حتماً باید درون شرط if __name__ == '__main__': قرار بگیرند؛ در غیر این صورت برنامه در سیستم‌عامل‌های مختلف دچار مشکل چرخه بی‌نهایت و خطاهای سیستمی می‌شود.

۵. جدول مقایسه جامع Thread و Process

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

ویژگی / سناریو گزینه مناسب و علت آن
کارهای شبکه و API (وب اسکرپینگ) ThreadPoolExecutor؛ چرا که گلوگاه برنامه سرعت شبکه است و سیستم معطل دریافت داده‌ها می‌ماند.
ریاضیات، رمزنگاری و هوش مصنوعی ProcessPoolExecutor؛ به دلیل دور زدن GIL و استفاده از قدرت واقعی تمامی هسته‌های CPU شما.
مصرف حافظه رم (RAM) ThreadPoolExecutor؛ نخ‌ها حافظه مشترک دارند و سبک‌تر هستند در حالی که پروسس‌ها کپی کامل مفسر را در رم لود می‌کنند.
به اشتراک‌گذاری آسان متغیرها ThreadPoolExecutor؛ به دلیل دسترسی مستقیم به فضای حافظه سراسری برنامه.

۶. شیء Future چیست و چگونه آن را مدیریت کنیم؟

وقتی کاری را به یک مجری می‌سپارید، فوراً یک شیء از نوع Future به شما برگردانده می‌شود. این شیء در واقع یک رسید یا وعده است که به شما می‌گوید: «من این کار را در پس‌زمینه شروع کرده‌ام، هر زمان که تمام شد می‌توانی نتیجه‌اش را از من تحویل بگیری».

برخلاف متد map که نتایج را دقیقاً به همان ترتیب ورودی برمی‌گرداند، با ترکیب متد submit و تابع as_completed می‌توانید نتایج را به محض آماده شدن دریافت کنید. این کار سرعت دسترسی به خروجی‌های اولیه را در تکنیک‌های توسعه نرم‌افزار به شدت بالا می‌برد.

import concurrent.futures
import time

def process_data(name, delay):
    time.sleep(delay)
    return f"بخش {name} پردازش شد."

with concurrent.futures.ThreadPoolExecutor() as executor:
    # ثبت کارهای پس‌زمینه با متد submit
    future_to_task = {
        executor.submit(process_data, 'A', 3): 'کار اول',
        executor.submit(process_data, 'B', 1): 'کار دوم',
        executor.submit(process_data, 'C', 2): 'کار سوم'
    }

    # دریافت نتایج به محض آماده شدن (به ترتیب سرعت اتمام کارها)
    for future in concurrent.futures.as_completed(future_to_task):
        task_name = future_to_task[future]
        try:
            result = future.result()
            print(f"نتیجه {task_name}: {result}")
        except Exception as e:
            print(f"کار {task_name} با خطای مواجه شد: {e}")

خروجی این تکه کد نشان می‌دهد که کار دوم (با تاخیر ۱ ثانیه) زودتر از کار اول (با تاخیر ۳ ثانیه) چاپ می‌شود. این همزان‌سازی هوشمندانه به شما کمک می‌کند رابط کاربری یا سرویس‌های پویاتری بسازید.

۷. عیب‌یابی سریع و رفع خطاهای رایج (Troubleshooting)

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

🚨 مشکل ۱: برنامه در ویندوز دچار کرش ناگهانی یا خطای BrokenProcessPool می‌شود

علت: سیستم‌عامل ویندوز فاقد قابلیت Forking برای ساخت پروسس جدید است و باید کل کد را دوباره لود کند.
راه‌حل: حتماً تمام منطق مربوط به شروع کار با ProcessPoolExecutor را در بلوک محافظت شده زیر قرار دهید:
if __name__ == '__main__':

🚨 مشکل ۲: خطاها در زمان اجرا به من نمایش داده نمی‌شوند و برنامه در سکوت می‌میرد

علت: وقتی از ریسمان‌ها استفاده می‌کنید، استثناها داخل شیء Future ذخیره می‌شوند و تا زمانی که خروجی را با متد .result() فراخوانی نکنید، ارور پرتاب نمی‌شود.
راه‌حل: همیشه کدهای خود را با استفاده از بلاک‌های try-except درون کدهای کارگر مدیریت کنید یا مطمئن شوید که future.result() را بررسی می‌کنید.

🚨 مشکل ۳: خطای TypeError: cannot pickle… در پروسس‌ها رخ می‌دهد

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

۸. سوالات متداول (FAQ)

در این بخش به چند سوال پرتکرار شما در زمینه مدیریت همزمانی در پایتون پاسخ می‌دهیم تا تمام ابهامات برطرف شود.

❓ چه تفاوتی بین map و submit در این ماژول وجود دارد؟

متد map برای مواقعی است که می‌خواهید یک تابع یکسان را روی تعداد زیادی داده متفاوت اجرا کنید و ترتیب خروجی برای شما مهم است. اما متد submit برای ارسال وظایف متفاوت به مفسر استفاده می‌شود و کنترل بیشتری روی شیء Future به شما می‌دهد.

❓ آیا تعداد کارگرها (max_workers) را هر چقدر بخواهیم می‌توانیم بالا ببریم؟

خیر. برای پردازش‌های سنگین (ProcessPool) بهترین تعداد کارگر معادل با تعداد هسته‌های فیزیکی CPU شماست. بالا بردن این عدد باعث سربار شدید سیستم و کندی کار می‌شود. برای کارهای تحت شبکه (ThreadPool) می‌توانید این مقدار را تا چند برابر هسته‌های سیستم نیز افزایش دهید (معمولاً بین ۱۰ تا ۳۲ عالی است).

❓ چطور می‌توان یک کار در حال اجرا را لغو کرد؟

می‌توانید از متد future.cancel() استفاده کنید. توجه داشته باشید کارهایی که در صف انتظار هستند به راحتی لغو می‌شوند، اما کاری که پردازش آن از قبل توسط سیستم شروع شده باشد را معمولاً نمی‌توان در حین اجرا متوقف کرد.

جمع‌بندی و گام‌های بعدی

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

Table of Contents

آخرین نوشته‌ها