ورود به دنیای همزمانی با پایتون
در این آموزش به تحلیل همزمانی در پایتون می پردازیم. ما در مورد Threads و Process ها و اینکه چگونه آنها مشابه و متفاوت هستند بحث خواهیم کرد. شما همچنین در مورد Multi-threading، Multi-processing، Asynchronous Programming و Concurrency به طور کلی در پایتون خواهید آموخت.
بسیاری از آموزش ها این مفاهیم را مورد بحث قرار می دهند، اما به دلیل عدم وجود مثال های واضح، درک آنها می تواند چالش برانگیز باشد. من شما را از طریق مفاهیم راهنمایی می کنم و به دنیای Concurrency در پایتون می پردازم و توضیحاتی ساده برای درک بهتر ارائه می دهم.
پیش نیازها
شما باید با کدنویسی پایتون آشنا باشید و درک اولیه ای از چند رشته ای داشته باشید.
اگرچه من برای کمک به کسانی که با این مفاهیم آشنا نیستند، نمونههایی از کدهای غیر چند رشتهای ارائه کردهام، اگر قبلاً در این زمینهها دانش دارید، میتوانید درک عمیقتری کسب کنید.
فهرست مطالب
برنامه نویسی متوالی
با دیدن کلمه "Concurrency" نترسید. فعلاً آن را کنار بگذاریم.
هنگامی که شروع به یادگیری زبان های برنامه نویسی مانند پایتون یا جاوا می کنید، برنامه شما معمولاً به صورت sequence
اجرا می شود که از بالا شروع می شود و به پایین می رود.
مثلا:
a = 25 b = 30 def add(a, b): print("Addition value: ", a+b) def sub(a, b): print("Subtraction Value: ", ab) add(a,b) sub(a,b)
هنگامی که برنامه بالا را اجرا می کنید، مفسر پایتون شما شروع به اجرای خط به خط آن از بالا به پایین در یک دنباله می کند. از خط a=25
شروع می شود و به تابع sub()
ختم می شود.
مهم نیست کد شما چقدر طولانی باشد، پایتون آن را از بالا اجرا می کند. این روش اجرای کد همچنین به عنوان رویکرد بالا به پایین یا ترتیبی شناخته می شود - یا به سادگی برنامه نویسی متوالی !
موضوعات چیست؟
هنگامی که برنامه خود را اجرا می کنید، مفسر پایتون یک برنامه کوچک را در داخل ایجاد می کند و به آن دستور می دهد که شروع به اجرا کند. این برنامه کوچک که توسط مفسر ایجاد شده است Thread نامیده می شود. thread یک برنامه کوچک است که وظیفه خاصی را انجام می دهد.
برای هر برنامه یا برنامه ای، پایتون یک رشته ایجاد می کند و به آن دستور می دهد تا اجرا را شروع کند. به این رشته، Main thread
می گویند. مهم است که توجه داشته باشید که Main thread
همچنین می تواند چندین رشته دیگر ایجاد کند اگر به آن بگویید این کار را انجام دهد.
این درست است! می توانید برنامه ای بنویسید تا به رشته اصلی بگویید چندین رشته دیگر ایجاد کند که وظایف شما را انجام می دهند. پس ، یک رشته کار یا وظیفه ای را انجام می دهد (در اینجا رشته اصلی برنامه شما را از بالا به پایین اجرا می کند).
پس می توانید چندین کار ایجاد کنید و یک رشته را برای انجام یک کار اختصاص دهید. یا می توانید یک رشته ایجاد کنید و چندین کار را اجرا کنید - درست مانند مفسر پایتون که Main thread
ایجاد کرد و هر دو کار add
و sub
را به ترتیب اجرا کرد.
فرآیندها چیست؟
تصور کنید صاحب یک شرکت هستید و وظایف زیادی برای انجام دادن دارید. برای رسیدگی به آنها، شروع به استخدام افراد و تعیین تکلیف به هر فرد خواهید کرد.
هر فرد به طور مستقل روی وظیفه خود کار می کند. آنها مجموعه ای از ابزارها و منابع مخصوص به خود را دارند و اگر یک نفر کار خود را به پایان برساند، مستقیماً بر کاری که دیگران انجام می دهند تأثیر نمی گذارد.
در دنیای کامپیوتر، انجام هر یک از افراد مانند یک فرآیند جداگانه است. فرآیندها مستقل هستند، فضای حافظه مخصوص به خود را دارند و مستقیماً منابع را به اشتراک نمی گذارند.
به عنوان مثال، هنگامی که رایانه خود را روشن می کنید و شروع به باز کردن مرورگر، دفترچه یادداشت و MS Office می کنید، اساسا چندین فرآیند را شروع می کنید. هر برنامه ای که باز می کنید، مانند مرورگر یا دفترچه یادداشت، یک فرآیند جداگانه با حافظه خاص خود است. این فرآیندها به یکدیگر متصل نیستند.
هنگامی که یک برنامه را اجرا می کنید، سیستم عامل شما یک فرآیند ایجاد می کند، مقداری حافظه به آن اختصاص می دهد، مقداری از زمان CPU را اختصاص می دهد و برنامه را در آن اجرا می کند. نحوه عملکرد برنامه در فرآیند با نحوه طراحی آن تعیین می شود. در اصل، می توان بيان کرد که یک برنامه یک فرآیند است.
موضوعات در مقابل فرآیندها
حالا، ممکن است گیج کننده به نظر برسد، درست است؟ اما تفاوت کوچکی بین Thread ها و Process ها وجود دارد. بگذارید آن را برای شما روشن کنم.
یک فرآیند مانند یک وظیفه واحد است که توسط سیستم عامل انجام می شود. به تنهایی کار می کند و فضای حافظه مخصوص به خود را دارد.
یک رشته مانند یک بخش کوچکتر از یک کار در یک برنامه است. کارهای کوچک را انجام می دهد. Thread ها در یک فرآیند ایجاد می شوند و توسط برنامه کنترل می شوند.
یک برنامه می تواند چندین رشته ایجاد کند و آنها می توانند منابع را به اشتراک بگذارند، با یکدیگر ارتباط برقرار کنند، اما فقط در یک فرآیند.
برای ساده کردن، مرورگر را به عنوان یک فرآیند در نظر بگیرید. در این فرآیند، می تواند یک یا چند رشته را اجرا کند.
هنگامی که در مورد همزمانی در برنامه نویسی یاد می گیرید، عباراتی مانند "Multi-Threading"، "Multi-Processing"، "Asynchronous" و در نهایت بزرگ "Concurrency" را خواهید دید. اجازه ندهید این اصطلاحات شما را بترسانند - مفاهیم ساده تر از آن چیزی هستند که فکر می کنید. و آنها چیزی هستند که در ادامه در مورد آنها خواهید آموخت.
Multi-threading چیست؟
زندگی آسان تر است وقتی کارگران زیادی برای انجام تمام وظایف ضروری دارید، اینطور نیست؟ به طور مشابه، اگر چندین رشته داشته باشید، به شما کمک می کنند تا تمام وظایف برنامه خود را به بخش های کوچکتر تقسیم کنید.
البته، اگر کارگران زیادی دارید، مدیریت آنها می تواند دشوار باشد و هزینه بیشتری نیز دارد. اینها مزایا و معایب Multi-threading نیز هستند.
استفاده از رشته های متعدد برای انجام وظایف، چند رشته ای نامیده می شود.
بیایید مثال را دوباره مرور کنیم:
a = 25 b = 30 def add(a, b): print("Addition value: ", a+b) def sub(a, b): print("Subtraction Value: ", ab) add(a,b) sub(a,b)
می توانید به مفسر پایتون بگویید دو رشته ایجاد کند و add()
با thread-1 و sub()
با thread-2 اجرا کند. این خوب است، اما چرا می خواهید این کار را انجام دهید؟
رویکرد متوالی را در نظر بگیرید: مفسر ابتدا add()
و سپس sub()
را اجرا می کند. چرا در حالی که add()
در حال اجرا است منتظر sub()
باشیم؟ در این مورد، جمع و تفریق کاملاً متفاوت هستند و به یکدیگر وابسته نیستند، پس چرا باید صبر کرد؟ آیا هر دو تابع به طور همزمان اجرا نمی شوند؟
اینجاست که چندین رشته وارد بازی می شوند. اگر وظایف شما مستقل هستند و نیازی به صبر کردن برای تکمیل یکدیگر ندارید، این امتیاز را دارید که وظایف خود را با استفاده از چندین رشته اجرا کنید.
توجه داشته باشید که چه یک رشته یا چند رشته ایجاد کنید، مفسر پایتون شما رشته اصلی را ایجاد می کند و اجرای برنامه شما را مدیریت می کند. اگر به برنامه خود دستور دهید تا 2 رشته ایجاد کند، موضوع اصلی آنها را برای شما ایجاد می کند و تعیین می کند که هر رشته کدام وظیفه را باید انجام دهد.
پس ، صرف نظر از اینکه در برنامه خود یک Thread ایجاد کنید یا نه، موضوع اصلی ایجاد می شود و اجرای برنامه شما را بر عهده می گیرد.
چند پردازش چیست؟
از خود نام، ممکن است بتوانید درک کنید که چند پردازش به معنای اجرای چندین فرآیند به طور جداگانه بدون اشتراک گذاری مستقیم منابع است.
به عنوان مثال، دو رستوران را تصور کنید که هر کدام سرآشپزهای خاص خود را دارند. روشی که یک سرآشپز در رستوران 1 برای پختن غذا استفاده می کند، بر رستوران 2 تأثیر نمی گذارد یا به آن متکی نیست. آنها به طور مستقل عمل می کنند، حتی اگر در هر دو رستوران سفارش یکسانی داشته باشید.
اما ممکن است از خود بپرسید: چگونه میتوانیم در مورد چندوظیفگی در رایانهای با یک پردازنده اما تعداد زیادی هستههای CPU صحبت کنیم؟
در رایانه های مدرن، برخی از پردازنده ها دارای چندین هسته هستند و هر هسته مانند پردازنده خود کار می کند. اگر پردازنده ای 4 هسته داشته باشد، می تواند با اختصاص دادن هر کار به یک هسته متفاوت، 4 کار را به طور همزمان انجام دهد. این بدان معناست که CPU، حافظه و منابع ذخیره سازی بین این وظایف بدون تکیه بر یکدیگر به اشتراک گذاشته می شوند.
این رویکرد برای چندوظیفگی در چند هسته در یک پردازنده به عنوان چند پردازش متقارن شناخته می شود.
اگر چندین پردازنده دارید، می توانید هر وظیفه را به یک پردازنده متفاوت اختصاص دهید تا به طور مستقل اجرا شود. این نوع پردازش چندگانه را Distributed Multi-processing می نامند.
می خواهم به شما یادآوری کنم که چند رشته ای در یک فرآیند اتفاق می افتد. به زبان ساده، چند رشته ای فقط در یک فرآیند اتفاق می افتد.
بیایید درک عملی از چند پردازش به دست آوریم. من از همان مثالی استفاده خواهم کرد که در بخش برنامه نویسی متوالی و چند رشته ای بحث کردیم، اما این بار از یک نسخه چند پردازشی استفاده می کنم.
همانطور که شما برنامه را با استفاده از thread ها اجرا کردید، اکنون برنامه را با استفاده از فرآیندها اجرا خواهیم کرد.
from multiprocessing import Process a = 25 b = 30 def add(a, b): try: print("Addition value: ", a + b) time.sleep(10) except Exception as e: print(e) def sub(a, b): try: print("Subtraction Value: ", a - b) time.sleep(20) except Exception as e: print(e) if __name__ == "__main__": # Create two processes, one for add and one for sub add_process = Process(target=add, args=(a, b)) sub_process = Process(target=sub, args=(a, b)) # Start the processes add_process.start() sub_process.start() # Wait for both processes to finish add_process.join() sub_process.join()
ما از ماژول multiprocessing
در این برنامه برای ایجاد 2 فرآیند استفاده می کنیم: add_process
و sub_process
. یک فرآیند تابع add()
و دیگری تابع sub()
را به ترتیب اجرا می کند.
سپس هر دو فرآیند را با استفاده از متد .start()
هر دو شیء فرآیند شروع می کنیم. پس از این، ما از متد .join()
از اشیاء فرآیند استفاده می کنیم تا main thread
منتظر بماند تا هر دو فرآیند تکمیل شوند.
بیایید این را با جزئیات بیشتری درک کنیم. من برنامه را در یک کامپیوتر مبتنی بر سیستم عامل ویندوز اجرا می کنم. برنامه task manager
را باز کنید تا تمام فرآیندهای در حال اجرا در سیستم عامل خود را مشاهده کنید. برای اجرای برنامه خود باید از command prompt
ویندوز یا cmd
یا terminal
خود در لینوکس یا مک خود استفاده کنید.
تصویر بالا Task Manager را در سمت چپ و CMD را در سمت راست نشان می دهد. شما به راحتی می توانید Windows Command Processor یا CMD را در حال اجرا به عنوان یک پردازش در Task Manager مشاهده کنید. همچنین میتوانید ببینید که من Google Chrome
و Snipping Tool
در پسزمینه اجرا میکنم که هر دو در Task Manager من نیز فهرست شدهاند.
من قصد دارم برنامه را اکنون از CMD با اجرای دستور python sample.py,
اجرا کنم که sample
نام برنامه است.
در تصویر بالا، من برنامه را با CMD سمت راست اجرا می کنم. برنامه بلافاصله خروجی هر دو تابع add()
و sub()
را چاپ کرد. برنامه همچنان در حال اجرا است، زیرا ما در هر دو تابع time.sleep()
استفاده کردیم که اجرای add_process
را به مدت 10 ثانیه و sub_process
به مدت 20 ثانیه متوقف می کند.
با کنار گذاشتن خروجی، بیایید تمرکز خود را به Task Manager تغییر دهیم. اگر Windows Command Processor
را گسترش دهید، 3 فرآیند پایتون در حال اجرا را خواهید دید. بیایید تحلیل کنیم که چرا به جای دو فرآیند پایتون سه ( add_process
و sub_process
) وجود دارد.
از آنجایی که ما برنامه را از طریق CMD راه اندازی کردیم، برنامه پایتون شما از طریق Windows Command Processor یا CMD راه اندازی می شود و پس در فهرست CMD قرار می گیرد. ما قبلاً می دانیم که وقتی یک برنامه پایتون را اجرا می کنید، مفسر پایتون شما Main Thread
شروع می کند که اجرای برنامه را مدیریت می کند.
این موضوع اصلی به عنوان یک فرآیند فهرست شده است. سپس، همانطور که ما در حال ایجاد دو فرآیند در داخل برنامه هستیم، آنها به عنوان دو فرآیند اضافی در زیر پردازنده فرمان ویندوز (همانطور که در تصویر بالا مشاهده می شود) فهرست می شوند.
توجه داشته باشید که هر دو فرآیند کاملا مستقل هستند و هیچ منبع مشترکی ندارند. اگر میخواهید چیزی را بین فرآیندها به اشتراک بگذارید، باید مکانیسمهای خاصی مانند صف را پیادهسازی کنید.
برنامه نویسی ناهمزمان چیست؟
برنامه نویسی ناهمزمان یک رویکرد خاص برای مدیریت وظایف است که شامل انتظار برای تکمیل عملیات خارجی است. این به برنامه اجازه می دهد تا در حالی که منتظر پایان این عملیات است، به اجرای سایر وظایف ادامه دهد ، نه اینکه آنها را مسدود کند.
توجه: اگر با کدنویسی چند رشته ای در پایتون آشنایی ندارید، می توانید به مثال-2 بروید.
مثال 1:
دوباره همان مثال، اما این بار توابع با استفاده از thread ها اجرا می شوند. add()
توسط thread-1
و sub()
توسط thread-2
اجرا می شود. در زیر نسخه threading مثال آمده است:
import threading import time a = 25 b = 30 def add(a, b): print("Inside add function\n") print("Waiting for 20 seconds in add \n") time.sleep(20) print("Addition value:", a + b) def sub(a, b): print("Inside sub function\n") print("Subtraction Value {}:".format(a - b)) print("Waiting for 10 seconds in sub\n") time.sleep(10) # Create threads for add and sub functions add_thread = threading.Thread(target=add, args=(a, b)) sub_thread = threading.Thread(target=sub, args=(a, b)) # Start both threads add_thread.start() sub_thread.start() # Wait for both threads to finish add_thread.join() sub_thread.join() print("Complete")
ما add_thread
و sub_thread
ایجاد کردیم و توابع add
و sub
به عنوان هدف اختصاص دادیم. اگر این برنامه را اجرا کنید، می توانید خروجی زیر را ببینید:
بیایید خروجی را تجزیه کنیم:
هر دو تابع add
و sub
با استفاده از رشته های جداگانه ( add_thread
و sub_thread
) شروع می شوند.
تابع add
"Inside add function" را چاپ می کند و سپس نشان می دهد که با استفاده از time.sleep(20)
20 ثانیه منتظر است.
تابع sub
"Inside Sub function" و "Subtraction Value -5:" (نتیجه عملیات تفریق) را چاپ می کند و به دنبال آن پیامی مبنی بر اینکه به مدت 10 ثانیه با استفاده از time.sleep(10)
منتظر است را چاپ می کند.
از آنجایی که رشتهها به طور همزمان در حال اجرا هستند، بسته به ترتیب اجرای رشتهها، پیامهای هر دو تابع ممکن است به صورت درهم بهنظر برسند.
پس از دوره های انتظار، هر دو رشته نتایج عملیات مربوطه خود را چاپ می کنند. تابع add
"مقدار اضافی: 55" را چاپ می کند و تابع sub
هیچ چیز دیگری را چاپ نمی کند.
در نهایت، برنامه اصلی پس از اتمام اجرای هر دو رشته، "کامل" را چاپ می کند.
در حالی که add_thread()
به مدت 20 ثانیه منتظر است، sub_thread()
شما نتیجه را ارائه کرده و به مدت 10 ثانیه به خواب رفته است. در همین حال، add_thread
زمان انتظار خود را تکمیل کرد، مقدار اضافه را محاسبه کرد و نتیجه را چاپ کرد. پس ، در حالی که یک رشته در انتظار است، رشته دوم شما شروع به اجرا کرد و در حالی که رشته دوم شما منتظر بود، رشته اول شما شروع به اجرا کرد.
این روش مدیریت وظایف برای اجرای همزمان زمانی که یک کار در انتظار منابع خارجی است، برنامه نویسی ناهمزمان نامیده می شود.
نکته کلیدی که باید به آن توجه داشت این است که رشته ها به طور همزمان اجرا می شوند و به برنامه اجازه می دهند وظایف را به صورت موازی انجام دهد. خروجی درون لایه ماهیت ناهمزمان threading را نشان می دهد، جایی که بخش های مختلف برنامه می توانند به طور همزمان اجرا شوند.
مثال 2:
بیایید دو کار را در نظر بگیریم: وظیفه 1 فهرست ی از کارکنان را از پایگاه داده یک شرکت بازیابی می کند و وظیفه 2 فهرست ی از پروژه های فعال همان شرکت را بازیابی می کند. واضح است که این وظایف به یکدیگر مرتبط یا وابسته نیستند - آنها وظایف مستقلی هستند.
بیایید چند کد نمونه برای این کارها بنویسیم:
def get_employees(): # connect to the database # code to get employees list from database return employees_list def get_active_projects(): # connect to the database # code to get the projects under development return active_projects # run task1 get_employees() # run task2 get_active_projects()
نگران کد داخل توابع نباشید - ما قصد داریم مفهوم را در اینجا بفهمیم.
وقتی برنامه بالا را اجرا می کنید، مفسر پایتون به main thread
می گوید که برنامه را مرحله به مرحله اجرا کند، از بالا شروع کرده و به پایین بروید.
ابتدا task-1 را انجام می دهد که فرض کنید 2 ثانیه طول می کشد و سپس به task-2 می رود که فرض کنید آن نیز 2 ثانیه طول می کشد. ما می دانیم که این وظایف مستقل هستند، پس چرا task-2 باید منتظر باشد تا task-1 تمام شود؟
اگر مکانیزمی وجود داشته باشد که بتوانم به مترجم بگویم که task-1 را شروع کند (دریافت فهرست ی از کارمندان از پایگاه داده)، و در حالی که منتظر تکمیل task-1 است، می توانم به او دستور دهم تا task-2 را شروع کند (دریافت یک فهرست پروژه های فعال از پایگاه داده)، پس من اساساً هر دو کار را تقریباً همزمان اجرا می کنم. این به من کمک می کند کل زمان اجرای هر دو کار را کاهش دهم. این نمونه دیگری از برنامه نویسی ناهمزمان است.
حالا معلوم است. چطور این کار را انجام دهیم؟ چگونه اطمینان حاصل کنیم که task-2 اجرا می شود در حالی که منتظر task-1 هستیم تا فهرست کارمندان را از پایگاه داده دریافت کنیم؟ ما ابزارهای زیادی برای آن داریم. Multithreading یکی از آنه است.
در مثال بالا، شما از دو رشته استفاده می کنید: thread-1 برای task-1 و سپس بلافاصله Thread-2 را برای task-2 شروع می کنید. Thread-2 لازم نیست منتظر پایان Thread-1 باشد.
همچنین میتوانید از مفاهیمی مانند async/wait ، callbacks ، و وعدهها در زبانهای برنامهنویسی مختلف برای پیادهسازی برنامهنویسی ناهمزمان استفاده کنید. در اینجا می توانید اطلاعات بیشتری در مورد آن مفاهیم در جاوا اسکریپت بخوانید .
Concurrency چیست؟
بالاخره به همزمانی رسیدیم!
اکنون باید درک کنید که چند رشته یا چند پردازش شامل استفاده از چندین رشته و فرآیند برای انجام چندین کار به طور همزمان برای کاهش زمان لازم برای اجرای یک برنامه یا برنامه است.
اما چگونه این اتفاق می افتد؟ در پس زمینه چه می گذرد؟ چگونه پردازنده اطمینان حاصل می کند که رشته ها یا پردازش ها به طور همزمان اجرا می شوند؟
تصور کنید دو وظیفه دارید: رانندگی ماشین و برقراری تماس تلفنی. تصمیم می گیرید هر دو را یکجا انجام دهید. در حین رانندگی، با تلفن همراه خود با دوست خود تماس می گیرید و صحبت می کنید.
شما این وظایف را به طور همزمان انجام می دهید، اما یک جزئیات کوچک مهم است: مغز شما به سرعت جاده را اسکن می کند، ماشین های دیگر را تحلیل می کند و اطمینان می دهد که تمرکز و ثبات دارید. اینجا حدود یک میلیثانیه میگذرد، سپس به صحبت با دوستتان میرود، که ممکن است یک میلیثانیه دیگر طول بکشد، و سپس به رانندگی در هر میلیثانیه بازمیگردد. به طور مداوم بین هر دو کار سوئیچ می کند.
از آنجایی که زمان صرف شده برای هر کار بسیار کوتاه است (فقط یک میلی ثانیه)، ممکن است فکر کنید که هر دو کار را همزمان انجام می دهید. اما یک تفاوت بسیار کوچک در زمان برای هر کار وجود دارد، که باعث می شود به نظر برسد هر دو کار به طور همزمان انجام می شوند.
به روشی مشابه، زمانی که دو کار توسط دو رشته یا دو پردازش انجام می شود، پردازنده شما خیلی سریع بین این وظایف سوئیچ می کند. thread-1 را به مدت 1 میلی ثانیه اجرا می کند، سپس حالت خود را ذخیره می کند و به thread-2 تغییر می کند و آن را برای 1 میلی ثانیه دیگر اجرا می کند. پس از ذخیره حالت thread-2، به thread-1 برمی گردد و آن را برای یک میلی ثانیه دیگر اجرا می کند.
من از 1 میلی ثانیه به عنوان مثال استفاده می کنم، اما در واقعیت، حتی سریعتر اتفاق می افتد. سرعت سوئیچینگ به پردازنده شما بستگی دارد.
از آنجایی که جابجایی بین کارها بسیار سریع است، این تصور را ایجاد می کند که هر دو کار به طور همزمان اجرا می شوند. اما توجه به این نکته مهم است که حتی اگر بخواهید هر دو Thread-1 و Thread-2 به طور همزمان اجرا شوند، پردازنده و سیستم عامل شما تصمیم می گیرند که کدام یک را ابتدا اولویت بندی کنند، چه مقدار زمان به هر کدام اختصاص دهند، و به چه ترتیبی. تا آنها را اجرا کنند.
به طور خلاصه، این مانند شعبده بازی چند کار به طور همزمان است. شما یک کار را شروع میکنید، در صورت نیاز به کار دیگری تغییر میدهید، و تا زمانی که همه چیز تمام شود، از آنها عبور میکنید. این مفهوم همزمانی نامیده می شود.
همزمانی مفهومی برای مدیریت پیشرفت چندین کار به طور همزمان است، حتی اگر آنها به طور همزمان اجرا نشوند.
چطور به این میرسی؟ استفاده از Multi-threading و multi-processing دوباره!
شاید تا به حال متوجه شده باشید که Concurrency و Asynchronous Programming مفاهیم اصلی هستند، در حالی که multi-threading و multi-processing پیاده سازی این مفاهیم هستند.
قفل مترجم جهانی (GIL)
مفاهیم برنامه نویسی همزمان و ناهمزمان، صرف نظر از اینکه از چه زبان برنامه نویسی استفاده می کنید، یکسان هستند. اما اجرای این مفاهیم بستگی به زبان برنامه نویسی انتخابی شما دارد.
وقتی صحبت از چند رشته می شود، پایتون کمی عجیب رفتار می کند. بیایید با یک مثال کوچک این را بفهمیم.
در دوران کودکی من و برادر کوچکترم بازی های رایانه ای انجام می دادیم. مادرم قانونی وضع کرد که برادرم باید اول 30 دقیقه بازی کند و بعد من می توانم 30 دقیقه دوم بازی کنم. این قانون برای اطمینان از این بود که هیچ کس با تلاش برای انجام کارها به طور همزمان، بازی را به هم نریزد. پس من 30 دقیقه صبر می کردم تا نوبت من برسد تا 30 دقیقه بازی را انجام دهم.
در دنیای کامپیوتر، برنامه ها مانند من و برادرم هستند که سعی می کنیم این بازی را انجام دهیم. قفل مترجم جهانی (GIL) مانند قاعدهای است که فقط به یک نفر، یا من یا برادرم (یا نخ) اجازه میدهد که بازی را انجام دهیم (یا بایت کد پایتون را اجرا کنیم).
GIL قانونی است که به تنها یک رشته در هر زمان اجازه می دهد تا بایت کد پایتون را اجرا کند. Global Interpreter Lock قفلی است که از دسترسی به اشیاء پایتون محافظت میکند و مانع از اجرای همزمان بایت کد پایتون توسط چندین رشته در یک فرآیند میشود. این بدان معنی است که حتی در یک برنامه پایتون چند رشته ای، فقط یک رشته می تواند بایت کد پایتون را در هر زمان معین اجرا کند.
در نتیجه، این باعث محدودیت عملکرد چند رشته ای در وظایف محدود به CPU می شود. توجه داشته باشید که وظایف محدود به CPU وظایفی هستند که بیشتر به CPU متکی هستند تا عملیات IO. محاسبات ریاضی، فشرده سازی و رفع فشرده سازی فایل ها، و کامپایل برنامه ها توسط یک کامپایلر برنامه چند نمونه از کارهای محدود به CPU هستند که از CPU بیشتری استفاده می کنند.
بیایید به یک مثال نگاه کنیم:
import time def count_up(): count = 0 for i in range(100000000): count = count + i def count_down(): count = 0 for i in range(100000000): count = count + i if __name__ == "__main__": start_time = time.time() count_up() count_down() end_time = time.time() print(f"Time taken: {end_time - start_time} seconds")
در برنامه فوق، توابع count_up()
و count_down()
به صورت متوالی یکی پس از دیگری اجرا می کنیم. توابع count_up
و count_down
هر کدام در یک محدوده بزرگ تکرار می شوند و مجموع تجمعی اعداد را محاسبه می کنند. خروجی برنامه این است:
Time taken: 25.86127805709839 seconds
بیایید همان برنامه را با استفاده از Multi-threading به صورت زیر بنویسیم:
import threading import time def count_up(): count = 0 for i in range(100000000): count = count + i def count_down(): count = 0 for i in range(100000000): count = count + i if __name__ == "__main__": start_time = time.time() # Create two threads, each running a CPU-bound task thread1 = threading.Thread(target=count_up) thread2 = threading.Thread(target=count_down) # Start both threads thread1.start() thread2.start() # Wait for both threads to finish thread1.join() thread2.join() end_time = time.time() print(f"Time taken: {end_time - start_time} seconds")
دو رشته thread1
و thread2
برای اجرای توابع count_up
و count_down
به طور همزمان ایجاد می شوند. این برنامه زمان لازم برای تکمیل هر دو رشته را با استفاده از ماژول time
اندازه گیری می کند. خروجی برنامه این است:
Time taken: 24.498752117156982 seconds
توجه داشته باشید که در رایانه شخصی شما، زمان صرف شده برای تکمیل این برنامه می تواند متفاوت باشد. اگر مشاهده کنید، خروجی نسخه های sequential
و multi-threading
برنامه (25.8 seconds vs 24.98 seconds)
تفاوت زیادی بین زمان صرف شده وجود ندارد. این به دلیل GIL است.
Global Interpreter Lock (GIL) در CPython، که به تنها یک رشته اجازه می دهد تا بایت کد پایتون را در یک زمان اجرا کند، به این معنی است که زمان اجرا در مقایسه با اجرای متوالی وظایف، بهبود قابل توجهی را نشان نمی دهد.
این محدودیت چند رشته ای در پایتون را برای وظایف محدود به CPU به دلیل GIL برجسته می کند.
اما شایان ذکر است که در حالی که GIL از اجرای همزمان بایت کد پایتون توسط چندین رشته جلوگیری می کند، به طور کلی از Threading جلوگیری نمی کند. رشتههای پایتون همچنان میتوانند برای کارهای I/O-bound که در آن رشتهها بیشتر وقت خود را در انتظار عملیات خارجی (مانند شبکه یا ورودی/خروجی دیسک) به جای انجام محاسبات فشرده CPU میگذرانند، مفید باشند.
پس چگونه محدودیت های GIL را حذف می کنید؟
از طعم دیگری از پایتون استفاده کنید
پیاده سازی استاندارد پایتون Cpython است. این پایتون است که با استفاده از زبان C طراحی شده است و بیشتر در سراسر جهان استفاده می شود. برای اجتناب از GIL، میتوانیم از Jython (پایتون توسعهیافته با جاوا)، IronPython (پایتون توسعهیافته با استفاده از NET) یا PyPy (پایتون توسعهیافته با پایتون) استفاده کنیم.
برای اطلاعات بیشتر می توانید منابع زیر را تحلیل کنید:
از ماژول multiprocessing استفاده کنید
Multiprocessing ماژولی است که به شما کمک می کند از چندین هسته CPU خود برای اجرای برنامه خود در فرآیندهای جداگانه استفاده کنید.
هر فرآیند دارای هسته CPU، حافظه، منابع و interpreter
خاص خود است. بله، هر فرآیندی مفسر خاص خود را برای اجرای کد پایتون شما دارد. اگر 4 هسته CPU دارید، می توانید 4 پردازش را اجرا کنید که هر کدام مفسر خاص خود را دارند که برنامه شما را در حافظه خود اجرا می کند.
بیایید برنامه ای را که در مفهوم GIL بحث کردیم با استفاده از پردازش چندگانه به صورت زیر بنویسیم:
import multiprocessing import time def count_up(): count = 0 for i in range(100000000): count = count + i def count_down(): count = 0 for i in range(100000000): count = count + i if __name__ == "__main__": start_time = time.time() # Create two threads, each running a CPU-bound task process1 = multiprocessing.Process(target=count_up) process2 = multiprocessing.Process(target=count_down) # Start both threads process1.start() process2.start() # Wait for both threads to finish process1.join() process2.join() end_time = time.time() print(f"Time taken: {end_time - start_time} seconds")
خروجی برنامه زمانی که آن را از طریق ترمینال یا CMD اجرا می کنید این است:
Time taken: 8.376060724258423 seconds
این نشان دهنده بهبود قابل توجهی در زمان صرف شده در مقایسه با تکنیک هایی مانند Sequential و Multi-threading (25.8 در مقابل 24.4 در مقابل 8.3 ثانیه) است. این نشان می دهد که چگونه استفاده از چند پردازش می تواند به کاهش زمان CPU شما کمک کند.
توجه داشته باشید که اگر بخواهید بین فرآیندها ارتباط برقرار کنید، سربار وجود خواهد داشت. در نتیجه، در حالی که پردازش چندگانه می تواند برای وظایف محدود به CPU موثر باشد، ممکن است برای همه سناریوها مناسب نباشد، به ویژه آنهایی که شامل ارتباطات مکرر یا انتقال داده های بزرگ بین فرآیندها هستند.
اجرای برنامه نویسی ناهمزمان!
اگر برنامه یا برنامه شما محدود به CPU سنگین نیست یا بیشتر به پردازش CPU وابسته است، با استفاده از ماژول asyncio
پایتون، می توانید مفهوم برنامه نویسی ناهمزمان را پیاده سازی کنید (منتظر تکمیل کار 1 قبل از شروع کار 2 نباشید).
این ماژول عمدتا در عملیات I/O استفاده می شود. Asyncio به شما امکان می دهد کدهای ناهمزمان بنویسید که به طور مشترک بدون تکیه بر رشته ها چند کار را انجام می دهند، که می تواند در شرایطی که در غیر این صورت رشته ها در انتظار تکمیل عملیات I/O مسدود می شوند، کمک کند.
با استفاده از asyncio، میتوانید کد غیرمسدود کننده بنویسید که به سایر وظایف اجازه میدهد در حالی که منتظر پایان عملیات ورودی/خروجی هستند، اجرا شوند، پس به طور بالقوه همزمانی و پاسخگویی کلی برنامه شما را بهبود میبخشد.
توجه به این نکته مهم است که asyncio خود GIL را حذف نمی کند - بلکه یک مدل همزمانی جایگزین ارائه می دهد که می تواند برای انواع خاصی از برنامه ها کارآمدتر باشد.
برای کارهای محدود به CPU ، asyncio ممکن است مزایای عملکردی مشابه روشهای چند پردازشی یا دیگر تکنیکهای موازی را ارائه ندهد، زیرا همچنان در محدودیتهای GIL عمل میکند. در چنین مواردی، چند پردازش یا سایر رویکردهای همزمان ممکن است مناسب تر باشند.
خلاصه
در این مقاله با تفاوت های Multi-threading، Multi-processing، Asynchronous Programming و Concurrency آشنا شدید. اکنون به طور خلاصه آنها را مرور می کنیم:
چند رشته ای:
تعریف : Multi-threading شامل استفاده از چندین رشته برای اجرای همزمان وظایف در یک فرآیند واحد است. همزمان چیزی جز مفهوم Concurrency در اینجا نیست.
توضیح : رشتهها فضای حافظه یکسانی را به اشتراک میگذارند و به آنها اجازه میدهد به راحتی با هم ارتباط برقرار کنند، اما برای جلوگیری از تضاد، نیاز به همگامسازی دقیق دارند.
پردازش چندگانه:
تعریف : چند پردازش شامل استفاده از چندین فرآیند برای اجرای برنامه یا وظایف شما است. هر فرآیند فضای حافظه و منابع خاص خود را دارد.
توضیح : فرآیندها مستقل هستند و ارتباط بین آنها اغلب به مکانیسم های ارتباط بین فرآیندی (IPC) نیاز دارد. هر فرآیند در فضای حافظه خود عمل می کند.
برنامه نویسی ناهمزمان:
تعریف : برنامه نویسی ناهمزمان یک مفهوم برنامه نویسی است که اجازه می دهد وظایف جدا از جریان اصلی برنامه اجرا شوند. لزوماً به معنای اجرای همزمان نیست.
توضیح : وظایف ناهمزمان برنامه اصلی را مسدود نمیکند و برنامه را قادر میسازد تا در حالی که منتظر تکمیل وظایف ناهمزمان است، به اجرای خود ادامه دهد. این معمولاً در عملیات I/O برای بهبود کارایی استفاده می شود.
همزمانی:
تعریف : همزمانی مفهوم گسترده تری است که به توانایی یک سیستم برای اجرای چندین کار در فواصل زمانی همپوشانی اشاره دارد، یعنی جابجایی بین وظایف در بازه های زمانی مشخص.
توضیح : همزمانی اجرای همزمان را تضمین نمی کند، بلکه بر مدیریت کارآمد چندین کار با جابجایی بین آنها تمرکز می کند. این شامل برنامه نویسی چند رشته ای، چند پردازشی و ناهمزمان به عنوان راه هایی برای دستیابی به اجرای همزمان است.
امیدوارم اکنون درک روشنی از مفاهیمی داشته باشید که ممکن است قبلاً ترسناک به نظر می رسیدند. من میدانم که بیشتر جنبه تئوری دارد، اما میخواهم مطمئن شوم که قبل از وارد شدن به جنبه عملی، مفهوم را به خوبی درک کردهاید.
تا آن زمان، دوست شما در اینجا، Hemachandra، در حال امضای قرارداد است…
برای دوره های بیشتر، می توانید به وب سایت شخصی من مراجعه کنید.
روز خوبی داشته باشی!
ارسال نظر