این ماژول با اجرای برنامه روی چند پردازشگر، امکان اجرای موازی برنامهها را برای ما فراهم میکند. با توجه به اینکه این ماژول برای اجرای برنامه روی چند پردازشگر به صورت همزمان طراحی شده است، استفاده از آن در کامپیوترهای تکپردازنده منطقی نیست.
در multiprocessing به هر پردازش منابع جداگانهای اختصاص مییابد. مثلا اگر دو process همزمان به یک لیست با مقدار [1, 2, 3] نیاز داشته باشند، این لیست یک بار برای پردازش اول و یک بار هم برای پردازش دوم در RAM ساخته میشود. البته با ترفندهایی میتوان منابع را بین process ها به اشتراک گذاشت که این امر بهینگی را کاهش میدهد.
بین همه تکنیک های Concurrent Programming، تنها multiprocessing است که با اجرای برنامه روی چند پردازشگر واقعا برنامهها را به صورت parallel اجرا میکند. پس بهتر است تسکهای CPU Bound را با استفاده از multiprocessing انجام دهیم.
با توجه به اینکه هر process در multiprocessing منابع اختصاصی خود را میخواهد، استفاده بیرویه از multiprocessing میتواند ما را با بحران کمبود منابع سختافزاری مواجه کند.
نحوه اجرای یک برنامه به صورت multiprocessing
برای ایجاد process با استفاده از ماژول multiprocessing دو راه داریم:
- روش اول – ساختن instance از کلاس
Process - روش دوم – ارثبری از کلاس Process
روش اول – ساختن instance از کلاس Process
به تکه کد زیر توجه کنید. این کد شامل دو تابع است که اجرای هر کدام ۲ ثانیه طول میکشد. به عبارت دیگر اجرای کل برنامه به ۴ ثانیه زمان نیاز دارد.
from time import sleep
def task1():
print('Process 1 started')
sleep(2)
print('Process 1 completed')
def task2():
print('Process 2 started')
sleep(2)
print('Process 2 completed')
task1()
task2()اکنون با کمی تغییر در کد بالا، این دو تابع را به صورت موازی اجرا کنیم که در این صورت زمان اجرای برنامه به ۲ ثانیه کاهش مییابد.
from time import sleep
from multiprocessing import Process
def task1():
print('Process 1 started')
sleep(2)
print('Process 1 completed')
def task2():
print('Process 2 started')
sleep(2)
print('Process 2 completed')
if __name__ == "__main__":
Process(target=task1).start()
Process(target=task2).start()توضیح کد بالا:
- خط ۴ تا ۷: یک تابع تعریف شده که اجرای آن ۲ ثانیه طول میکشد.
- خط ۹ تا ۱۲: یک تابع تعریف شده که اجرای آن ۲ ثانیه طول میکشد.
- خط ۱۵: تابع
task1را به عنوان یک برنامه مستقل اجرا میکند. - خط ۱۶: تابع
task2را به عنوان یک برنامه مستقل اجرا میکند.
در تکه کد بالا هر تابع به صورت کاملا مستقل از دیگری اجرا میشود. این دو تابع هیچ درکی از یک دیگر ندارند. اجرای این دو تابع در process های جداگانه مانند این است که این دو تابع هر کدام در یک فایل جداگانه نوشته و اجرا شده باشند.
ارسال ورودی به process
در صورتی که process به ورودی نیاز داشته باشد، میتوان ورودی ها را تحت عنوان args به آن فرستاد. به تکه کد زیر توجه کنید.
from multiprocessing import Process
def task(name):
print(f"process {name} is running")
Process(target=task, args=('p1', )).start()
# ^^^^^^^^^^^^^توضیح کد بالا:
- خط ۳ تا ۴: یک تابع تعریف شده که یک ورودی میگیرد. این ورودی قرار است آبجکت یک process باشد.
- خط ۶: تابع
taskبه عنوان یک process اجرا شده است. در این قسمت ورودی های مورد نیازtaskتحت عنوانargsبه آن ارسال شده است.
روش دوم – ارثبری از کلاس Process
به تکه کد زیر توجه کنید. این کد شامل دو تابع است که اجرای هر کدام ۲ ثانیه طول میکشد. به عبارت دیگر اجرای کل برنامه به ۴ ثانیه زمان نیاز دارد.
from time import sleep
def task1():
print('Process 1 started')
sleep(2)
print('Process 1 completed')
def task2():
print('Process 2 started')
sleep(2)
print('Process 2 completed')
task1()
task2()اکنون با ارثبری از کلاس Process کلاس MyProcess را میسازیم تا با استفاده از آن این دو تابع را به صورت موازی اجرا کنیم که در این صورت زمان اجرای برنامه به ۲ ثانیه کاهش مییابد.
from time import sleep
from multiprocessing import Process
class MyProcess(Process):
pass
def task1():
print('Process 1 started')
sleep(2)
print('Process 1 completed')
def task2():
print('Process 2 started')
sleep(2)
print('Process 2 completed')
if __name__ == "__main__":
MyProcess(target=task1).start()
MyProcess(target=task2).start()با ساختن کلاس MyProcess و ارثبری آن از Process میتوان به امکانات زیادی که برنامهنویسی شیگرا در اختیار ما قرار میدهد دسترسی پیدا کنیم. این امکانات در بخش بعدی به تفصیل توضیح داده شده است. به همین دلیل از شرح آنها در این قسمت صرف نظر میکنیم.
درک مستقل بودن پردازش ها
به تکه کد زیر توجه کنید. این تکه کد به صورت sequential توسعه یافته است.
lst = []
def task1():
lst.append(1)
print('lst value from task1:', lst)
def task2():
lst.append(2)
print('lst value from task2:', lst)
task1()
task2()
print('lst final value:', lst)خروجی کد بالا مشابه زیر است.
lst value from task1: [1]
lst value from task2: [1, 2]
lst final value: [1, 2]
همانطور که انتظار میرود، هر یک از توابع مقدار جدیدی را به lst اضافه میکنند و در نهایت مقدار lst با مقدار اولیه اش متغیر است.
اکنون در ادامه همین کد را به روش multiprocessing بازنویسی میکنیم.
from multiprocessing import Process
lst = []
def task1():
lst.append(1)
print('lst value from task1:', lst)
def task2():
lst.append(2)
print('lst value from task2:', lst)
Process(target=task1).start()
Process(target=task2).start()
print('lst final value:', lst)توضیح کد بالا:
- خط ۳: یک لیست تعریف شده تا مقدارش را بعدا در
task1وtask2تغییر دهیم. - خط ۵ تا ۷: یک تابع تعریف شده که عدد 1 را به lst اضافه میکند.
- خط ۹ تا ۱۱: یک تابع تعریف شده که عدد 2 را به lst اضافه میکند.
- خط ۱۳: تابع
task1را به عنوان یک برنامه مستقل اجرا میکند. - خط ۱۴: تابع
task2را به عنوان یک برنامه مستقل اجرا میکند. - خط ۱۵: مقدار نهایی lst را چاپ میکند.
خروجی کد بالا ممکن است هر یک از دو حالت زیر باشد.
[tabby title=”خروجی اول”]
lst final value: []
lst value from task1: [1]
lst value from task2: [2]
[tabby title=”خروجی دوم”]
lst value from task1: [1]
lst final value: []
lst value from task2: [2]
[tabbyending]
با توجه به این خروجی دو نکته مهم قابل برداشت است.
- نکته اول: مقدار نهایی
lstبا مقدار آن در هر یک از توابعtask1وtask2متفاوت است. این نشان میدهد کهlstدر هر یک از این توابع عملکرد مستقلی دارد. - نکته دوم: نمیتوان کاملا با قطعیت گفت که process ها با چه ترتیبی اجرا میشوند. هر بار که این برنامه اجرا شود ممکن است ترتیب print ها متفاوت باشد. این مورد برای thread ها هم وجود داشت. این سیستمعامل است که تصمیم میگیرد کدام thread یا process ابتدا اجرا شود. ما فقط با اجرای متد
startبه سیستمعامل اطلاع میدهیم که چه thread یا process ای آماده اجرا است.
اهمیت استفاده از __name__ == "__main__"
process ها در سیستمعاملهای یونیکسی مانند لینوکس و macOS با استفاده از تابع fork ساخته میشوند. به همین دلیل هر process میداند که باید از همان نقطهای که process والد متوقف شده بود، ادامه دهد.
این در صورتی است که process ها در ویندوز با استفاده از تابع spawn ساخته میشوند. تابع spawn یک process جدید ایجاد میکند که کل اسکریپت پایتون را از ابتدا اجرا میکند. در این حالت اگر از این شرط استفاده نشود process جدید نیز دوباره کد ایجاد process ها را اجرا میکند که منجر به ایجاد بینهایت process و مصرف بیش از حد منابع سیستم میشود.
به تکه کد زیر توجه کنید. در این کد از این شرط استفاده نشده است.
from time import sleep
from multiprocessing import Process
def task1():
print('Process 1 started')
sleep(2)
print('Process 1 completed')
def task2():
print('Process 2 started')
sleep(2)
print('Process 2 completed')
Process(target=task1).start()
Process(target=task2).start()در صورت اجرای این کد در ویندوز با خطای زیر مواجه خواهید شد.
