Python Programming

باز کردن سرعت پایتون: نگاهی عمیق به همزمانی و پردازش موازی

یکی از خرافات پرتکرار در جامعه پایتون این است که این زبان ذاتاً کند است. اگرچه درست است که پایتون دارای قفل مفسر جهانی (GIL) است که اجرای موازی واقعی بایت‌کدها را در CPython استاندارد جلوگیری می‌کند، اما این بدان معنا نیست که نمی‌تواند بارهای کاری با ورودی/خروجی (I/O) یا پردازنده (CPU) بالا را به‌طور کارآمد مدیریت کند. کلید موفقیت در درک تفاوت بین همزمانی (Concurrency) و موازی‌سازی (Parallelism) و انتخاب ابزار مناسب برای هر کار است.

برای توسعه‌دهندگان متوسط تا پیشرفته، تسلط بر این مفاهیم تنها به معنای نوشتن کد سریع‌تر نیست؛ بلکه به معنای معماری سیستم‌هایی است که قابلیت مقیاس‌پذیری دارند. این مقاله به بررسی ظرافت‌های مدل‌های همزمانی پایتون می‌پردازد و استراتژی‌های عملی برای بهره‌گیری از پردازش موازی ارائه می‌دهد.

همزمانی در مقابل موازی‌سازی: تعریف اصطلاحات

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

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

Threadها: قهرمان کارهای وابسته به I/O

ماژول threading پایتون رویکرد سنتی برای همزمانی است. با این حال، به دلیل وجود GIL، Threadها برای وظایف وابسته به پردازنده (CPU-bound) مناسب نیستند. اگر پنج Thread را برای انجام محاسبات سنگین ریاضی ایجاد کنید، احتمالاً این Threadها به صورت ترتیبی و یکی پس از دیگری اجرا خواهند شد، زیرا در هر لحظه فقط یک Thread می‌تواند GIL را در اختیار داشته باشد.

با این حال، وقتی صحبت از وظایف وابسته به I/O (مانند درخواست‌های شبکه، خواندن فایل یا کوئری‌های پایگاه داده) می‌شود، Threadها بسیار مؤثر هستند. وقتی یک Thread منتظر I/O می‌ماند، GIL را آزاد می‌کند و به Threadهای دیگر اجازه اجرا می‌دهد. این ویژگی، Threadها را برای ساخت سرورهای شبکه با توان عبور داده (Throughput) بالا یا ربات‌های جمع‌آوری اطلاعات (Scrapers) ایده‌آل می‌کند.

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

import concurrent.futures
import urllib.request

URLS = ['http://www.foxnews.com/', 'http://www.cnn.com/', 'http://espn.com/']

def load_url(url, timeout):
    with urllib.request.urlopen(url, timeout=timeout) as conn:
        return conn.read()

with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
    for future in concurrent.futures.as_completed(future_to_url):
        url = future_to_url[future]
        try:
            data = future.result()
        except Exception as exc:
            print('%r generated an exception: %s' % (url, exc))
        else:
            print('%r page is %d bytes' % (url, len(data)))

Multiprocessing: دور زدن GIL برای وظایف CPU

برای وظایف وابسته به پردازنده، مانند پردازش تصویر، تحلیل داده یا شبیه‌سازی‌های پیچیده، Threadها بهبودی در عملکرد ایجاد نخواهند کرد. برای دستیابی به موازی‌سازی واقعی، باید از چندین فرآیند (Process) استفاده کنید. ماژول multiprocessing یک کپی محلی از مفسر پایتون را برای هر فرآیند فراهم می‌کند که کاملاً GIL را دور می‌زند.

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

from multiprocessing import Pool

def square(n):
    return n * n

if __name__ == '__main__':
    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    with Pool(4) as p:
        results = p.map(square, numbers)
    print(results)

Asyncio: استاندارد مدرن برای همزمانی I/O

در سال‌های اخیر، asyncio به استاندارد اصلی برای مدیریت I/O با همزمانی بالا در پایتون تبدیل شده است. برخلاف Threadها که به سوییچینگ زمینه (Context Switching) مدیریت شده توسط سیستم‌عامل وابسته هستند، asyncio از یک حلقه رویداد تک‌نخی برای مدیریت چندوظیفگی مشارکتی (Cooperative Multitasking) استفاده می‌کند. این رویکرد بسیار سبک و کارآمد است و به عنوان ستون فقرات فریم‌ورک‌های وب مدرن مانند FastAPI و Django Channels عمل می‌کند.

Asyncio به شما امکان می‌دهد کدی ناهمگام (Asynchronous) بنویسید که ظاهری همگام (Synchronous) دارد؛ این امر خوانایی را بهبود بخشیده و در عین حال عملکرد بالا را حفظ می‌کند.

import asyncio
import aiohttp

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in ['http://example.com', 'http://python.org']]
        results = await asyncio.gather(*tasks)
        print(results)

asyncio.run(main())

نتیجه‌گیری

انتخاب مدل همزمانی مناسب در پایتون به معنای انتخاب «بهترین» آن‌ها نیست، بلکه انتخاب مدلی است که با گلوگاه خاص شما سازگار باشد. از Threadها برای وظایف ساده و مسدودکننده I/O استفاده کنید. از Multiprocessing زمانی که نیاز به حداکثر استفاده از CPU برای محاسبات سنگین دارید استفاده کنید. و از Asyncio زمانی که در حال ساخت برنامه‌های ناهمگام با توان عبور داده بالا و مقیاس‌پذیر هستید استفاده کنید.

با درک این ابزارها و ملاحظات آن‌ها، می‌توانید برنامه‌های پایتونی بنویسید که نه تنها صحیح، بلکه با عملکرد بالا و مقیاس‌پذیر باشند. اجازه ندهید GIL پتانسیل شما را محدود کند؛ از امروز برای موازی‌سازی معماری‌سازی کنید.

Share: