مفاهیم عمومی برنامه نویسی ناهمگام (Asynchronous Programming) — به زبان ساده

۳۵۵ بازدید
آخرین به‌روزرسانی: ۲۷ شهریور ۱۴۰۲
زمان مطالعه: ۶ دقیقه
مفاهیم عمومی برنامه نویسی ناهمگام (Asynchronous Programming) — به زبان ساده

در این مقاله برخی مفاهیم مهم مرتبط با برنامه نویسی ناهمگام و کارکرد آن‌ها در مرورگرهای وب و جاوا اسکریپت را بررسی می‌کنیم. پس از مطالعه مقاله حاضر، این مفاهیم را درک خواهید کرد و آماده مطالعه بخش‌های بعدی این سری مقالات آموزشی جاوا اسکریپت خواهید بود. همچنین برای مطالعه قسمت قبلی این مجموعه مطلب آموزشی می‌توانید به مطلب «تمرین ساخت شیئ در جاوا اسکریپت (بخش دوم) — راهنمای کاربردی» مراجعه کنید.

ناهمگام یعنی چه؟

به طور معمول کد یک برنامه به صورت خطی و مستقیم اجرا می‌شود و هر لحظه تنها یک کار در حال انجام است. اگر تابعی برای اجرا به نتیجه تابع دیگری نیاز داشته باشد، باید منتظر بماند تا تابع دیگر کار خود را تمام کرده و نتیجه را بازگشت دهد و تا زمانی که این اتفاق بیفتد، کل برنامه عملاً از نظر کاربر متوقف می‌شود.

برای نمونه کاربران Mac برخی اوقات این وضعیت را به شکل یک کرسر رنگین‌کمانی در حال چرخش تجربه می‌کنند. سیستم عامل با نمایش این کرسر در واقع اعلام می‌کند که برنامه کنونی که استفاده می‌کنید باید متوقف مانده و منتظر باشد تا چیز دیگری انجام یابد و این کار به مدت‌زمانی نیاز دارد و بدین‌وسیله اعلام می‌کنیم که جای نگرانی نیست و مشغول کار هستیم.

برنامه نویسی ناهمگام

این تجربه ناخوشایندی است و استفاده مناسبی از توان پردازشی رایانه به خصوص در این عصر که رایانه‌ها چندین هسته پردازشی دارند، به حساب نمی‌آید. این که منتظر بنشینیم تا کار دیگری پایان یابد، در حالی که می‌توان آن را به هسته پردازشی دیگری واگذار کرد تا در زمان خاتمه کار به ما اطلاع دهد. بدین ترتیب می‌توان کارها را به صورت همزمان پیش برد که اساس «برنامه‌نویسی ناهمگام» (asynchronous programming) را تشکیل می‌دهد. همه چیز به محیط برنامه که استفاده می‌کنید بستگی دارد تا با ارائه API–های مختلف امکان اجرای نامتقارن وظایف را فراهم سازد. در زمینه برنامه‌نویسی وب این محیط مرورگر وب است.

کد مسدودکننده

تکنیک‌های ناهمگام در برنامه‌نویسی و به خصوص برنامه‌نویسی وب بسیار مهم هستند. زمانی که یک وب اپلیکیشن در مرورگر اجرا می‌شود و یک دسته کد سنگین را بدون بازگرداندن کنترل به مرورگر اجرا می‌کند، ممکن است این گونه به نظر برسد که مرورگر قفل شده است. این وضعیت «مسدودسازی» (blocking) نامیده می‌شود. در این حالت مرورگر نمی‌تواند ورودی کاربر را مدیریت و وظایف دیگر را اجرا کند و تا زمانی که کنترل از پردازنده به مرورگر بازنگردد تداوم خواهد داشت.

در ادامه به بررسی چند مثال می‌پردازیم تا معنی دقیق مسدودسازی را درک کنیم.

در این مثال (+) چنان که ملاحظه می‌کنید یک «شنونده رویداد» به دکمه اضافه شده است تا زمانی که دکمه کلیک می‌شود یک عملیات زمان‌گیر را اجرا کند. در این عملیات 10 میلیون تاریخ محاسبه می‌شود و نتیجه در کنسول نمایش پیدا می‌کند. سپس یک پاراگراف به DOM اضافه خواهد شد.

1const btn = document.querySelector('button');
2btn.addEventListener('click', () => {
3  let myDate;
4  for(let i = 0; i < 10000000; i++) {
5    let date = new Date();
6    myDate = date
7  }
8
9  console.log(myDate);
10
11  let pElem = document.createElement('p');
12  pElem.textContent = 'This is a newly-added paragraph.';
13  document.body.appendChild(pElem);
14});

کنسول مرورگر

زمانی که این مثال را اجرا می‌کنید، کنسول جاوا اسکریپت را باز کنید و سپس روی دکمه کلیک کنید، متوجه خواهید شد که پاراگراف مزبور به جز تا زمانی که تاریخ‌ها محاسبه نشده باشند و پیام نهایی در کنسول نمایش نیافته باشد در صفحه ظاهر نمی‌شود. این کد به همان ترتیب که نوشته شده اجرا می‌شود و عملیات بعدی تا زمانی که عملیات قبلی به پایان نرسیده باشد اجرا نخواهد شد.

نکته: مثال قبلی بسیار غیر واقع‌گرایانه است. ما هرگز در یک وب اپلیکیشن یک میلیون تاریخ را محاسبه نمی‌کنیم. این مثال صرفاً جهت این ارائه شده است که ایده اولیه‌ای از موضوع داشته باشید.

در مثال دوم (+) چیزی را شبیه‌سازی می‌کنیم که اندکی واقع‌گرایانه‌تر است و احتمال بیشتری برای دیدن آن در یک صفحه وب وجود دارد. ما تعامل‌پذیری کاربر را از طریق رندر کردن UI مسدود می‌کنیم. در این مثال دو دکمه داریم:

  • یک دکمه «Fill canvas» که وقتی کلیک شود، 1 میلیون دایره آبی را روی <canvas> ارائه می‌کند.
  • یک دکمه «Click me for alert» که وقتی کلیک شود یک پیام هشدار را نمایش می‌دهد.
1function expensiveOperation() {
2  for(let i = 0; i < 1000000; i++) {
3    ctx.fillStyle = 'rgba(0,0,255, 0.2)';
4    ctx.beginPath();
5    ctx.arc(random(0, canvas.width), random(0, canvas.height), 10, degToRad(0), degToRad(360), false);
6    ctx.fill()
7  }
8}
9
10fillBtn.addEventListener('click', expensiveOperation);
11
12alertBtn.addEventListener('click', () =>
13  alert('You clicked me!')
14);

اگر دکمه اول را کلیک کنید و سپس به سرعت روی دکمه دوم کلیک نمایید، متوجه خواهید شد که هشدار تا زمانی که رندر شدن دایره‌ها به پایان نرسیده است، ظاهر نمی‌شود. عملیات نخست، عملیات دوم را مسدود می‌کند تا این که اجرایش به پایان برسد.

نکته: در این مورد کد زشت است و قصد ما صرفاً ایجاد حالت مسدودسازی بوده است، اما این مشکل رایجی است که توسعه‌دهندگان وب‌اپلیکیشن‌های واقعی همواره با آن دست‌به‌گریبان هستند.

دلیل این وضعیت آن است که جاوا اسکریپت به بیان کلی یک زبان برنامه‌نویسی «تک نخی» (single threaded) است. در این مرحله ابتدا باید با مفهوم «نخ» (thread) در برنامه‌نویسی آشنا شویم.

نخ

نخ اساساً یک پردازش منفرد است که برنامه می‌تواند برای اجرای وظیفه‌ای به خدمت بگیرد. هر نخ می‌تواند در هر زمان تنها یک وظیفه منفرد را اجرا کند:

Task A --> Task B --> Task C

هر وظیفه به صورت ترتیبی اجرا می‌شود و برای این که وظیفه‌ای بتواند شروع شود باید وظیفه قبلی به پایان رسیده باشد.

چنان که پیش‌تر گفتیم، بسیاری از رایانه‌ها در حال حاضر دارای پردازنده‌های چندهسته‌ای هستند و از این رو می‌توانند چندین کار را به طور همزمان اجرا کنند. زبان‌های برنامه‌نویسی که می‌توانند از چندین نخ پشتیبانی کنند، می‌توانند برای اجرای وظایف به صورت همزمان چند هسته پردازشی را به کار بگیرند.

Thread 1: Task A --> Task B
Thread 2: Task C --> Task D

جاوا اسکریپت یک زبان تک نخی است

جاوا اسکریپت به صورت سنتی یک زبان تک نخی است. حتی اگر چندین هسته پردازشی را به کار بگیرید، در هر حال باید وظایف را روی یک نخ منفرد اجرا کنید که «نخ اصلی» (main thread) نام دارد. بدین ترتیب مثال فوق به صورت زیر اجرا می‌شود:

Main thread: Render circles to canvas --> Display alert()

البته پس از مدتی جاوا اسکریپت از برخی ابزارها کمک گرفت تا چنین مشکلاتی را رفع کند. «وب ورکر» (Web worker) امکان ارسال چند پردازش جاوا اسکریپت به یک نخ جدا را که worker نام دارد فراهم می‌سازد. بدین ترتیب می‌توان چندین دسته کد جاوا اسکریپت را به صورت همزمان اجرا کرد. به طور کلی از یک worker برای اجرای پردازش‌های سنگین و برداشتن این وظیفه از دوش نخ اصلی استفاده می‌شود. بدین ترتیب تعامل کاربر با مرورگر مسدود نمی‌شود.

Main thread: Task A --> Task C
Worker thread: Expensive task B

با در نظر داشتن این موضوع، نگاهی به این مثال (+) می‌اندازیم. کنسول مرورگر را باز نگهدارید. در واقع این مثال یک بازنویسی از مثال قبلی است که 10 میلیون تاریخ را در نخ worker جداگانه‌ای اجرا می‌کرد. اکنون زمانی که روی دکمه کلیک کنید، مرورگر می‌تواند پاراگراف را پیش از اتمام محاسبه تاریخ‌ها نمایش دهد. به این ترتیب عملیات نخست موجب مسدود شدن عملیات دوم نمی‌شود.

کد ناهمگام

وب ورکرها کاملاً مفید هستند، اما آن‌ها نیز محدودیت‌های خاص خود را دارند. مهم‌ترین محدودیت وب ورکرها این است که نمی‌توانند به DOM دسترسی داشته باشند. بنابراین نمی‌توان انتظار داشت که یک وب ورکر مستقیماً کاری برای به‌روزرسانی UI انجام دهد. ما نمی‌توانیم 1 میلیون دایره آبی را درون worker رندر کنیم، چون وظیفه آن صرفاً محاسبات است.

دومین مشکل این است که گرچه کد در یک ورکر مسدودسازی ندارد، اما همچنان در ذات خود همگام است. این وضعیت در مواردی که تابعی به نتایج پردازش‌های قبلی نیاز داشته باشد، مشکل‌ساز خواهد بود. نمودارهای نخ زیر را در نظر بگیرید:

Main thread: Task A --> Task B

در این حالت، وظیفه A کاری مانند واکشی کردن یک تصویر از سرور را انجام می‌دهد و وظیفه B کاری مانند اعمال یک فیلتر را روی تصویر اجرا می‌کند. اگر ابتدا شروع به اجرای وظیفه A و سپس بی‌درنگ شروع به اجرای وظیفه B بکنید، با خطایی مواجه خواهید شد، چون تصویر هنوز آماده نشده است.

Main thread: Task A --> Task B --> |Task D|
Worker thread: Task C -----------> | |

در این حالت، فرض کنید وظیفه D از نتایج هر دو وظیفه B و C استفاده می‌کند. اگر بتوانیم تضمین کنیم که این نتایج هر دو به صورت همزمان آماده خواهند بود، در این صورت مشکلی وجود نخواهد داشت، اما چنین وضعیتی نامحتمل است. اگر وظیفه D تلاش کند زمانی که ورودی‌اش هنوز آماده نشده است، اجرا شود با خطا مواجه خواهد شد.

قابلیت‌های ناهمگام مرورگر

مرورگرها برای اصلاح چنین خطاهایی به ما اجازه می‌دهند که برخی عملیات را به صورت ناهمگام اجرا کنیم. قابلیت‌هایی مانند Promise-ها امکان تعیین حالت اجرایی برای یک عملیات (برای نمونه واکشی یک تصویر از سرور) و سپس صبر کردن برای بازگشت نتیجه تا پیش از اجرای عملیات بعدی را فراهم می‌سازند:

Main thread: Task A Task B
Promise: |__async operation__|

از آنجا که عملیات در جای دیگری اتفاق می‌افتد، در زمانی که عملیات ناهمگام در حال پردازش است، نخ اصلی مسدود نمی‌شود. در بخش بعدی این سری مقالات آموزشی به بررسی روش‌های نوشتن کدهای ناهمگام خواهیم پرداخت.

سخن پایانی

اصول طراحی مدرن نرم‌افزار به طور فزاینده‌ای پیرامون استفاده از برنامه‌نویسی ناهمگام توسعه می‌یابند تا به برنامه‌ها امکان اجرای همزمان چندین کار را بدهند. زمانی که از API-های مدرن و قدرتمندتر استفاده می‌کنید، مواردی بیشتری را خواهید یافت که تنها راه اجرای یک وظیفه برنامه‌نویسی ناهمگام است. نوشتن کد ناهمگام دشوارتر است و عادت کردن به آن به کمی زمان نیاز دارد، اما به مرور آسان‌تر خواهد شد. در بخش‌های بعدی این سری مقالات آموزشی به بررسی بیشتر دلیل اهمیت کدهای ناهمگام و روش‌های طراحی کد که از بروز مشکلات مطرح شده در این مقاله جلوگیری می‌کنند خواهیم پرداخت. برای مطالعه بخش بعدی به لینک زیر رجوع کنید:

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۶ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
developer.mozilla
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *