مقدمه
تصمیمگیری بین مدلهای برنامهنویسی همزمان و ناهمزمان تنها یک موضوع فنی در توسعه نرمافزار نیست؛ بلکه بر نحوه همکاری برنامهها، تکمیل وظایف و واکنش به ورودیهای کاربر تأثیر میگذارد.به خاطر داشته باشید که انتخاب مدل مناسب میتواند موفقیت یا شکست یک پروژه را تعیین کند، بهویژه هنگامی که این دو پارادایم را با یکدیگر مقایسه میکنیم. هدف این مقاله روشن کردن برخی از ابهامات در مورد این مفاهیم است، با ایجاد تمایز واضح بین برنامهنویسی همزمان و ناهمزمان و توضیح مزایا، معایب و بهترین کاربردهای آنها. با درک دقیق هر استراتژی، توسعهدهندگان میتوانند تصمیمات هوشمندانه بگیرند و رویکرد خود را با نیازهای برنامههایشان تطبیق دهند.
درک برنامهنویسی همزمان
برنامهنویسی همزمان چیست؟
در برنامهنویسی همزمان، وظایف به صورت ترتیبی انجام میشوند. مانند یک کتاب، شما از ابتدا شروع کرده و هر کلمه و خط را میخوانید. برنامهنویسی همزمان نیازمند تکمیل هر وظیفه قبل از شروع وظیفه بعدی است. جریان کنترل قابل پیشبینی و ساده است.
سیستم ممکن است گیر کند یا به پاسخگویی نرسد اگر یک وظیفه بیش از حد طول بکشد. رفتار مسدودکننده یکی از ویژگیهای برجسته برنامهنویسی همزمان است.
چگونه کار میکند؟
مدل برنامهنویسی همزمان عملیات را به صورت خطی پیش میبرد. این فرآیند به صورت زیر ساده میشود:
- اجرای برنامه به صورت ترتیبی است.
- وظایف به ترتیب کد اجرا میشوند.
- از بالا به پایین، هر خط کد اجرا میشود.
اگر یک کار زمان زیادی ببرد، مانند خواندن یک فایل حجیم یا انتظار برای ورودی انسانی، برنامه تا اتمام کار مسدود میماند. برنامهنویسی همزمان مسدود میکند.
موارد استفادهای که برنامهنویسی همزمان برجسته میشود
برنامهنویسی همزمان بهویژه در سناریوهایی مفید است که وظایف باید به ترتیب خاصی اجرا شوند. به عنوان مثال، اگر بخواهید یک کیک بپزید، نمیتوانید آن را در فر بگذارید قبل از اینکه مواد را مخلوط کرده باشید. بهطور مشابه، در یک برنامه، ممکن است لازم باشد دادهها را از یک پایگاه داده بازیابی کنید قبل از اینکه بتوانید آنها را پردازش کنید.
مثال: خواندن فایل به صورت ترتیبی
در اینجا مثالی از نحوه کار برنامهنویسی همزمان در زمینه خواندن فایلها آورده شده است:
function readFilesSequentially(fileList) {
for each file in fileList {
content = readFile(file) // This is a blocking operation
process(content)
}
}
در این شبهکد، تابع readFile(file)
یک عملیات همزمان است. تابع process(content)
تا زمانی که readFile(file)
به طور کامل خواندن فایل را تمام نکرده است، اجرا نمیشود. این یک نمایش واضح از ماهیت ترتیبی و مسدودکننده برنامهنویسی همزمان است.
بررسی برنامهنویسی ناهمزمان
برنامهنویسی ناهمزمان چیست؟
برنامهنویسی ناهمزمان یک پارادایم است که اجازه میدهد وظایف به صورت همزمان، به جای ترتیبی، اجرا شوند. این به این معناست که اجرای برنامه نیازی به انتظار برای تکمیل یک وظیفه قبل از ادامه به وظیفه بعدی ندارد. این شبیه بودن در یک بوفه است – نیازی نیست صبر کنید تا یک نفر سرو غذای خود را تمام کند تا شما شروع کنید.
در برنامهنویسی ناهمزمان، وظایف اغلب شروع میشوند و سپس کنار گذاشته میشوند تا به وظایف دیگر اجازه اجرا داده شود. پس از اتمام وظیفه اصلی، میتوان آن را از همانجایی که متوقف شده بود ادامه داد. این ویژگی غیرمسدودکننده یکی از ویژگیهای کلیدی برنامهنویسی ناهمزمان است.
چگونه کار میکند
اجرای همزمان: یکی از جنبههای اصلی برنامهنویسی ناهمزمان توانایی اجرای چندین وظیفه به صورت همزمان است. این میتواند منجر به افزایش قابل توجهی در کارایی و عملکرد برنامه شود، بهویژه در سناریوهایی که وظایف مستقل هستند یا نیاز به انتظار برای منابع خارجی مانند درخواست شبکه دارند.
ماهیت غیرمسدودکننده: برنامهنویسی ناهمزمان بقیه برنامه را مسدود نمیکند، زیرا برای وظایف طولانی مانند عملیات I/O منتظر نمیماند. در برنامهنویسی رابط کاربری، این میتواند تجربه کاربری و پاسخگویی را بهبود بخشد.
موارد استفادهای که برنامهنویسی ناهمزمان باید استفاده شود
وظایف وابسته به I/O اغلب به صورت ناهمزمان برنامهنویسی میشوند. وظایف ناهمزمان میتوانند در توسعه وب برای ارسال درخواستهای API، دسترسی به پایگاههای داده و مدیریت ورودیهای کاربر بدون وقفه در رشته اصلی استفاده شوند.
مثال: درخواستهای AJAX در توسعه وب با شبهکد
برنامهنویسی ناهمزمان میتواند برای ارسال درخواستهای AJAX در توسعه وب استفاده شود. مثال زیر را ببینید:
function fetchAndDisplayData(url) {
// This is a non-blocking operation
data = asyncFetch(url);
data.then((response) => {
// This code will run once the data has been fetched
displayData(response);
});
}
در شبهکد بالا، تابع asyncFetch(url)
یک عملیات ناهمزمان است. تابع displayData(response)
تا زمانی که asyncFetch(url)
فرآیند دریافت داده را تمام نکرده است، اجرا نمیشود. در همین حال، کدهای دیگر میتوانند در پسزمینه ادامه پیدا کنند که ماهیت غیرمسدودکننده برنامهنویسی ناهمزمان را نشان میدهد.
مقایسه برنامهنویسی ناهمزمان و همزمان
تفاوتهای بین برنامهنویسی همزمان و ناهمزمان از نظر عملکرد، اجرای برنامه و زمان اجرا به شرح زیر است:
اجرای برنامه
همزمان: در یک سیستم همزمان، وظایف به صورت ترتیبی، یکی پس از دیگری اجرا میشوند. نتیجه این است که جریان کنترل پیشبینی و پیادهسازی آن ساده است.
ناهمزمان: در یک محیط ناهمزمان، وظایف میتوانند به صورت همزمان اجرا شوند. این باعث میشود که نرمافزار نیازی به انتظار برای اتمام یک وظیفه قبل از ادامه به وظیفه بعدی نداشته باشد.
عملکرد
همزمان: با عملکرد همزمان، اگر یک وظیفه زمان زیادی برای اتمام نیاز داشته باشد، کل سیستم ممکن است یخ بزند یا غیرقابل پاسخ شود.
ناهمزمان: ویژگی غیرمسدودکننده برنامهنویسی ناهمزمان میتواند منجر به تجربه کاربری پاسخگوتر و یکپارچهتر شود، بهویژه در زمینه توسعه رابط کاربری.
مناسبت برنامهها
همزمان: ایدهآل برای موقعیتهایی که نیاز به اجرای مراحل به ترتیب از پیش تعیین شده دارند.
ناهمزمان: زمانی که وظایف وابسته به I/O به جای وابسته به CPU هستند، به عنوان ناهمزمان در نظر گرفته میشوند.
چه زمانی باید از برنامهنویسی ناهمزمان استفاده کرد
- برنامههای مبتنی بر وب: برای جلوگیری از مختل شدن رشته اصلی اجرا، میتوان از وظایف ناهمزمان برای انجام عملیاتهایی مانند انجام درخواستهای API استفاده کرد.
- مدیریت پایگاه داده: عملیات خواندن و نوشتن دادهها ممکن است وقتگیر باشد و لزومی ندارد که قبل از انجام وظایف دیگر تمام شوند.
- برنامهنویسی رابط کاربری: با برنامهنویسی ناهمزمان، تجربه کاربری پاسخگوتر و روانتری در هنگام مدیریت ورودیهای کاربر ممکن میشود.
- عملیات I/O فایل: بهطور کلی، نیازی نیست که عملیات I/O فایلهای زمانبر قبل از ادامه به مرحله بعدی تمام شوند.
حلقه رویداد و پشته فراخوانی
در جاوااسکریپت، کار با کد ناهمزمان به طور مؤثر مستلزم درک حلقه رویداد و پشته فراخوانی آن است. به طور ساده، این جایی است که پشته فراخوانی کد را به ترتیب اجرا میکند. وظایف همزمان ابتدا اجرا میشوند و در نهایت به حلقه رویداد اجازه داده میشود تا هر دستور کد ناهمزمان، مانند setTimeout
یا درخواستهای API، را پس از پردازش کد همزمان مدیریت کند.
اینگونه است که جاوااسکریپت به نظر میرسد که همزمان کارهای زیادی انجام میدهد، حتی اگر از نظر فنی تکپردازشی باشد. در حالی که این عملیاتهای ناهمزمان در حال اجرا هستند، حلقه رویداد اطمینان حاصل میکند که تمام دادهها در زمان مناسب پردازش میشوند بدون اینکه رشته اصلی مسدود شود.
درک نحوه تعامل حلقه رویداد و پشته فراخوانی به ما کمک میکند تا کد ناهمزمان بهتری بنویسیم و از مشکلات رایج مانند یخ زدن رابط کاربری یا تعاملات بسیار کند جلوگیری کنیم.
برنامهنویسی ناهمزمان با استفاده از Web Workers
ابزار بعدی که برای مدیریت وظایف به صورت ناهمزمان بسیار مفید است، Web Workers هستند. آنها به ما این امکان را میدهند که جاوااسکریپت را در پسزمینه اجرا کنیم بدون اینکه رشته اصلی را مسدود کنیم، و این برای عملکرد و کارهایی که باید انجام دهیم، مانند محاسبات پیچیده یا دریافت دادههای زیاد، بسیار مفید است. Web Workers به ما همزمانی واقعی میدهند، به این معنی که میتوانیم کارهای سنگین را به یک رشته دیگر منتقل کنیم و رشته اصلی را مسئول نگه داریم. با این حال، نکتهای که باید به خاطر داشته باشیم این است که Workers به DOM دسترسی ندارند و بنابراین برای کارهایی که نیاز به بروزرسانی مستقیم رابط کاربری ندارند، مناسبتر هستند.
در اینجا یک مثال سریع از نحوه استفاده از Web Workers آورده شده است:
// In the main script
const worker = new Worker("./worker.js");
worker.postMessage("Start the task");
// In the worker script (worker.js)
onmessage = function (event) {
// Perform long-running task here
postMessage("Task done");
};
چه زمانی باید از برنامهنویسی همزمان استفاده کرد
- دریافت و پردازش داده به صورت ترتیبی: برای برخی از برنامهها، دریافت داده از یک پایگاه داده پیشنیاز پردازش آن دادهها است.
- نوشتن اسکریپتهای پایه: هنگام کار با اسکریپتهای کوچک، برنامهنویسی همزمان ممکن است قابل درکتر و خطایابی آن آسانتر باشد.
- وظایف وابسته به CPU: انجام عملیاتهای سنگین که وابسته به CPU هستند. برنامهنویسی همزمان ممکن است برای وظایف وابسته به CPU به جای وظایف وابسته به I/O کارآمدتر باشد.
مثالهای عملی در کد
مثال کد همزمان: پردازش یک لیست از وظایف به صورت ترتیبی
در برنامهنویسی همزمان، وظایف به صورت ترتیبی پردازش میشوند. در اینجا یک مثال در پایتون آورده شده است:
import time
def process_userData(task):
# Simulate task processing time
time.sleep(1)
print(f"Task {task} processed")
tasks = ['task1', 'task2', 'task3']
for task in tasks:
process_userData(task)
وظایف به صورت ترتیبی توسط این روش همزمان process_userData
پردازش میشوند. اگر یک وظیفه برای اتمام زمان زیادی بگیرد، وظایف بعدی باید منتظر بمانند به دلیل این پردازش ترتیبی که میتواند باعث تأخیر شود. عملکرد برنامه و تجربه کاربری ممکن است به دلیل این موضوع آسیب ببیند.
مثال کد ناهمزمان: دریافت داده از منابع متعدد به صورت همزمان
بر خلاف این، برنامهنویسی ناهمزمان امکان پردازش وظایف به صورت همزمان را میدهد. در اینجا یک مثال در پایتون با استفاده از کتابخانه asyncio
آورده شده است:
import asyncio
async def retrieve_data(source):
# Simulate time taken to fetch data
await asyncio.sleep(1)
print(f"Data retrieved {source}")
sources = ['source1', 'source2', 'source3']
async def main():
tasks = retrieve_data(source) for source in sources]
await asyncio.gather(*tasks)
asyncio.run(main())
روش ناهمزمان چندین فرآیند را همزمان شروع میکند. این اطمینان حاصل میکند که برنامه میتواند بدون وقفه از یک وظیفه به وظیفه دیگر منتقل شود. با این کار میتوانیم عملکرد برنامه و تجربه کاربری را بهبود بخشیم. با این حال، مدیریت وظایف و callbackها میتواند پیادهسازی را دشوارتر کند.
console.log("Start"); // First task (synchronous) - goes to call stack
setTimeout(() => {
console.log("Timeout callback"); // This task(aysnchronous) is put into the event loop
}, 1000);
console.log("End"); // Second task (synchronous) - in call stack
پشته فراخوانی (Call Stack):
تابع console.log('Start')
ابتدا اجرا میشود زیرا یک عملیات همزمان است. این تابع پردازش شده و بلافاصله از پشته فراخوانی حذف میشود.
تابع setTimeout()
یک تابع ناهمزمان است، بنابراین callback آن، یعنی console.log('Timeout callback')
به تأخیر میافتد و به حلقه رویداد (event loop) فرستاده میشود تا پس از 1 ثانیه (1000 میلیثانیه) اجرا شود، اما خود setTimeout()
باعث مسدود شدن اجرای کد نمیشود.
سپس console.log('End')
اجرا میشود زیرا یک عملیات همزمان است که در رشته اصلی قرار دارد.
حلقه رویداد (Event Loop):
پس از آنکه وظایف همزمان (مانند console.log('Start')
و console.log('End')
) اجرا شدند، حلقه رویداد منتظر تأخیر 1 ثانیهای میماند و سپس callback ناهمزمان داده شده به setTimeout
را پردازش میکند.
پس از آماده شدن callback، حلقه رویداد آن را به پشته فراخوانی ارسال کرده و سپس اجرا میشود و 'Timeout callback'
را چاپ میکند.
خروجی:
Start
End
Timeout callback
این مثال نشان میدهد که چگونه جاوااسکریپت ابتدا وظایف همزمان را اجرا میکند، سپس پس از پاک شدن پشته فراخوانی اصلی، وظایف ناهمزمان را با استفاده از حلقه رویداد پردازش میکند.
بهترین شیوهها و الگوها برای استفاده مؤثر از هر مدل برنامهنویسی
برنامهنویسی همزمان
- استفاده کنید زمانی که سادگی مهم است: برنامهنویسی همزمان ساده و قابل درک است، بنابراین برای وظایف و اسکریپتهای ساده ایدهآل است.
- از آن برای وظایف وابسته به I/O خودداری کنید: برنامهنویسی همزمان میتواند در هنگام انتظار برای عملیاتهای I/O (مانند درخواستهای شبکه یا خواندن/نوشتن دیسک) رشته اجرایی را مسدود کند. برای اینگونه وظایف از برنامهنویسی ناهمزمان استفاده کنید تا از مسدود شدن جلوگیری شود.
برنامهنویسی ناهمزمان
- استفاده برای وظایف وابسته به I/O: برنامهنویسی ناهمزمان زمانی که با وظایف وابسته به I/O سروکار دارید درخشان عمل میکند. این امکان را میدهد که رشته اجرایی در حین انتظار برای تکمیل عملیات I/O، به انجام سایر وظایف ادامه دهد.
- به منابع مشترک توجه کنید: برنامهنویسی ناهمزمان میتواند منجر به شرایط رقابتی (race conditions) شود اگر چندین وظیفه در حال دسترسی به منابع مشترک و تغییر آنها باشند. برای جلوگیری از این مشکل، از سازوکارهای همگامسازی مانند قفلها یا شمارندهها (semaphores) استفاده کنید.
الگوهای طراحی رایج
برنامهنویسی همزمان
رایجترین الگو در برنامهنویسی همزمان، الگوی اجرای ترتیبی است که در آن وظایف یکی پس از دیگری اجرا میشوند.
برنامهنویسی ناهمزمان
- پرامیسها (Promises): پرامیسها نمایانگر مقداری هستند که ممکن است هنوز در دسترس نباشد. آنها برای مدیریت عملیاتهای ناهمزمان استفاده میشوند و روشهایی برای اتصال callbackها فراهم میکنند که زمانی که مقدار در دسترس قرار میگیرد یا خطایی رخ میدهد، فراخوانی شوند.
- Async/Await: این ویژگی نوعی شیرینی سینتکسی بر روی پرامیسها است که کد ناهمزمان را به شکلی مشابه کد همزمان نشان میدهد. این باعث میشود که کد ناهمزمان راحتتر نوشته شده و قابل درکتر باشد.
چگونه از مشکلات رایج جلوگیری کنیم
جهنم Callback (Callback Hell)
«جهنم Callback» به تو در تو بودن فراخوانیها اشاره دارد که باعث میشود کد خوانا و قابل درک نباشد. در اینجا چند روش برای جلوگیری از آن آورده شده است:
- کد خود را مدولار کنید: کد خود را به توابع کوچکتر و قابل استفاده مجدد تقسیم کنید.
- استفاده از پرامیسها یا Async/Await: این ویژگیهای جاوااسکریپت میتوانند کد شما را صاف کرده و آن را خوانا و قابل درکتر کنند.
- مدیریت خطا: همیشه مدیریت خطا را برای callbackهای خود در نظر بگیرید. خطاهای بدون مدیریت میتوانند منجر به نتایج غیرقابل پیشبینی شوند.
برنامهنویسی ناهمزمان – مدیریت حافظه
میخواهم چند نکته را در مورد نحوه مدیریت موثر حافظه هنگام کار با برنامهنویسی ناهمزمان به اشتراک بگذارم، زیرا رسیدگی نادرست میتواند منجر به مشکلاتی در عملکرد مانند نشت حافظه شود.
مدیریت حافظه در برنامهنویسی ناهمزمان
هنگام کار با کد ناهمزمان، بسیار مهم است که به نحوه تخصیص حافظه و نحوه پاکسازی آن توجه کنیم. این موضوع به وظایف بلندمدت یا پرامیسهایی مربوط میشود که حل نشده باقی میمانند و اگر به درستی مدیریت نشوند، میتوانند منجر به نشت حافظه شوند.
جمعآوری زباله (Garbage Collection)
در جاوااسکریپت، حافظه توسط جمعآوریکننده زباله (garbage collector) مدیریت میشود. جمعآوریکننده زباله به طور خودکار حافظهای که دیگر توسط برنامه استفاده نمیشود را پاکسازی میکند. اما هنگام استفاده از برنامهنویسی ناهمزمان، اگر دقت نکنیم، ممکن است حافظه بیشتر از آنچه که نیاز است باقی بماند. به عنوان مثال، پرامیسهایی که هرگز حل نمیشوند، شنوندههای رویدادی که هنوز متصل هستند یا تایمرهای در حال اجرا ممکن است بخشهای بزرگتری از حافظه را نگه دارند.
دلایل رایج نشت حافظه در کد ناهمزمان
- پرامیسهای حلنشده: اگر یک پرامیس هرگز حل یا رد نشود، میتواند مانع از پاکسازی حافظه شود.
let pendingPromise = new Promise(function (resolve, reject) {
// This promise never resolves
});
- شنوندگان رویداد (Event Listeners): فراموش کردن حذف یک شنونده رویداد زمانی که دیگر نیازی به آن نیست، راحت است. این امر باعث مصرف غیرضروری حافظه میشود.
element.addEventListener("click", handleClick);
// Forgetting to remove the listener
// element.removeEventListener('click', handleClick);
- تایمرها (Timers): استفاده از
setTimeout
یاsetInterval
بدون پاکسازی آنها زمانی که دیگر نیازی به آنها نیست، میتواند منجر به نگهداری حافظه برای مدت طولانیتر از آنچه که لازم است، شود.
var timer = setInterval(function () {
console.log("Running.");
}, 1000);
// Forgetting to clear the interval
// clearInterval(timer);
بهترین شیوههایی که میتوان برای جلوگیری از نشت حافظه به کار برد
- پرامیسها، حل یا رد کردن: یک پرامیس باید حل یا رد شود تا اطمینان حاصل شود که هرگاه دیگر نیازی به آن نیست، حافظهاش آزاد میشود.
let myPromise = new Promise((resolve, reject) =>
setTimeout(() => {
resolve("Task complete");
}, 1000),
);
myPromise.then((result) => console.log(result));
- حذف شنوندگان رویداد (Event Listeners): هر زمان که شنوندگان رویداد متصل شدند، آنها را زمانی که دیگر نیاز نیست حذف کنید، چه به دلیل حذف عنصر مربوطه یا اینکه عملکرد آن دیگر نیاز نباشد.
element.addEventListener("click", handleClick);
// Proper cleanup when no longer needed
element.removeEventListener("click", handleClick);
- پاکسازی تایمرها (Clear Timers): اگر از
setTimeout
یاsetInterval
استفاده میکنید، به یاد داشته باشید که آنها را زمانی که کار خود را انجام دادند پاکسازی کنید تا از نگهداری حافظه غیرضروری جلوگیری شود.
var interval = setInterval(function () {
console.log('Doing something...');
}, 1000);
// Clear the interval when done
clearInterval(interval);
ارجاعات ضعیف (Weak References)
یکی دیگر از تکنیکهای پیشرفته استفاده از WeakMap
یا WeakSet
برای مدیریت اشیایی است که ممکن است به طور خودکار توسط جمعآوریکننده زباله پاکسازی شوند زمانی که دیگر در کد شما ارجاع داده نمیشوند. این ساختارها به شما اجازه میدهند که اشیاء را بدون جلوگیری از پاکسازی آنها توسط جمعآوریکننده زباله ارجاع دهید.
let myWeakMap = new WeakMap();
let obj = {};
myWeakMap.set(obj, "someValue");
// If obj gets dereferenced somewhere else, it will be garbage-collected.
obj = null;
نتیجه
با پایان بحث خود در مورد مدلهای برنامهنویسی همزمان و ناهمزمان، واضح است که هرکدام مزایای خاص خود را دارند که برای شرایط خاصی مناسب هستند. از آنجا که برنامهنویسی همزمان بهصورت ترتیبی و مسدودکننده عمل میکند، درک آن آسان است و برای وظایفی که نیاز به انجام به صورت خطی دارند، عالی است.
از طرف دیگر، برنامهنویسی ناهمزمان که به دلیل عدم مسدودسازی و امکان اجرای چندین وظیفه به طور همزمان شناخته میشود، بهترین عملکرد را زمانی دارد که نیاز به پاسخگویی و کارایی بالا باشد، بهویژه در عملیاتهای وابسته به ورودی/خروجی. استفاده از هرکدام از این رویکردها به درک شما از نیازهای برنامه، مشکلات عملکردی و تجربه کاربری که میخواهید بستگی دارد.