SharedArrayBuffer ใน JavaScript

SharedArrayBuffer ใน JavaScript

บทความนี้อธิบายเกี่ยวกับ SharedArrayBuffer ใน JavaScript

เราจะให้คำอธิบายในเชิงลึกเกี่ยวกับพื้นฐานของ SharedArrayBuffer, วิธีการใช้งาน, กรณีการใช้งานเฉพาะ และข้อพิจารณาด้านความปลอดภัย

YouTube Video

SharedArrayBuffer ใน JavaScript

SharedArrayBuffer เป็นเครื่องมือที่ทรงพลังใน JavaScript สำหรับการแชร์หน่วยความจำระหว่างหลายเธรด โดยเฉพาะเมื่อใช้ร่วมกับ Web Workers มันช่วยให้เกิดการประมวลผลแบบขนาน ทำให้มีประสิทธิภาพสำหรับงานที่ใช้การคำนวณหนักและแอปพลิเคชันที่ต้องการความสามารถแบบเรียลไทม์

SharedArrayBuffer คืออะไร?

SharedArrayBuffer ให้บริการหน่วยความจำบัฟเฟอร์ใน JavaScript ที่ช่วยให้สามารถแชร์ข้อมูลไบนารีระหว่างหลายเธรด (ส่วนใหญ่สำหรับ Web Workers) โดยปกติ ArrayBuffer จำเป็นต้องคัดลอกข้อมูลระหว่างเธรดหลักและ workers แต่ SharedArrayBuffer ช่วยให้การแชร์หน่วยความจำเป็นไปโดยตรงโดยไม่ต้องคัดลอก ส่งผลให้ประสิทธิภาพดีขึ้นมาก

คุณสมบัติ

  • หน่วยความจำที่ใช้ร่วมกัน ช่วยให้หลายเธรดสามารถทำงานกับพื้นที่หน่วยความจำเดียวกันได้
  • การปรับปรุงประสิทธิภาพ เนื่องจากไม่ต้องก๊อปปี้ข้อมูล จึงช่วยลดค่าใช้จ่ายส่วนเกินเมื่อประมวลผลข้อมูลจำนวนมาก
  • การซิงโครไนซ์ของเธรด สามารถใช้ร่วมกับ 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 ทั้งเธรดหลักและ worker สามารถอ่านและเขียนข้อมูลในหน่วยความจำเดียวกัน ทำให้สามารถตรวจสอบการอัปเดตแบบสองทิศทางได้ ด้านล่างนี้เป็นตัวอย่างที่เธรดหลักตั้งค่าค่า ตัว 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// 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 ก่อน แล้ว worker จะอ่านค่าและเปลี่ยนเป็น 200 หลังจากนั้น worker จะแจ้งให้เธรดหลักทราบ และเธรดหลักจะอ่านหน่วยความจำที่ใช้ร่วมกันอีกครั้งเพื่อยืนยันการอัปเดต ด้วยวิธีนี้ การรวมการแจ้งเตือนทำให้สามารถตรวจสอบการอัปเดตแบบสองทิศทางได้

การซิงโครไนซ์ด้วย 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 อาจมีกรณีที่ worker ต้องรอจนกว่าจะมีเงื่อนไขบางอย่างเกิดขึ้น และเมื่อ worker ตัวอื่นทำเงื่อนไขนั้นสำเร็จ จะต้องแจ้งเตือน worker ที่รออยู่ ในกรณีแบบนี้ Atomics.wait และ Atomics.notify จะมีประโยชน์

Atomics.wait จะบล็อกเธรดไว้จนกว่าค่าที่ตำแหน่งหนึ่งในหน่วยความจำที่ใช้ร่วมกันจะเปลี่ยนแปลง ส่วน Atomics.notify จะเป็นตัวแจ้งให้เธรดที่กำลังรออยู่ดำเนินการต่อไป วิธีนี้ช่วยให้สามารถรอและแจ้งเตือนกันระหว่าง worker หลายตัวได้อย่างปลอดภัย อย่างไรก็ตาม Atomics.wait ไม่สามารถใช้ได้บนเธรดหลักและ สามารถใช้ได้เฉพาะใน worker เท่านั้น

 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 ที่มีเพียงหนึ่งสมาชิก ช่องข้อมูลจำนวนเต็มช่องเดียวนี้ถูกใช้เป็นสัญญาณเพื่อให้การทำงานประสานกันระหว่าง worker จากนั้นจะสร้าง worker สองตัว และกำหนดบทบาทให้แต่ละตัวโดยใช้คุณสมบัติ name ได้แก่ waiter (ผู้รอ) และ notifier (ผู้แจ้งเตือน) สุดท้าย buffer ที่ใช้งานร่วมกันจะถูกส่งไปยัง worker ทั้งสอง และมีการตั้งค่าตัวจัดการ onmessage เพื่อรับข้อความที่ส่งจากแต่ละ worker

ทางฝั่งของ 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!
  • ในตัวอย่างนี้ worker waiter จะอยู่ในสถานะรอโดยใช้ Atomics.wait ตราบเท่าที่ค่าที่ตำแหน่งที่อยู่ 0 เป็น 0 ในทางกลับกัน เมื่อ worker notifier เปลี่ยนค่าเป็น 123 ด้วย Atomics.store และเรียกใช้ Atomics.notify worker waiter จะกลับมาทำงานต่อและสามารถรับค่าที่ถูกอัปเดตได้ ด้วยวิธีนี้ สามารถรอและแจ้งเตือนระหว่างเธรดอย่างมีประสิทธิภาพและปลอดภัย

กรณีการใช้งานของ SharedArrayBuffer

SharedArrayBuffer มีประโยชน์อย่างยิ่งสำหรับกรณีการใช้งานต่อไปนี้:

  • การประมวลผลแบบเรียลไทม์ เหมาะสำหรับแอปพลิเคชันที่ต้องการความหน่วงต่ำ เช่น การประมวลผลเสียง วิดีโอ หรือเอนจิ้นเกม ซึ่งต้องใช้การแชร์ข้อมูลทันทีระหว่างเธรด
  • การประมวลผลแบบขนาน เมื่อประมวลผลข้อมูลจำนวนมากด้วยหลายเธรดพร้อมกัน การใช้ SharedArrayBuffer จะช่วยหลีกเลี่ยงการก๊อปปี้หน่วยความจำและเพิ่มประสิทธิภาพได้
  • การเรียนรู้ของเครื่อง ด้วยการประมวลผลงาน เช่น การเตรียมข้อมูลและการอนุมานแบบขนาน ทำให้สามารถคำนวณได้อย่างมีประสิทธิภาพ

ข้อพิจารณาด้านความปลอดภัย

SharedArrayBuffer เป็นคุณสมบัติที่ทรงพลัง แต่ก็มีความเสี่ยงด้านความปลอดภัยเช่นกัน โดยเฉพาะอย่างยิ่ง ความกังวลเกี่ยวกับการโจมตีผ่าน channel ด้านข้าง เช่น Spectre ได้หยุดสนับสนุนไว้ชั่วคราว เพื่อบรรเทาช่องโหว่นี้ เบราว์เซอร์ได้ดำเนินมาตรการต่อไปนี้:

  • การแยกไซต์ (Site Isolation) เว็บไซต์ที่อนุญาตให้ใช้ SharedArrayBuffer จะทำงานในโปรเซสที่แยกออกจากเว็บไซต์อื่นโดยสมบูรณ์
  • นโยบายทรัพยากรข้ามโดเมน เพื่อให้สามารถใช้ SharedArrayBuffer ได้ ต้องกำหนด header Cross-Origin-Opener-Policy และ Cross-Origin-Embedder-Policy ให้ถูกต้อง

ตัวอย่างเช่น โดยการตั้งค่า head ดังต่อไปนี้ การใช้ SharedArrayBuffer จะกลายเป็นไปได้:

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

สิ่งนี้ช่วยป้องกันไม่ให้ทรัพยากรภายนอกรบกวนเนื้อหาปัจจุบันและเพิ่มความปลอดภัย

สรุป

SharedArrayBuffer เป็นเครื่องมือที่ทรงพลังมากสำหรับการแชร์หน่วยความจำระหว่างเธรดหลายตัว เป็นเทคโนโลยีที่สำคัญสำหรับการปรับปรุงประสิทธิภาพ และผลลัพธ์มีความเด่นชัดเป็นพิเศษในด้านการประมวลผลแบบเรียลไทม์และการคำนวณแบบขนาน อย่างไรก็ตาม มันยังมีความเสี่ยงด้านความปลอดภัย ดังนั้นการตั้งค่าและการทำให้การทำงานซิงโครไนซ์อย่างถูกต้องจึงเป็นสิ่งสำคัญ

ด้วยการใช้ SharedArrayBuffer คุณสามารถสร้างเว็บแอปพลิเคชันที่ล้ำสมัยและมีประสิทธิภาพสูงกว่าเดิม

คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย

YouTube Video