
کلاس Pool چیست؟
کلاس Pool در ماژول multiprocessing یک «استخر» از پردازههای کارگر (worker) میسازد و کارها را بین آنها پخش میکند. بهجای اینکه خودتان دستی برای هر کار یک Process بسازید، start و join کنید، فقط یک فهرست از دادهها را به Pool میدهید و آن نتایج را بهصورت موازی محاسبه و جمعآوری میکند. این کلاس مخصوصاً وقتی مفید است که میخواهید یک تابع را روی دادههای زیادی بهصورت موازی اجرا کنید.
مثال پایه: محاسبهی مربع اعداد
یک Pool با تعداد مشخصی پردازه میسازیم و با map یک تابع را روی همهی عناصر یک فهرست اجرا میکنیم:
from multiprocessing import Pool
def square(x):
return x * x
if __name__ == "__main__":
numbers = [1, 2, 3, 4, 5]
# create a Pool with 3 worker processes
with Pool(processes=3) as pool:
results = pool.map(square, numbers)
print(results) # [1, 4, 9, 16, 25]عدد ۳ میگوید سه پردازهی موازی ساخته شود. دستور with کمک میکند که پس از پایان کار، Pool بهطور خودکار بسته و منابعش آزاد شود (جلوگیری از نشت منابع). results فهرست نتایج را به همان ترتیب ورودی نگه میدارد.
متدهای مهم Pool
Pool چند روش برای ارسال کار دارد که هرکدام جای خودش را دارد:
map(func, iterable): تابع را روی هر عنصر اجرا میکند و نتایج را به ترتیب ورودی برمیگرداند (منتظر پایان همه میماند).starmap(func, iterable): مثلmapاما برای توابعی که چند آرگومان میگیرند؛ هر عنصر یک tuple از آرگومانهاست.apply(func, args): یک بار تابع را با آرگومانهای دادهشده اجرا میکند و منتظر نتیجه میماند.apply_async/map_async: نسخههای غیرمسدودکننده که فوراً یک شیء نتیجه برمیگردانند؛ بعداً با.get()نتیجه را میگیرید.imap(func, iterable): نسخهی «تنبل» (lazy) که نتایج را یکییکی و بهمحض آمادهشدن بهصورت iterator میدهد؛ برای ورودیهای خیلی بزرگ مناسب است.
from multiprocessing import Pool
def square(x):
return x * x
def add(a, b):
return a + b
if __name__ == "__main__":
with Pool(processes=3) as pool:
print(pool.map(square, [1, 2, 3, 4, 5])) # [1, 4, 9, 16, 25]
print(pool.starmap(add, [(1, 2), (3, 4), (5, 6)])) # [3, 7, 11]
result = pool.apply_async(square, (10,)) # non-blocking
print(result.get()) # 100
for r in pool.imap(square, [1, 2, 3]): # results as they arrive
print(r)اندازهی مناسب Pool
اگر پارامتر processes را ندهید، Pool بهصورت پیشفرض به تعداد هستههای پردازنده کارگر میسازد. برای کارهای CPU-bound معمولاً همین انتخاب خوبی است. میتوانید تعداد هستهها را با multiprocessing.cpu_count() بگیرید:
import multiprocessing
print("cores:", multiprocessing.cpu_count())
with multiprocessing.Pool() as pool: # defaults to cpu_count() workers
...مثال – کار سنگین موازی روی چند هسته
مهمترین کاربرد Pool، موازیکردن کارهای CPU-bound (مثل محاسبات سنگین، پردازش تصویر یا رمزنگاری) روی چند هسته است. چون هر پردازه GIL مستقل خودش را دارد، این کارها واقعاً موازی اجرا میشوند. مقایسهی اجرای ترتیبی و با Pool:
import time
from multiprocessing import Pool
def heavy(n):
total = 0
for i in range(n):
total += i
return total
if __name__ == "__main__":
tasks = [10_000_000] * 4
start = time.time()
[heavy(t) for t in tasks] # sequential
print("sequential: %.2fs" % (time.time() - start))
start = time.time()
with Pool() as pool: # parallel across cores
pool.map(heavy, tasks)
print("pool: %.2fs" % (time.time() - start))روی یک ماشین چهارهستهای، نسخهی Pool چند برابر سریعتر از نسخهی ترتیبی است. (بهیاد داشته باشید که این سود فقط برای کارهای CPU-bound است؛ برای کارهای I/O-bound معمولاً threading یا asyncio انتخاب بهتریاند.)
جمعبندی
Poolیک استخر از پردازههای کارگر میسازد و کارها را بین آنها پخش میکند؛ عالی برای اجرای یک تابع روی دادههای زیاد بهصورت موازی.map(تکآرگومان)،starmap(چند آرگومان)،apply_async/map_async(غیرمسدودکننده) وimap(تنبل) روشهای ارسال کارند.- با
withکار کنید تا Pool خودکار بسته شود؛ اندازهی پیشفرض برابر تعداد هستههاست. - Pool برای کارهای CPU-bound سرعت را چند برابر میکند؛ برای I/O از threading/asyncio استفاده کنید.