آموزش ماژول multiprocessing – Manager و حافظه مشترک

Please login to bookmark Close

این کد را اجرا کنید و حدس بزنید چه چاپ می‌شود. یک dict معمولی می‌سازیم، پنج پردازش راه می‌اندازیم و هرکدام یک کلید به آن اضافه می‌کند:

import multiprocessing

def add_item(data, worker_id):
    data[worker_id] = worker_id * worker_id   # writes into the child's OWN copy

if __name__ == '__main__':
    data = {}                                 # a plain dict
    procs = [multiprocessing.Process(target=add_item, args=(data, i)) for i in range(5)]
    for p in procs: p.start()
    for p in procs: p.join()
    print(data)                               # prints: {}

خروجی یک دیکشنریِ خالی است: {}. همه‌ی تغییرات ناپدید شدند! چرا؟ چون برخلافِ تردها که حافظه را با هم شریکند، هر پردازش فضای حافظهٔ جدا و مستقل خودش را دارد. وقتی پردازشِ فرزند ساخته می‌شود، یک نسخهٔ جداگانه از data دریافت می‌کند؛ پس هر پردازش فقط نسخهٔ خودش را تغییر می‌دهد و پردازشِ اصلی چیزی نمی‌بیند.

راه‌حل: اشتراک‌گذاری داده با Manager و حافظه مشترک

برای حلِ این مشکل، ماژول multiprocessing ابزاری به نام Manager دارد. Manager یک پردازشِ جداگانه راه می‌اندازد که دادهٔ مشترک را در خود نگه می‌دارد؛ بقیهٔ پردازش‌ها برای خواندن یا تغییرِ آن، در پس‌زمینه با همین پردازش پیام رد و بدل می‌کنند (یعنی یک proxy). پس هر تغییری که یک پردازش اعمال کند، بقیه هم می‌بینند.

def add_item(data, worker_id):
    data[worker_id] = worker_id * worker_id   # goes through the manager proxy

if __name__ == '__main__':
    manager = multiprocessing.Manager()
    data = manager.dict()                      # a SHARED dict, not a plain one
    procs = [multiprocessing.Process(target=add_item, args=(data, i)) for i in range(5)]
    for p in procs: p.start()
    for p in procs: p.join()
    print(dict(data))   # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

فقط یک خط عوض شد — manager.dict() به‌جای {} — و حالا هر پنج پردازش روی همان دادهٔ واقعی کار می‌کنند. از همین شیء manager می‌توانید انواعِ دیگری هم بسازید: list، dict، Value، Array، و حتی Lock.

مثال واقعی: شمارندهٔ مشترک و دامِ race condition

فرض کنید چند پردازش، تعدادِ درخواست‌های پردازش‌شده را در یک شمارندهٔ مشترک جمع می‌زنند. درست مثلِ درسِ threading، «مشترک بودن» به تنهایی امن نیست:

def increment(shared, lock, worker_id):
    with lock:                          # make read-modify-write atomic
        shared['counter'] += 1          # unsafe without the lock

if __name__ == '__main__':
    manager = multiprocessing.Manager()
    shared = manager.dict()
    shared['counter'] = 0
    lock = manager.Lock()               # a lock the children can share too

    procs = [multiprocessing.Process(target=increment, args=(shared, lock, i))
             for i in range(50)]
    for p in procs: p.start()
    for p in procs: p.join()
    print(shared['counter'])            # reliably 50

چرا Lock لازم است؟ چون shared['counter'] += 1 یک عملِ سه‌مرحله‌ای است: خواندن، افزایش، نوشتن. اگر دو پردازش هم‌زمان مقدار را بخوانند، هر دو روی عددِ قدیمی کار می‌کنند و یک افزایش گم می‌شود — همان race conditionی که اول دوره دیدیم. Manager داده را «مشترک» می‌کند، اما «اتمی بودن» را نه؛ آن را Lock فراهم می‌کند.

گزینهٔ سریع‌تر: Value و Array

برای دادهٔ عددی و ساده، راهِ سبک‌تری هم هست که نیازی به پردازشِ جداگانهٔ Manager ندارد: Value و Array. این دو مستقیماً روی حافظهٔ مشترکِ خام (shared memory) کار می‌کنند و معمولاً سریع‌تر از Manager هستند، اما فقط انواعِ ساده را نگه می‌دارند.

counter = multiprocessing.Value('i', 0)   # 'i' = signed int, initial value 0
with counter.get_lock():                  # Value has its own built-in lock
    counter.value += 1

arr = multiprocessing.Array('i', [0, 0, 0])   # a shared array of 3 ints

کاراکترِ 'i' نوعِ داده (عددِ صحیح) را مشخص می‌کند و مقدار از طریقِ .value خوانده و نوشته می‌شود. توجه کنید که Value یک قفلِ درونی هم دارد که با get_lock() در دسترس است.

کدام را کی انتخاب کنیم؟

Manager انعطاف‌پذیر است و ساختارهای پیچیده (dict و list تودرتو) را پشتیبانی می‌کند، اما چون هر دسترسی یک پیامِ شبکه‌ای بینِ پردازش‌هاست، کندتر است. Value/Array سریع‌ترند ولی فقط برای اعداد و آرایه‌های ساده. دامِ رایج: فراموش کردنِ Lock. اشتراک‌گذاری خودبه‌خود عملیات را امن نمی‌کند، و نتیجه‌ی فراموشی، اعدادِ اشتباهِ تصادفی است که دیباگِ آن دردسرساز است.

جمع‌بندی

  • پردازش‌ها حافظه را شریک نمی‌شوند؛ یک dict یا list معمولی بینِ پردازش‌ها مشترک نیست و تغییرات گم می‌شوند.
  • برای دادهٔ مشترکِ واقعی از Manager (انعطاف‌پذیر) یا برای اعدادِ ساده از Value/Array (سریع) استفاده کنید.
  • اشتراک‌گذاری مساویِ ایمنی نیست؛ هر جا چند پردازش هم‌زمان داده را تغییر می‌دهند، با Lock از race condition جلوگیری کنید.
  • Value یک قفلِ درونی دارد (get_lock())؛ برای Manager باید Lock را خودتان بسازید.
Please login to bookmark Close
پیشرفت شما در «دوره آموزش کانکارنسی در پایتون» (86%)
نظرات

دیدگاهتان را بنویسید

86%
پیشرفت

سرفصل دوره

فهرست مطالب

سرفصل دوره

تمرین

این قسمت تمرین ندارد!

پاسخ تمرین ها

هنوز برای تمرین‌های این قسمت پاسخی ثبت نشده است!

اشتراک گذاری

چرا بهتره از فیلترشکن استفاده کنید؟

من همه ویدئو ها و پادکست های کُدباز رو توی یوتیوب و ساندکلود و پلتفرم هایی آپلود می‌کنم که اغلب فیلتر هستند.

اغلب آموزش‌ها ویدئو و پادکست دارند. پس اگر می‌خواهید از محتوای سایت بیشترین استفاده رو ببرید نیاز به فیلتر شکن دارید.

توجه داشته باشید که برای خرید از فروشگاه بهتره فیلتر شکن رو خاموش کنید.

تنظیمات

انتخاب زبان
تغییر تم