همزمانی در جاوااسکریپت

گردآوری و تالیف : ارسطو عباسی
تاریخ انتشار : 01 شهریور 1398
دسته بندی ها : جاوا اسکریپت

برای شروع کار با موضوع همزمانی در جاوااسکریپت ابتدا نیاز است تا دقیقا معنی همزمانی را بدانیم.

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

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

sayHi()
function sayHi() {
 console.log("Hello")
}
//this will work
sayHello()
let sayHello = () => {
 console.log("hello")
}
//this will not work

در کدهای بالا تابع sayHi() اجرا می‌شود چرا که تعریف تابع آن به صورت معمولی اتفاق افتاده و در این حالت اجرای تابع حتی قبل از تعریف آن می‌تواند صورت بگیرد.

اما مورد دوم یعنی تابع sayHello() از آنجایی که به صورت متغیر تعریف شده نمی‌تواند اجرا شود.

این بدان معناست که روش تفسیر متغیرها در جاوااسکریپت به صورت ترتیبی است. این دقیقا خلاف حالتی است که ما قصد رسیدن به آن را داریم. با این حال جاوااسکریپت مواردی را در اختیار ما قرار داده که قابلیت همزمانی را پیاده‌سازی می‌کنند. Callback، Promise و Async/Await. 

‌Callback

نودجی‌اس توانایی بالایی در مدیریت فرایند‌های I/O را دارد. این موضوع باعث می‌شود که استفاده از Callbackها بسیار جذاب و کاربردی شود. Callback را می‌توان یک تابع دانست که به صورت آرگومان ورودی یک تابع دیگر فراخوانی می‌شود. اما اگر به صورت تخصصی‌تر به این موضوع نگاه کنیم: رشته اصلی جاوااسکریپت در زمان اجرای این دسته از توابع یک رویداد را دریافت می‌کند که برای کار با آن تابع handler را اجرا می‌نماید. تابع handler برای این رویداد یک وظیفه جدید را تعیین کرده و پردازش آن را شروع می‌کند، این در حالی‌ست که رشته اصلی منتظر دریافت رویدادهای دیگری است. در این حالت اگر درخواست دیگری به اپلیکیشن وارد شود رشته اصلی هنوز می‌تواند آن را مدیریت کند. برای انجام این کار نیز رشته اصلی یک تابع handler را دوباره فراخوانی کرده و باز هم منتظر دریافت رویداد جدیدی می‌شود. به همین دلیل است که نودجی‌اس می‌تواند به صورت non-Blocking عمل کند. در نهایت انجام تمام این موارد به ما کمک می‌کند تا عملیات‌های همزمانی را اجرا کنیم.

let callbackFunc = (arg, callback) => {
  let arr = []
  for(i = 0; i < arg; i++) {
    arr.push({ name: 'User ${i + 1}', id: i + 1 })
  }
  callback(arr)
}

//an asynchronous callbackFunc that returns an array of 'n' objects where 'n' = arg

Promise

Promise سینتکسی است که در اکمااسکریپت ۶ به جاوااسکریپت اضافه شد. Promise یک شئ‌ است که می‌تواند روند تکمیل یا شکست یک عملیات غیرهمزمان را نشان دهد. این عملیات می‌تواند fetch() کردن یک api باشد. یک مثال ساده از Promise را می‌توانید در زیر مشاهده کنید:

let promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 5000)
})

promise1.then(value => {
  console.log(value)
  //output's 'success' after 5 seconds
}).catch(err => {
  console.log(err)
  //outputs the exception just in case one occurs and reject is returned instead.
})

console.log(promise1)
//outputs [object Promise]

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

یک Promise در حال کلی سه وضعیت می‌تواند داشته باشد:

  • Pending: وضعیت اولیه که در زمان اجرا اتفاق می‌افتد.
  • Fulfilled: زمانی که عملیات با موفقیت کامل انجام شود.
  • Rejected: زمانی که عملیات با مشکل مواجه شود.

وضعیت اولیه یا همان Pending در نهایت منجر به دو وضعیت بعدی خود خواهد شد.

برای آشنایی بیشتر با این موضوع می‌توانید این لینک را مطالعه نمایید. 

دستورات Async/Await

دستور async یک تابع غیرهمزمان را ایجاد می‌کند که یک شئ AsyncFunction را در نهایت برمی‌گرداند. این تابع با حلقه رویداد‌ها به صورت غیرهمزمان رفتار می‌کند. البته هر دو حالت Promise و Async شبیه به هم هستند اما سینتکس Async شباهت بیشتری با برنامه‌نویسی همزمانی داشته و به همین دلیل سینتکس محبوب برای پیاده‌سازی برنامه‌نویسی غیرهمزمان است. 

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

var resolveAfter25Seconds = (func) => {
    console.log(`starting a slow promise on: ${func}`)
    return new Promise(resolve => {
        setTimeout(function() {
            resolve(25)
            console.log(`slow promise is done on: ${func}`)
        }, 25000)
    })
}

var resolveAfter1Second = (func) => {
    console.log(`Starting quick promise on: ${func}`)
    return new Promise(resolve => {
        setTimeout(function() {
            resolve(25)
            console.log(`quick promise is done on: ${func}`)
        }, 1000)
    })
}

var sequentialStart = async () => {
    console.log("---Sequential Start---")
    const slow = await resolveAfter25Seconds('sequential start')
    const fast = await resolveAfter1Second('sequential start')
    console.log(slow + ' - sequential start')
    console.log(fast + ' - sequential start')
}

var concurrentStart = async () => {
    console.log("--- Concurrent Start ---")
    const slow = resolveAfter25Seconds('concurrent start')
    const fast = resolveAfter1Second('concurrent start')
    console.log(await slow + ' - concurrent start')
    console.log(await fast + ' - concurrent start')
}

var stillSerial = () => {
    console.log("-- Concurrent with Promise.then -- ")
    Promise.all([resolveAfter25Seconds('still serial'), resolveAfter1Second('still serial')]).then(([slow, fast]) => {
        console.log(slow + ' - still serial')
        console.log(fast + ' - still serial')
    })
}

var parallel = () => {
    console.log("-- Parallel with promise.then")
    resolveAfter25Seconds('parallel').then(message => { console.log(message + ' - parallel') })
    resolveAfter1Second('parallel').then(message => { console.log(message + ' - parallel') })
}

sequentialStart()
setTimeout(function() {
    console.log('5 seconds later...')
    concurrentStart()
    }, 5000)
setTimeout(function() {
    console.log('10 seconds later...')
    stillSerial()
    }, 10000)
setTimeout(function() {
    console.log('15 seconds later...')
    parallel()
    }, 15000)

کدهای بالا را اجرا کرده و به دقت روند اجرای توابع را مشاهده کنید. 

در پایان

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

در نهایت نیز باید بدانید که نودجی‌اس به صورت پیشفرض براساس مفهوم رویدادهای غیرهمزمان پیاده‌سازی شده است. بنابراین سازگاری بالایی با مدل همزمان دارد. 

منبع

مقالات پیشنهادی

  • 10 کتابخانه مفید جاوااسکریپت

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

    ارسطو عباسی