SharedArrayBuffer في JavaScript

SharedArrayBuffer في JavaScript

تشرح هذه المقالة SharedArrayBuffer في JavaScript۔

سنقدم شرحًا مفصلًا لأساسيات SharedArrayBuffer، وكيفية استخدامه، والحالات العملية المحددة، واعتبارات الأمان.۔

YouTube Video

SharedArrayBuffer في JavaScript

SharedArrayBuffer هي أداة قوية في JavaScript لمشاركة الذاكرة بين عدة خيوط (Threads)۔ خاصة عند استخدامها مع Web Workers، توفر معالجة متوازية، مما يجعلها فعالة للمهام المرهقة حسابيًا والتطبيقات التي تتطلب قدرات في الوقت الفعلي۔

ما هو SharedArrayBuffer؟

SharedArrayBuffer يوفر ذاكرة تخزين مؤقتة في JavaScript تسمح بمشاركة البيانات الثنائية بين عدة خيوط (خصوصًا Web Workers)۔ ArrayBuffer العادي يتطلب نسخ البيانات بين الخيط الرئيسي والـ Workers، بينما يسمح SharedArrayBuffer بمشاركة الذاكرة مباشرة دون الحاجة إلى النسخ، مما يحسن الأداء بشكل كبير۔

الميزات

  • الذاكرة المشتركة يسمح للعديد من الخيوط (threads) بالعمل في نفس مساحة الذاكرة۔
  • تحسين الأداء نظرًا لإمكانية الاستغناء عن النسخ، يتم تقليل العبء الإضافي عند معالجة كميات كبيرة من البيانات۔
  • مزامنة الخيوط (threads) يمكنك استخدامه مع Atomics لإجراء المزامنة من أجل تجنب التعارضات عند الوصول إلى الذاكرة۔

مثال على الاستخدام الأساسي

 1// Create a 16-byte shared memory
 2const sharedBuffer = new SharedArrayBuffer(16);
 3
 4// Treat it as an Int32Array
 5const sharedArray = new Int32Array(sharedBuffer);
 6
 7// Set a value
 8sharedArray[0] = 42;
 9
10console.log(sharedArray[0]);  // 42

في هذا المثال، نقوم بإنشاء مساحة ذاكرة بحجم 16 بايت باستخدام SharedArrayBuffer ونتعامل مع تلك المساحة كـ Int32Array۔ يمكن مشاركة هذه الذاكرة المؤقتة بين خيوط متعددة۔

الاستخدام مع Web Workers

تظهر القيمة الحقيقية لـ SharedArrayBuffer عند استخدامها بالتزامن مع Web Workers۔ الكود التالي هو مثال لاستخدام ذاكرة مشتركة بين الخيط الرئيسي و Worker۔

في الخيط الرئيسي

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Modify the memory
12// Output : Main thread: 100
13sharedArray[0] = 100;
14console.log("Main thread: ", sharedArray[0]);

على جانب Worker (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    // Use the received shared buffer
 4    const sharedArray = new Int32Array(event.data);
 5
 6    // Read the contents of the memory
 7    // Output : Worker thread: 100
 8    console.log("Worker thread: ", sharedArray[0]);
 9
10    // Change the value
11    sharedArray[0] = 200;
12};
  • في هذا المثال، يقوم الخيط الرئيسي بإنشاء ذاكرة مشتركة ويمررها إلى الـ Worker۔ يمكن للـ Worker الوصول إلى هذه الذاكرة لقراءة القيم وتعديلها۔ بهذه الطريقة، يمكن مشاركة البيانات بين الخيوط دون الحاجة إلى النسخ۔

تأكيد التحديث ثنائي الاتجاه

من خلال استخدام SharedArrayBuffer يمكن لكل من الخيط الرئيسي (main thread) والعاملين (workers) القراءة والكتابة إلى نفس الذاكرة، مما يتيح تأكيد التحديث ثنائي الاتجاه۔ فيما يلي مثال حيث يقوم الخيط الرئيسي بتعيين قيمة، ثم يقوم عامل بتغيير هذه القيمة، وبعد ذلك يتحقق الخيط الرئيسي من التحديث۔

في الخيط الرئيسي

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Set initial value
12// Output : Main thread initial: 100
13sharedArray[0] = 100;
14console.log("Main thread initial:", sharedArray[0]);
15
16// Listen for worker confirmation
17worker.onmessage = () => {
18    // Output : Main thread after worker update: 200
19    console.log("Main thread after worker update:", sharedArray[0]);
20};

على جانب Worker (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    // Read initial value
 6    // Output : Worker thread received: 100
 7    console.log("Worker thread received:", sharedArray[0]);
 8
 9    // Update the value
10    sharedArray[0] = 200;
11
12    // Notify main thread
13    self.postMessage("Value updated");
14};
  • في هذا المثال، يكتب الخيط الرئيسي القيمة 100 أولاً، وبعد أن يقرأها العامل، يكتبها مرة أخرى كـ 200۔ بعد ذلك، يقوم العامل بإخطار الخيط الرئيسي، ثم يقرأ الخيط الرئيسي الذاكرة المشتركة مرة أخرى لتأكيد التحديث۔ بهذه الطريقة، يتيح الجمع بين الإشعارات تأكيد التحديث ثنائي الاتجاه۔

المزامنة باستخدام Atomics

عند استخدام الذاكرة المشتركة، يجب توخي الحذر من ظروف السباق على البيانات وعدم الاتساق۔ عندما تقوم عدة خيوط برمجية بالوصول إلى نفس الذاكرة في وقت واحد، قد تحدث تعارضات۔ لتجنب ذلك، يستخدم JavaScript كائن Atomics للمزامنة۔

على سبيل المثال، لزيادة عداد بأمان باستخدام عدة خيوط برمجية، يمكن استخدام Atomics لتجنب التعارضات۔

1const sharedBuffer = new SharedArrayBuffer(16);
2const sharedArray = new Int32Array(sharedBuffer);
3
4// Increment the counter
5Atomics.add(sharedArray, 0, 1);
6
7console.log(Atomics.load(sharedArray, 0));  // 1

Atomics.add يقوم بزيادة القيمة في فهرس معين بشكل ذري ويعيد القيمة الجديدة۔ هذه العملية مضمونة لتكون خالية من التعارض مع الخيوط البرمجية الأخرى۔ يتم استخدام Atomics.load أيضًا لقراءة القيم بأمان من الذاكرة المشتركة۔

الانتظار والإخطار باستخدام Atomics.wait وAtomics.notify

عند استخدام SharedArrayBuffer هناك حالات يحتاج فيها عامل إلى الانتظار حتى يتحقق شرط معين، وعندما يفي عامل آخر بهذا الشرط، يجب عليه إخطار العامل المنتظر۔ في مثل هذه الحالات، تكون Atomics.wait وAtomics.notify مفيدة۔

Atomics.wait تقوم بحجب الخيط حتى تتغير القيمة عند مؤشر معين في الذاكرة المشتركة، بينما Atomics.notify تقوم بإخطار الخيوط المنتظرة بأنه يمكنها المتابعة۔ يتيح هذا الانتظار والإخطار بشكل آمن بين عدة عمال۔ ومع ذلك، لا يمكن استخدام Atomics.wait في الخيط الرئيسي وهو متوفر فقط داخل العمال (workers)۔

 1// Create a shared buffer (1 Int32 slot is enough for signaling)
 2const sharedBuffer = new SharedArrayBuffer(4);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create workers with names
 6const waiter = new Worker('worker.js', { name: 'waiter' });
 7const notifier = new Worker('worker.js', { name: 'notifier' });
 8
 9// Pass the shared buffer to both
10waiter.postMessage(sharedBuffer);
11notifier.postMessage(sharedBuffer);
12
13// Listen for messages
14waiter.onmessage = (event) => {
15    console.log(`[Main] Message from waiter:`, event.data);
16};
17notifier.onmessage = (event) => {
18    console.log(`[Main] Message from notifier:`, event.data);
19};
  • في الخيط الرئيسي، يتم إنشاء SharedArrayBuffer كذاكرة مشتركة ويتم تحويلها إلى Int32Array يحتوي على عنصر واحد فقط۔ يُستخدم هذا الموقع الصحيح المفرد كإشارة للمزامنة بين العمال۔ بعد ذلك، يتم إنشاء عاملين ويتم تعيين دور لكل منهما باستخدام خاصية name: الأول هو waiter (دور الانتظار)، والثاني هو notifier (دور الإشعار)۔ وأخيرًا، يتم تمرير المخزن المؤقت المشترك إلى كلا العاملين ويتم إعداد معالجات onmessage حتى يمكن استقبال الرسائل المرسلة من كل عامل۔

على جانب Worker (worker.js)

 1// worker.js
 2onmessage = (event) => {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    if (self.name === 'waiter') {
 6        postMessage('Waiter is waiting...');
 7        // Wait until notifier signals index 0
 8        Atomics.wait(sharedArray, 0, 0);
 9        postMessage('Waiter was notified!');
10    }
11
12    if (self.name === 'notifier') {
13        postMessage('Notifier is preparing...');
14        setTimeout(() => {
15            // Notify waiter after 2 seconds
16            Atomics.store(sharedArray, 0, 1);
17            Atomics.notify(sharedArray, 0, 1);
18            postMessage('Notifier has sent the signal!');
19        }, 2000);
20    }
21};
22// Output
23// [Main] Message from waiter: Waiter is waiting...
24// [Main] Message from notifier: Notifier is preparing...
25// [Main] Message from notifier: Notifier has sent the signal!
26// [Main] Message from waiter: Waiter was notified!
  • في هذا المثال، يبقى عامل waiter في حالة انتظار باستخدام Atomics.wait طالما أن القيمة عند الفهرس 0 هي 0۔ من ناحية أخرى، عندما يقوم عامل notifier بتغيير القيمة إلى 123 باستخدام Atomics.store وينادي Atomics.notify، سيستأنف عامل waiter العمل ويتمكن من الحصول على القيمة المحدثة۔ وبذلك يمكن تحقيق انتظار وإشعار فعال وآمن بين الخيوط۔

سيناريوهات استخدام SharedArrayBuffer

SharedArrayBuffer مفيد بشكل خاص في الحالات التالية:۔

  • المعالجة في الوقت الفعلي إنه مناسب للتطبيقات التي تتطلب انخفاض التأخير (زمن الاستجابة)، مثل معالجة الصوت والفيديو أو محركات الألعاب، حيث يجب مشاركة البيانات فورياً بين الخيوط۔
  • الحوسبة المتوازية عند معالجة كميات كبيرة من البيانات في الوقت نفسه باستخدام عدة خيوط، يمنع استخدام SharedArrayBuffer نسخ الذاكرة ويمكن أن يحسّن الأداء۔
  • التعلم الآلي من خلال موازاة المهام مثل المعالجة المسبقة للبيانات والاستدلال، يصبح الحوسبة الفعالة ممكنة۔

اعتبارات الأمان

SharedArrayBuffer ميزة قوية، لكنها تحمل أيضًا مخاطر أمنية۔ على وجه الخصوص، فإن المخاوف بشأن هجمات القنوات الجانبية مثل Spectre قد أوقفت دعمها مؤقتًا۔ لتقليل هذه الثغرة الأمنية، قامت المتصفحات بتنفيذ التدابير التالية:۔

  • عزل الموقع المواقع التي تسمح باستخدام SharedArrayBuffer ستعمل في عملية (process) معزولة تمامًا عن المواقع الأخرى۔
  • سياسة موارد المصدر المشترك (Cross-Origin Resource Policy) لاستخدام SharedArrayBuffer يجب ضبط رؤوس Cross-Origin-Opener-Policy و Cross-Origin-Embedder-Policy بشكل صحيح۔

على سبيل المثال، من خلال إعداد رؤوس مثل التالية، يصبح استخدام SharedArrayBuffer ممكنًا:۔

1Cross-Origin-Opener-Policy: same-origin
2Cross-Origin-Embedder-Policy: require-corp

هذا يمنع الموارد الخارجية من التدخل في المحتوى الحالي ويزيد من الأمان۔

الملخص

SharedArrayBuffer هي أداة قوية جدًا لمشاركة الذاكرة بين عدة خيوط برمجية۔ إنها تقنية أساسية لتحسين الأداء، وتظهر تأثيراتها بشكل خاص في مجالي المعالجة في الوقت الحقيقي والحوسبة المتوازية۔ ومع ذلك، فإنه ينطوي أيضًا على مخاطر أمنية، لذا فإن التهيئة الصحيحة والمزامنة مهمة۔

من خلال الاستفادة من SharedArrayBuffer يمكنك بناء تطبيقات ويب أكثر تطورًا وذات أداء أعلى۔

يمكنك متابعة المقالة أعلاه باستخدام Visual Studio Code على قناتنا على YouTube.۔ يرجى التحقق من القناة على YouTube أيضًا.۔

YouTube Video