چهارشنبه , ۲ خرداد ۱۳۹۷

برنامه‌نویسی موازی در جاوا اسکریپت با Web Workers

 

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

بعضی از این موانع مربوط به مرورگر، محدودیت‌ها و کارایی آن است. خوشبختانه در سال‌های اخیر در مرورگرها پیشرفت‌های بسیاری داشته‌اند و توسعه‌دهندگان آن‌ها، به میزان چشم‌گیری سرعت موتورهای جاوااسکریپت را بهبود بخشیده‌اند. اما یکی از چیزهایی که همچنان مانع بزرگی به شمار می‌رود، خود زبان جاوااسکریپت و ویژگی‌های آن است. جاوااسکریپت زبانی با یک رشته پردازشی (thread) است. در نتیجه اسکریپت‌های مختلف نمی‌توانند با هم در یک زمان اجرا شوند. برای نمونه، سایتی را در نظر بگیرید که لازم است در آن تعامل با واسط کاربر، پرس‌و‌جوها، پردازش داده‌هایی زیاد و اعمال تغییرات در DOM انجام شود. این موارد، اموری کاملاً عمومی هستند. با وجود این، به دلیل محدودیت‌های زمان اجرای جاوااسکریپت در مرورگر، نمی‌توانند هم‌زمان انجام شوند و اجرای اسکریپت‌ها در یک رشته پردازش انجام می‌شود.
برخی توسعه‌دهندگان با تکنیک‌هایی مانند استفاده از setTimeout() ،setInterval() ،XMLHttpRequest و اداره‌کننده‌های رویدادها، هم‌زمانی را شبیه‌سازی می‌کنند. اگرچه این ویژگی‌ها کارکردی ناهمگام دارند، این لزوماً به معنای هم‌زمانی نیست. اما هم‌زمان با HTML5 امکانی در اختیار توسعه‌دهندگان قرار داده شده است که دیگر نیازی به استفاده از این ترفندها نباشد.

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

 

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

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

var worker = new Worker(‘task.js’);

اگر فایل مشخص شده موجود باشد، مرورگر یک رشته پردازشی جدید تولید می‌کند و بارگذاری آن فایل را به صورت ناهمگام آغاز خواهد کرد. کار ورکر تا زمانی که فایل به طور کامل بارگذاری نشده باشد، آغاز نخواهد شد. در صورتی که مسیر درخواستی برای دریافت فایل، خطای 404 به معنای عدم وجود فایل را برگرداند، کار ورکر بدون اتفاق خاصی بی‌سرو صدا متوقف خواهد شد. پس از ساخت ورکر با فراخوانی متد (postMessage)، کار آن آغاز می‌شود:

 

worker.postMessage(); // Start the worker.

برقراری ارتباط با ورکر از طریق تبادل پیام ارتباط بین یک ورکر و صفحه والد آن با استفاده از یک event model  و متد (postMessage) برقرار می‌شود. بسته به مرورگر شما و نگارش آن، این متد می‌تواند یک رشته کاراکتری یا یک شی جی‌سان (JSON) را به عنوان تنها آرگومان خود بپذیرد. آخرین نگارش مرورگرهای مدرن از هر دو نوع آرگومان یاد شده برای ارسال به این متد پشتیبانی می‌کنند. در ادامه مثالی را مشاهده می‌کنید که در آن یک رشته کاراکتری به یک ورکر در فایل doWork.js ارسال شده است و ورکر نیز خیلی ساده، پیامی را که به آن ارسال شده است به عنوان نتیجه بر‌می‌گرداند.

اسکریپت اصلی:

var worker = new Worker(‘doWork.js’);

worker.addEventListener(‘message’, function(e) {
console.log(‘Worker said: ‘, e.data);
}, false);

// Send data to our worker.
worker.postMessage(‹Hello World›);

اسکریپت ورکر (فایل doWork.js):
self.addEventListener(‘message’, function(e) {
self.postMessage(e.data);
}, false);

زمانی که متد (postMessage) از صفحه اصلی فراخوانی می‌شود، ورکر آن پیام را با تعریف یک اداره‌کننده onmessage برای رویداد message اداره می‌کند. بدنه پیام (در اینجا رشته کاراکتری Hello World)  از طریق Event.data دسترس‌پذیر خواهد بود. اگرچه این مثال به‌خصوص چندان هیجان‌انگیز نیست، اما نمایانگر آن است که (postMessage) وسیله‌ای برای بازگرداندن داده به رشته پردازشی اصلی نیز هست.
توجه داشته باشید، پیام ارسال شده بین صفحه اصلی و ورکر یک پیام اشتراکی نیست و در زمان هر ارسال، یک کپی از پیام ساخته و ارسال می‌شود. به عنوان نمونه در مثال بعد، خصیصه1 msg مربوط به پیام جی‌سان از هر دو محل دسترس‌پذیر است. نشان می‌دهد که با وجود این‌که ورکر در فضایی اختصاصی و جداگانه در حال اجرا است، شی به طور مستقیم به آن ارسال می‌شود. این مثال که اندکی پیچیده‌تر از مثال قبل است، ارسال پیام با استفاده از اشیا جی‌سان را نشان می‌دهد.
اسکریپت اصلی:

<button onclick=”sayHI()”>Say HI</button>
<button onclick=”unknownCmd()”>Send unknown command</button>
<button onclick=”stop()”>Stop worker</button>
<output id=”result”></output>

<script>
function sayHI() {
worker.postMessage({‘cmd’: ‘start’, ‘msg’: ‘Hi’});
}

function stop() {
// worker.terminate() from this script would also stop the worker.
worker.postMessage({‘cmd’: ‘stop’, ‘msg’: ‘Bye’});
}

function unknownCmd() {
worker.postMessage({‘cmd’: ‘foobard’, ‘msg’: ‘???’});
}

var worker = new Worker(‘doWork2.js’);

worker.addEventListener(‘message’, function(e) {
document.getElementById(‘result’).textContent = e.data;
}, false);
</script>

فایل doWork2.js
self.addEventListener(‘message’, function(e) {
var data = e.data;
switch (data.cmd) {
case ‘start’:
self.postMessage(‘WORKER STARTED: ‘ + data.msg);
break;
case ‘stop’:
self.postMessage(‘WORKER STOPPED: ‘ + data.msg +
‘. (buttons will no longer work)’);
self.close(); // Terminates the worker.
break;
default:
self.postMessage(‘Unknown command: ‘ + data.msg);
};
}, false);

توجه داشته باشید برای پایان دادن به کار یک ورکر دو راه وجود دارد: یکی فراخوانی (worker.terminate) از صفحه اصلی و دیگری فراخوانی (self.close) از درون خود ورکر.

اشیای قابل انتقال2
بیش‌تر مرورگرها الگوریتم همسان‌سازی ساخت‌یافته‌ای3 را پیاده‌سازی کرده‌اند که به شما امکان ارسال انواع داده‌ای پیچیده‌تری همچون فایل، Blob، ArrayBuffer و جی‌سان را به ورکر می‌دهند. البته هنگام ارسال این انواع داده‌ای نیز با استفاده از (postMessage) همچنان یک کپی از آن‌ها ساخته می‌شود. بنابراین، اگر برای مثال قصد ارسال یک فایل بزرگ 50 مگابایتی را داشته باشید سربار درخور توجهی برای تبادل آن فایل بین ورکر و رشته پردازشی اصلی وجود خواهد داشت. همسان‌سازی ساخت‌یافته امکان بسیار خوبی است، اما ساخت یک کپی می‌تواند صدها و هزاران میلی‌ثانیه طول بکشد. راه حل این مشکل استفاده از اشیای قابل انتقال است. با وجود این اشیا، داده‌ها بدون اجرای عمل کپی، از یک متن به دیگری منتقل می‌شوند که این کار در هنگام انتقال داده به یک ورکر، کارایی را به میزان چشم‌گیری بهبود می‌بخشد. اگر تجربه کار با زبان C یا ++C را دارید، می‌توانید مفهوم آن را با ارسال مرجع4 قابل مقایسه بدانید. با این تفاوت که برخلاف روش ارسال مرجع، در زمان انتقال اشیا از یک متن به متن دیگر، شی در متن فراخواننده دسترس‌ناپذیر می‌شود. برای نمونه، زمان انتقال یک ArrayBuffer از برنامه اصلی به ورکر، ArrayBuffer اصلی پاک شده و دیگر قابل استفاده نخواهد بود و محتوای آن به متن ورکر منتقل خواهد شد.
برای استفاده از اشیای قابل انتقال نیز از متد (postMessage) البته با اندکی تفاوت در فراخوانی استفاده می‌شود:

worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);

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

worker.postMessage({data: int8View, moreData: anotherBuffer},
[int8View.buffer, anotherBuffer]);

اما آرگومان دوم لازم است یک آرایه از نوع ArrayBuffer باشد که لیست عناصر قابل انتقال شما هستند. نمودار شکل 2 آزمایش میزان بهبود سرعت اشیا قابل انتقال نسبت به همسان‌سازی ساخت‌یافته را در مرورگر کروم نشان می‌دهد. نمودار سمت چپ مربوط به همسان‌سازی ساخت‌یافته در فایرفاکس است که بیش‌ترین سرعت را در بین مرورگر‌ها داشته است. نمودار وسط همسان‌سازی ساخت‌یافته در کروم را نشان می‌دهد که ضعیف‌تر از فایرفاکس عمل کرده است و بالاخره در نمودار سمت راست مشا‌هده می‌کنید که اشیا قابل انتقال به چه میزان در بهبود کارایی تأثیر داشته‌اند.
برای آزمایش عملی سرعت کارکرد اشیا قابل انتقال نیز می‌توانید برنامه موجود در آدرس ذیل را در مرورگر خود اجرا کنید:

http://html5-demos.appspot.com/static/workers/
transferables/index.html

حوزه کارکرد5 ورکر
در متن یک ورکر، هم نشانگر this و هم self به حوزه سراسری ورکر اشاره خواهند داشت. بنابراین، مثال قبل می‌تواند به این شکل نیز نوشته شود:

addEventListener(‘message’, function(e) {
var data = e.data;
switch (data.cmd) {
case ‘start’:
postMessage(‘WORKER STARTED: ‘ + data.msg);
break;
case ‘stop’:

}, false);

مشاهده می‌کنید که چون self به حوزه سراسری اشاره دارد، نوشتن آن در self.postMessage تأثیری در کارکرد این مثال ندارد. همچنین می‌توانید اداره کننده رویداد onmessage را به صورت مستقیم نیز تعریف کنید:

onmessage = function(e) {
var data = e.data;

};

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

ویژگی‌های در دسترس ورکرها
به دلیل خصلت چند رشته‌ای (multi-thread) ورکرها، فقط زیرمجموعه‌ای از ویژگی‌های جاوااسکریپت از جمله موارد ذیل برای ورکرها در دسترس هستند:
– شیء navigator
– شی‌ء location (فقط برای خواندن)
− XMLHttpRequest
− (setTimeout) و (clearTimeout)
−  (setInterval) و (clearInterval)
− Application Cache
– استفاده از اسکریپت‌های بیرونی با استفاده از (importScripts)
– ساخت ورکرهای دیگر

اما این ویژگی‌ها برای ورکرها در دسترس نیستند:
− DOM
– شی‌ء window
– شی‌ء document
– شی‌ء parent

بارگذاری اسکریپت‌های بیرونی
شما می‌توانید با استفاده از تابع (importScripts) فایل‌ها یا کتابخانه‌های بیرونی را داخل یک ورکر بارگذاری کنید. برای این کار باید نام فایل را به صورت رشته کاراکتری به این تابع ارسال کنید:

importScripts(‘script1.js’);
importScripts(‘script2.js’);

بارگذاری بیش از یک فایل با استفاده از یک دستور نیز امکان‌پذیر خواهد بود:

importScripts(‘script1.js’, ‘script2.js’);

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

ورکرهای درون‌برنامه‌ای7
ممکن است این سؤال پیش بیاید که اگر بخواهیم به صورت بی‌درنگ اسکریپت ورکر را بسازیم و یا صفحه‌ای داشته باشیم که بتوانیم در آن بدون ساختن فایل‌های جداگانه از ورکرها استفاده کنیم، چه باید بکنیم؟ پاسخ این پرسش استفاده از (blob) است. همان‌طور که در مثال بعد مشاهده خواهید کرد، با استفاده از (blob) می‌توان در همان فایل HTML که حاوی منطق اصلی برنامه است به ساخت ورکر درون برنامه‌ای پرداخت.

var blob = new Blob([
“onmessage = function(e) { postMessage(‘msg from worker’); }”]);

// Obtain a blob URL reference to our worker ‘file’.
var blobURL = window.URL.createObjectURL(blob);

var worker = new Worker(blobURL);
worker.onmessage = function(e) {
// e.data == ‘msg from worker’
};
worker.postMessage(); // Start the worker.

مشاهده می‌شود که برای این کار لازم است آرگومانی را که قرار است به جای URL به ورکر ارسال کنیم، با استفاده از (createObjectURL) مهیا سازیم. از این متد ارزشمند می‌توان برای ارجاع به داده‌ ذخیره شده در یک فایل DOM یا blob بهره گرفت.

در صورتی که بخواهیم اسکریپت درون‌برنامه‌ای ورکر را از بدنه اصلی برنامه جدا کنیم، می‌توانیم با استفاده از تگ script و تعیین نوع javascript/worker برای آن، به این هدف دست یابیم. با این کار آن بخش از کد که داخل این تگ نوشته شده باشد، در زمان تفسیر صفحه اصلی توسط موتور جاوااسکریپت تفسیر نمی‌شود و اجرای آن به زمان ساخت thread ورکر موکول می‌شود. متن کامل این مثال در ادامه قابل مطالعه است:

<!DOCTYPE html>
<html>
<head>
<meta charset=”utf-8″ />
</head>
<body>

<div id=”log”></div>

<script id=”worker1″ type=”javascript/worker”>
// This script won’t be parsed by JS engines
// because its type is javascript/worker.
self.onmessage = function(e) {
self.postMessage(‘msg from worker’);
};
// Rest of your worker code goes here.
</script>

<script>
function log(msg) {
// Use a fragment: browser will only render/reflow once.
var fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode(msg));
fragment.appendChild(document.createElement(‘br’));

document.querySelector(“#log”).appendChild(fragment);
}

var blob = new Blob([document.querySelector(‘#worker1’).textContent]);

var worker = new Worker(window.URL.createObjectURL(blob));
worker.onmessage = function(e) {
log(“Received: ” + e.data);
}
worker.postMessage(); // Start the worker.
</script>
</body>
</html>

این روش اندکی واضح‌تر و خواناتر است. کد مربوط به ورکر که با شناسه worker1 مشخص شده است، با استفاده از متد (querySelector) از درون فایل اصلی استخراج شده و برای ساخت فایل به (blob) ارسال می‌شود.

بارگذاری اسکریپت‌های بیرونی در ورکرهای درون‌برنامه‌ای
هنگامی‌که از تکنیک‌های شرح داده‌شده برای ادغام ورکرها درون فایل اصلی برنامه خود استفاده می‌کنید، (importScripts) تنها زمانی به درستی عمل خواهد کرد که به آن یک آدرس مطلق ارسال کنید. در صورتی‌که قصد ارسال یک آدرس محلی را داشته باشید، مرورگر جلوی این کار را با اعلام یک خطای امنیتی خواهد گرفت.
دلیل این رخداد به این شکل قابل توضیح است: ورکری که از یک blob ساخته می‌شود در مرورگر با آدرسی در دسترس برنامه قرار می‌گیرد که پیشوند:blob دارد. در حالی که برنامه شما با آدرسی در حال اجرا است که به احتمال پیشوند//:http دارد.
بنابراین، به‌دلیل وجود محدودیت‌های مسئله‌ای با نام مسئله چندمنبعی8 جلوی این کار گرفته می‌شود. یک راه برای استفاده از (importScripts) در یک ورکر درون‌برنامه‌ای، ارسال آدرس فعلی برنامه اصلی به ورکر و تبدیل آن به یک آدرس مطلق به صورت دستی، درون ورکر است. این کار تضمین خواهد کرد که اسکریپت بیرونی از همان منبع بارگذاری شده است و مشکل مربوط به چندمنبعی رخ نخواهد داد. فرض کنید برنامه اصلی شما از آدرس http://example.com/index.html در حال اجرا است. راهکار شرح داده شده به این شکل پیاده‌سازی خواهد شد:


<script id=”worker2″ type=”javascript/worker”>
self.onmessage = function(e) {
var data = e.data;

if (data.url) {
var url = data.url.href;
var index = url.indexOf(‘index.html’);
if (index != -1) {
url = url.substring(0, index);
}
importScripts(url + ‘engine.js’);
}

};
</script>
<script>
var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
worker.postMessage({url: document.location});
</script>

رسیدگی به خطاها
مانند هر برنامه دیگر، لازم است به خطاهایی که درون ورکر نیز رخ می‌دهند، رسیدگی شود. اگر هنگامی که ورکر درحال اجرا است خطایی پیش بیاید، رویداد ErrorEvent رخ خواهد داد. این رویداد سه خصیصه مفید برای تشخیص اشکال در اختیار ما می‌گذارد:
filenam: نام اسکریپت ورکری که موجب بروز خطا شده است؛
lineno: شماره خطی که خطا در آن رخ داده است؛
message: توصیفی قابل درک از خطای رخ داده.
در ادامه، مثالی از آماده‌سازی onerror به عنوان اداره‌کننده خطا برای نمایش خصیصه‌های یک خطا مشاهده می‌کنید:

<output id=”error” style=”color: red;”></output>
<output id=”result”></output>

<script>
function onError(e) {
document.getElementById(‘error’).textContent = [
‘ERROR: Line ‘, e.lineno, ‘ in ‘, e.filename, ‘: ‘, e.message
].join(”);
}

function onMsg(e) {
document.getElementById(‘result’).textContent = e.data;
}

var worker = new Worker(‘workerWithError.js’);
worker.addEventListener(‘message’, onMsg, false);
worker.addEventListener(‘error’, onError, false);
worker.postMessage(); // Start worker without a message.
</script>

ورکر workerWithError.js تلاش می‌کند عمل تقسیم 1 بر x را انجام دهد که مقدار x در آن تعریف نشده (undefined) است:

self.addEventListener(‘message’, function(e) {
postMessage(1/x); // Intentional error.
};

تمرین بیش‌تر
با وجود گذشت زمان زیادی از معرفی وب‌ورکرها، این موضوع هنوز به نسبت جدید است و چندان فراگیر نشده است. به همین دلیل ممکن است مثال‌های متنوعی از آن پیدا نکنید. مثال‌هایی از قبیل محاسبه اعداد اول نیز با وجود کمک به درک مفهوم وب‌ورکرها چندان جذابیتی برای توسعه‌دهندگان ندارند! اما، اکنون که با این مفهوم آشنا شده‌اید می‌توانید ذهن خود را با فکر کردن درباره کاربردهای مفیدتر تقویت کنید. چند نمونه از این کاربردها را ما پیشنهاد می‌کنیم:
– واکشی داده‌های مورد نیاز برنامه، قبل از آن‌که زمان استفاده از آن‌ها فرا رسیده باشد.
– رنگ‌بندی کد و یا هرگونه قالب‌دهی متن به صورت بی‌درنگ.
– بررسی و تصحیح املای کلمات در زمان تایپ متن.
– فراخوانی وب‌سرویس‌ها در پس‌زمینه.
– پردازش آرایه‌های بزرگ یا پردازش پاسخ‌های بزرگ جی‌سان    دریافتی از سرویس‌دهنده.
– پردازش یا اعمال تغییرات گرافیکی در Canvas.
شما نیز تلاش کنید تا با ذهن خلاق خود کاربردهای دیگری برای این ویژگی مفید پیدا کرده و از آن برای بهبود کارایی برنامه‌های خود استفاده کنید.

منبع : ماهنامه شبکه

Print Friendly, PDF & Email

جوابی بنویسید

ایمیل شما نشر نخواهد شدخانه های ضروری نشانه گذاری شده است. *

*