۲۹ شهریور ۱۴۰۳

Techboy

اخبار و اطلاعات روز تکنولوژی

مقدمه ای بر جاوا اسکریپت چند رشته ای

فرار از حلقه رویداد تک رشته ای در مرورگرها و سرور. در اینجا نحوه استفاده از worker thread و web work برای چند رشته ای مدرن در جاوا اسکریپت آمده است.

فرار از حلقه رویداد تک رشته ای در مرورگرها و سرور. در اینجا نحوه استفاده از worker thread و web work برای چند رشته ای مدرن در جاوا اسکریپت آمده است.

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

موازی در مقابل همزمانی

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

در جاوا اسکریپت وانیلی، می‌توانید به پلتفرم بگویید چند کار را انجام دهد:


function fetchPerson(id) {
  return new Promise((resolve, reject) => {
    fetch(`https://swapi.dev/api/people/${id}`)
      .then(response => response.json())
      .then(data => resolve(data))
      .catch(error => reject(error));
  });
}

const lukeId = 1;
const leiaId = 5;

console.log("Fetching Star Wars characters...");

// Fetch character data concurrently (non-blocking)
Promise.all([fetchPerson(lukeId), fetchPerson(leiaId)])
  .then(data => {
    console.log("Characters received:");
    console.log(data[0]); // Data for Luke Skywalker (ID: 1)
    console.log(data[1]); // Data for Leia Organa (ID: 5)
  })
  .catch(error => console.error("Error fetching characters:", error));

console.log("Moving on to other things...");

// Fetching Star Wars characters...
// Moving on to other things...

Characters received:
{name: 'Luke Skywalker', height: '172', mass: '77', …}
{name: 'Leia Organa', height: '150', mass: '49', …}

به نظر می‌رسد که با استفاده از Promise.all برای اجرای دو تماس fetch با هم، داده‌های لوک و لیا را به طور همزمان واکشی می‌کند. با این حال، در حقیقت، جاوا اسکریپت هر کار را به گونه‌ای برنامه‌ریزی می‌کند که توسط یک رشته برنامه مدیریت شود. 

این به این دلیل است که جاوا اسکریپت از یک حلقه رویداد استفاده می‌کند. حلقه چیزهایی را از یک صف به قدری سریع برمی‌دارد که به نظر می‌رسد اغلب به صورت همزمان اتفاق می‌افتد – اما این یک فرآیند واقعاً همزمان نیست.

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

چند ریدینگ با کارگران وب

کارگران وب راهی برای ایجاد موضوعات در مرورگر وب به شما ارائه می دهند. شما فقط می توانید یک اسکریپت کارگر مجزا از اسکریپت اصلی بارگیری کنید و پیام های ناهمزمان را مدیریت می کند. هر کنترل کننده پیام در رشته خاص خود اجرا می شود که به شما موازی سازی واقعی می دهد.

برای مثال ساده Star Wars API، می‌خواهیم رشته‌هایی ایجاد کنیم که درخواست‌ها را رسیدگی یا واکشی کنند. بدیهی است که استفاده از وب‌کارگر برای این کار زیاد است، اما همه چیز را ساده نگه می‌دارد. ما می‌خواهیم یک وب‌کارگر ایجاد کنیم که پیامی را از رشته اصلی بپذیرد و درخواست‌ها را صادر کند.

در اینجا اسکریپت اصلی ما (main.js) اکنون به نظر می رسد:


function fetchPersonWithWorker(id) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('worker.js');

    worker.onmessage = function(event) {
      if (event.data.error) {
        reject(event.data.error);
      } else {
        resolve(event.data);
      }
      worker.terminate(); // Clean up the worker after receiving the data
    }

    worker.postMessage({ url: `https://swapi.dev/api/people/${id}` });
  });
}

const lukeId = 1; const leiaId = 5;

console.log("Fetching Star Wars characters with web worker...");

// Fetch character data concurrently (truly parallel)
Promise.all([fetchPersonWithWorker(lukeId), fetchPersonWithWorker(leiaId)])
  .then(data => {
    console.log("Characters received:");
    console.log(data[0]); // Data for Luke Skywalker (ID: 1)
    console.log(data[1]); // Data for Leia Organa (ID: 5)
  })
  .catch(error => console.error("Error fetching characters:", error));

console.log("Moving on to other things...");

این شبیه به مثال اول است، اما به جای استفاده از تابعی که به صورت محلی در Promise.all کار می کند، تابع fetchPersonWithWorker را ارسال می کنیم. این تابع اخیر یک شی Worker به نام worker ایجاد می کند که با فایل worker.js پیکربندی شده است. 

هنگامی که شی کارگر ایجاد شد، یک رویداد onmessage روی آن ارائه می‌کنیم. ما از این برای مدیریت پیام‌هایی که از طرف کارگر بازمی‌گردد استفاده می‌کنیم. در مورد ما، قولی را که برمی‌گردانیم حل می‌کنیم یا آن را رد می‌کنیم (که توسط Promise.all در اسکریپت اصلی مصرف می‌شود)، سپس کارگر را فسخ می‌کنیم.

پس از آن، worker.postMessage() را فراخوانی می کنیم و یک شیء ساده JSON را با یک فیلد URL به آدرس URL که می خواهیم فراخوانی کنیم، ارسال می کنیم. 

کارگر وب

در اینجا طرف دیگر معادله در worker.js است:


// worker.js
onmessage = function(event) {
  console.log(“onmessage: “ + event.data); //  {"url":"https://swapi.dev/api/people/1"}
  const { url } = event.data;
  fetch(url)
    .then(response => response.json())
    .then(data => postMessage(data))
    .catch(error => postMessage({ error }));
}

کنترل‌کننده onmessage ساده ما رویداد را می‌پذیرد و از فیلد URL برای برقراری تماس‌های واکشی مشابه قبلی استفاده می‌کند، اما این بار از postMessage() برای برقراری ارتباط نتایج استفاده می‌کنیم. بازگشت به main.js.

بنابراین، می‌توانید ببینید که ما بین دو جهان با پیام‌هایی با استفاده از postMessage و onmessage ارتباط برقرار می‌کنیم. به یاد داشته باشید: کنترل‌کننده‌های onmessage در worker به‌صورت ناهمزمان در رشته‌های خودشان رخ می‌دهند. (از متغیرهای محلی برای ذخیره داده ها استفاده نکنید. احتمالاً پاک می شوند).

کارگران مشترک

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

نوارهای سمت سرور با رشته های کارگر

حالا با استفاده از Node، نگاهی به سمت سرور بیندازیم. js. در این مورد، به جای وب کارگران، از مفهوم کارگر رشته استفاده می کنیم. یک worker thread شبیه به web worker است که پیام‌ها را از رشته اصلی به worker منتقل می‌کنیم. 

به عنوان مثال، فرض کنید دو فایل داریم، main.js و worker.js. ما main.js را اجرا می کنیم (با استفاده از دستور: node main.js) و با بارگیری worker.js به عنوان یک رشته ایجاد می کند. یک نخ کارگر اینجا فایل main.js ما است:


const { Worker } = require('worker_threads');

function fetchPersonWithWorker(id) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData: id });

    worker.on('message', (data) => {
      if (data.error) {
        reject(data.error);
      } else {
        resolve(data);
      }
      worker.terminate();
    });

    worker.on('error', (error) => reject(error));

    let url = `https://swapi.dev/api/people/${id}`;
    worker.postMessage({ url });
  });
}

const lukeId = 1;
const leiaId = 5;

console.log("Fetching Star Wars characters with worker threads...");

Promise.all([fetchPersonWithWorker(lukeId), fetchPersonWithWorker(leiaId)])
  .then(data => {
    console.log("Characters received: "+ JSON.stringify(data) );
    console.log(data[0]); // Data for Luke Skywalker (ID: 1)
    console.log(data[1]); // Data for Leia Organa (ID: 5)
  })
  .catch(error => console.error("Error fetching characters:", error));

console.log("Moving on to other things...");

ما Worker را از ماژول worker_threads وارد می‌کنیم، اما توجه داشته باشید که در Node تعبیه شده است، بنابراین برای این کار نیازی به NPM نداریم. برای راه اندازی worker، یک شی Worker جدید ایجاد می کنیم و فایل worker.js را به عنوان پارامتر به آن می دهیم. هنگامی که این کار انجام شد، یک شنونده پیام اضافه می کنیم که قول ما را حل می کند یا رد می کند – این دقیقاً مانند کاری است که برای وب کارگر انجام دادیم. همچنین پس از اتمام کار، برای پاکسازی منابع، کارگر را اخراج می کنیم.

در نهایت، یک پیام جدید حاوی URL مورد نظر برای بازیابی به کارگر ارسال می کنیم.

رشته کارگر

در اینجا نگاهی به worker.js داریم:


const { parentPort } = require('worker_threads');

parentPort.on('message', (msg) => {
        console.log("message(worker): " + msg.url);

  fetch(msg.url)
    .then(response => response.json())
    .then(data => parentPort.postMessage(data))
    .catch(error => parentPort.postMessage({ error }));
});

دوباره از worker_threads وارد می کنیم، این بار شی parentPort. این شیئی است که به ما امکان می دهد با رشته اصلی ارتباط برقرار کنیم. در مورد ما، ما به رویداد پیام گوش می دهیم، و پس از دریافت، فیلد url را از آن باز می کنیم و از آن برای صدور درخواست استفاده می کنیم.

به این ترتیب ما به درخواست‌های واقعاً همزمان برای URLها دست یافته‌ایم. اگر نمونه را با node main.js اجرا کنید، داده‌های خروجی هر دو URL را به کنسول خواهید دید.

نتیجه گیری

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

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

نمونه‌های قابل اجرا برای این مقاله را می‌توانید در GitHub اینجا پیدا کنید. برای اجرای مثال web worker،

را تایپ کنید


$ node server.js 

از دایرکتوری ریشه.

برای اجرای مثال worker thread، تایپ کنید:


~/worker-thread $ node main.js

موارد بیشتر توسط متیو تایسون: