বাংলা

জাভাস্ক্রিপ্টে সত্যিকারের মাল্টিথ্রেডিং আনলক করুন। এই বিস্তারিত গাইডটিতে SharedArrayBuffer, Atomics, Web Workers এবং উচ্চ-পারফরম্যান্স ওয়েব অ্যাপ্লিকেশনের জন্য নিরাপত্তা প্রয়োজনীয়তাগুলি আলোচনা করা হয়েছে।

জাভাস্ক্রিপ্ট SharedArrayBuffer: ওয়েবে কনকারেন্ট প্রোগ্রামিং-এর গভীরে

কয়েক দশক ধরে, জাভাস্ক্রিপ্টের সিঙ্গল-থ্রেডেড প্রকৃতি তার সরলতার উৎস এবং একই সাথে একটি উল্লেখযোগ্য পারফরম্যান্সের প্রতিবন্ধকতা ছিল। ইভেন্ট লুপ মডেলটি বেশিরভাগ UI-ভিত্তিক কাজের জন্য চমৎকারভাবে কাজ করে, কিন্তু যখন এটি কম্পিউটেশনালি ইন্টেনসিভ অপারেশনের মুখোমুখি হয়, তখন এটি সমস্যার সৃষ্টি করে। দীর্ঘ সময় ধরে চলা গণনা ব্রাউজারকে ফ্রিজ করে দিতে পারে, যা ব্যবহারকারীদের জন্য একটি হতাশাজনক অভিজ্ঞতা তৈরি করে। যদিও ওয়েব ওয়ার্কার্স (Web Workers) স্ক্রিপ্টগুলিকে ব্যাকগ্রাউন্ডে চালানোর অনুমতি দিয়ে একটি আংশিক সমাধান দিয়েছিল, কিন্তু তাদের নিজস্ব একটি বড় সীমাবদ্ধতা ছিল: অদক্ষ ডেটা কমিউনিকেশন।

এবার আসা যাক SharedArrayBuffer (SAB)-এর কথায়, এটি একটি শক্তিশালী ফিচার যা ওয়েবে থ্রেডগুলির মধ্যে সত্যিকারের, নিম্ন-স্তরের মেমরি শেয়ারিং চালু করে খেলার নিয়মকে মৌলিকভাবে পরিবর্তন করে দিয়েছে। Atomics অবজেক্টের সাথে যুক্ত হয়ে, SAB সরাসরি ব্রাউজারে উচ্চ-পারফরম্যান্স, কনকারেন্ট অ্যাপ্লিকেশনগুলির একটি নতুন যুগের সূচনা করে। তবে, বড় শক্তির সাথে বড় দায়িত্ব এবং জটিলতাও আসে।

এই গাইডটি আপনাকে জাভাস্ক্রিপ্টে কনকারেন্ট প্রোগ্রামিংয়ের জগতে নিয়ে যাবে। আমরা অন্বেষণ করব কেন আমাদের এটি প্রয়োজন, কীভাবে SharedArrayBuffer এবং Atomics কাজ করে, আপনাকে অবশ্যই যে গুরুতর নিরাপত্তা বিবেচনাগুলি মোকাবেলা করতে হবে এবং আপনাকে শুরু করার জন্য ব্যবহারিক উদাহরণ দেখাবো।

পুরানো বিশ্ব: জাভাস্ক্রিপ্টের সিঙ্গল-থ্রেডেড মডেল এবং এর সীমাবদ্ধতা

সমাধানের গুরুত্ব বোঝার আগে, আমাদের অবশ্যই সমস্যাটি পুরোপুরি বুঝতে হবে। ব্রাউজারে জাভাস্ক্রিপ্ট এক্সিকিউশন ঐতিহ্যগতভাবে একটি সিঙ্গল থ্রেডে ঘটে, যাকে প্রায়শই "মেইন থ্রেড" বা "UI থ্রেড" বলা হয়।

ইভেন্ট লুপ

মেইন থ্রেড সবকিছুর জন্য দায়ী: আপনার জাভাস্ক্রিপ্ট কোড এক্সিকিউট করা, পেজ রেন্ডার করা, ব্যবহারকারীর ইন্টারঅ্যাকশনে (যেমন ক্লিক এবং স্ক্রল) সাড়া দেওয়া এবং CSS অ্যানিমেশন চালানো। এটি একটি ইভেন্ট লুপ ব্যবহার করে এই কাজগুলি পরিচালনা করে, যা ক্রমাগত বার্তাগুলির (টাস্ক) একটি কিউ (queue) প্রসেস করে। যদি একটি টাস্ক সম্পূর্ণ হতে অনেক সময় নেয়, তবে এটি পুরো কিউ ব্লক করে দেয়। অন্য কিছুই ঘটতে পারে না—UI ফ্রিজ হয়ে যায়, অ্যানিমেশন থেমে যায় এবং পেজটি প্রতিক্রিয়াহীন হয়ে পড়ে।

ওয়েব ওয়ার্কার্স: সঠিক দিকে একটি পদক্ষেপ

এই সমস্যাটি প্রশমিত করার জন্য ওয়েব ওয়ার্কার্স চালু করা হয়েছিল। একটি ওয়েব ওয়ার্কার মূলত একটি পৃথক ব্যাকগ্রাউন্ড থ্রেডে চলমান একটি স্ক্রিপ্ট। আপনি ভারী গণনাগুলি একটি ওয়ার্কারে অফলোড করতে পারেন, যা মেইন থ্রেডকে ইউজার ইন্টারফেস পরিচালনা করার জন্য মুক্ত রাখে।

মেইন থ্রেড এবং একটি ওয়ার্কারের মধ্যে যোগাযোগ postMessage() API-এর মাধ্যমে ঘটে। যখন আপনি ডেটা পাঠান, তখন এটি স্ট্রাকচার্ড ক্লোন অ্যালগরিদম (structured clone algorithm) দ্বারা পরিচালিত হয়। এর মানে হল ডেটাটি সিরিয়ালাইজড, কপি এবং তারপর ওয়ার্কারের কনটেক্সটে ডিসিরিয়ালাইজড হয়। যদিও এটি কার্যকর, তবে বড় ডেটাসেটের জন্য এই প্রক্রিয়ার উল্লেখযোগ্য অসুবিধা রয়েছে:

ব্রাউজারে একটি ভিডিও এডিটরের কথা ভাবুন। সেকেন্ডে ৬০ বার প্রসেস করার জন্য একটি সম্পূর্ণ ভিডিও ফ্রেম (যা কয়েক মেগাবাইট হতে পারে) একটি ওয়ার্কারে পাঠানো এবং ফেরত আনা অনেক ব্যয়বহুল হবে। এই নির্দিষ্ট সমস্যা সমাধানের জন্যই SharedArrayBuffer ডিজাইন করা হয়েছিল।

গেম-চেঞ্জার: SharedArrayBuffer-এর পরিচিতি

একটি SharedArrayBuffer হলো একটি নির্দিষ্ট দৈর্ঘ্যের কাঁচা বাইনারি ডেটা বাফার, যা একটি ArrayBuffer-এর মতো। মূল পার্থক্য হল একটি SharedArrayBuffer একাধিক থ্রেডে (যেমন, মেইন থ্রেড এবং এক বা একাধিক ওয়েব ওয়ার্কার) শেয়ার করা যেতে পারে। যখন আপনি postMessage() ব্যবহার করে একটি SharedArrayBuffer "পাঠান", আপনি একটি কপি পাঠাচ্ছেন না; আপনি একই মেমরি ব্লকের একটি রেফারেন্স পাঠাচ্ছেন।

এর মানে হল একটি থ্রেড দ্বারা বাফারের ডেটাতে করা যেকোনো পরিবর্তন অন্য সব থ্রেডের কাছে অবিলম্বে দৃশ্যমান হয় যাদের কাছে এটির রেফারেন্স আছে। এটি ব্যয়বহুল কপি-এবং-সিরিয়ালাইজ পদক্ষেপটি দূর করে, প্রায়-তাত্ক্ষণিক ডেটা শেয়ারিং সক্ষম করে।

এটিকে এভাবে ভাবুন:

শেয়ার্ড মেমরির বিপদ: রেস কন্ডিশন (Race Conditions)

তাত্ক্ষণিক মেমরি শেয়ারিং শক্তিশালী, কিন্তু এটি কনকারেন্ট প্রোগ্রামিংয়ের জগৎ থেকে একটি ক্লাসিক সমস্যাও নিয়ে আসে: রেস কন্ডিশন

একটি রেস কন্ডিশন ঘটে যখন একাধিক থ্রেড একই সময়ে একই শেয়ার্ড ডেটা অ্যাক্সেস এবং পরিবর্তন করার চেষ্টা করে, এবং চূড়ান্ত ফলাফল তাদের এক্সিকিউশনের অনির্দেশ্য ক্রমের উপর নির্ভর করে। একটি SharedArrayBuffer-এ সংরক্ষিত একটি সাধারণ কাউন্টারের কথা ভাবুন। মেইন থ্রেড এবং একটি ওয়ার্কার উভয়ই এটি বাড়াতে চায়।

  1. থ্রেড A বর্তমান মান পড়ে, যা হল 5।
  2. থ্রেড A নতুন মান লেখার আগেই, অপারেটিং সিস্টেম এটিকে পজ করে এবং থ্রেড B-তে সুইচ করে।
  3. থ্রেড B বর্তমান মান পড়ে, যা এখনও 5।
  4. থ্রেড B নতুন মান (6) গণনা করে এবং এটি মেমরিতে লিখে দেয়।
  5. সিস্টেম আবার থ্রেড A-তে ফিরে আসে। এটি জানে না যে থ্রেড B কিছু করেছে। এটি যেখান থেকে ছেড়েছিল সেখান থেকে পুনরায় শুরু করে, তার নতুন মান (5 + 1 = 6) গণনা করে এবং 6 মেমরিতে লিখে দেয়।

যদিও কাউন্টারটি দুবার বাড়ানো হয়েছিল, চূড়ান্ত মান 6, 7 নয়। অপারেশনগুলি অ্যাটমিক (atomic) ছিল না—এগুলি বাধাগ্রস্ত হতে পারতো, যার ফলে ডেটা হারিয়ে গেছে। ঠিক এই কারণেই আপনি এর গুরুত্বপূর্ণ সঙ্গী: Atomics অবজেক্ট ছাড়া একটি SharedArrayBuffer ব্যবহার করতে পারবেন না।

শেয়ার্ড মেমরির অভিভাবক: Atomics অবজেক্ট

Atomics অবজেক্ট SharedArrayBuffer অবজেক্টগুলিতে অ্যাটমিক অপারেশন সম্পাদন করার জন্য স্ট্যাটিক পদ্ধতির একটি সেট সরবরাহ করে। একটি অ্যাটমিক অপারেশন অন্য কোনো অপারেশন দ্বারা বাধাগ্রস্ত না হয়ে সম্পূর্ণরূপে সম্পাদিত হওয়ার গ্যারান্টি দেয়। এটি হয় সম্পূর্ণরূপে ঘটে অথবা একেবারেই ঘটে না।

Atomics ব্যবহার করে শেয়ার্ড মেমরিতে রিড-মডিফাই-রাইট অপারেশনগুলি নিরাপদে সম্পাদিত হয় তা নিশ্চিত করে রেস কন্ডিশন প্রতিরোধ করা যায়।

গুরুত্বপূর্ণ Atomics মেথড

আসুন Atomics দ্বারা প্রদত্ত কিছু সবচেয়ে গুরুত্বপূর্ণ মেথড দেখে নেওয়া যাক।

সিঙ্ক্রোনাইজেশন: সাধারণ অপারেশনের বাইরে

কখনও কখনও আপনার কেবল নিরাপদ পড়া এবং লেখার চেয়ে বেশি প্রয়োজন। আপনার থ্রেডগুলিকে সমন্বয় করতে এবং একে অপরের জন্য অপেক্ষা করতে হবে। একটি সাধারণ অ্যান্টি-প্যাটার্ন হল "ব্যস্ত-অপেক্ষা" (busy-waiting), যেখানে একটি থ্রেড একটি টাইট লুপে বসে থাকে, ক্রমাগত একটি মেমরি লোকেশন পরিবর্তনের জন্য পরীক্ষা করে। এটি সিপিইউ সাইকেল নষ্ট করে এবং ব্যাটারির আয়ু কমিয়ে দেয়।

Atomics wait() এবং notify() এর মাধ্যমে একটি অনেক বেশি কার্যকর সমাধান প্রদান করে।

সবকিছু একসাথে: একটি ব্যবহারিক গাইড

এখন যেহেতু আমরা তত্ত্বটি বুঝতে পেরেছি, আসুন SharedArrayBuffer ব্যবহার করে একটি সমাধান বাস্তবায়নের ধাপগুলির মধ্য দিয়ে যাই।

ধাপ ১: নিরাপত্তা পূর্বশর্ত - ক্রস-অরিজিন আইসোলেশন

এটি ডেভেলপারদের জন্য সবচেয়ে সাধারণ বাধা। নিরাপত্তার কারণে, SharedArrayBuffer শুধুমাত্র সেই পেজগুলিতে উপলব্ধ যা একটি ক্রস-অরিজিন আইসোলেটেড (cross-origin isolated) অবস্থায় আছে। এটি স্পেকটারের মতো স্পেকুলেটিভ এক্সিকিউশন দুর্বলতাগুলি প্রশমিত করার জন্য একটি নিরাপত্তা ব্যবস্থা, যা সম্ভাব্যভাবে শেয়ার্ড মেমরির মাধ্যমে সম্ভব হওয়া উচ্চ-রেজোলিউশন টাইমার ব্যবহার করে অরিজিন জুড়ে ডেটা ফাঁস করতে পারে।

ক্রস-অরিজিন আইসোলেশন সক্ষম করতে, আপনাকে আপনার প্রধান ডকুমেন্টের জন্য দুটি নির্দিষ্ট HTTP হেডার পাঠানোর জন্য আপনার ওয়েব সার্ভার কনফিগার করতে হবে:


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

এটি সেট আপ করা চ্যালেঞ্জিং হতে পারে, বিশেষ করে যদি আপনি তৃতীয় পক্ষের স্ক্রিপ্ট বা রিসোর্সের উপর নির্ভর করেন যা প্রয়োজনীয় হেডার সরবরাহ করে না। আপনার সার্ভার কনফিগার করার পরে, আপনি ব্রাউজারের কনসোলে self.crossOriginIsolated প্রপার্টি পরীক্ষা করে আপনার পেজটি আইসোলেটেড কিনা তা যাচাই করতে পারেন। এটি অবশ্যই true হতে হবে।

ধাপ ২: বাফার তৈরি এবং শেয়ার করা

আপনার প্রধান স্ক্রিপ্টে, আপনি SharedArrayBuffer তৈরি করেন এবং Int32Array এর মতো একটি TypedArray ব্যবহার করে এটির উপর একটি "ভিউ" তৈরি করেন।

main.js:


// Check for cross-origin isolation first!
if (!self.crossOriginIsolated) {
  console.error("This page is not cross-origin isolated. SharedArrayBuffer will not be available.");
} else {
  // Create a shared buffer for one 32-bit integer.
  const buffer = new SharedArrayBuffer(4);

  // Create a view on the buffer. All atomic operations happen on the view.
  const int32Array = new Int32Array(buffer);

  // Initialize the value at index 0.
  int32Array[0] = 0;

  // Create a new worker.
  const worker = new Worker('worker.js');

  // Send the SHARED buffer to the worker. This is a reference transfer, not a copy.
  worker.postMessage({ buffer });

  // Listen for messages from the worker.
  worker.onmessage = (event) => {
    console.log(`Worker reported completion. Final value: ${Atomics.load(int32Array, 0)}`);
  };
}

ধাপ ৩: ওয়ার্কারে অ্যাটমিক অপারেশন সম্পাদন করা

ওয়ার্কার বাফারটি গ্রহণ করে এবং এখন এটির উপর অ্যাটমিক অপারেশন সম্পাদন করতে পারে।

worker.js:


self.onmessage = (event) => {
  const { buffer } = event.data;
  const int32Array = new Int32Array(buffer);

  console.log("Worker received the shared buffer.");

  // Let's perform some atomic operations.
  for (let i = 0; i < 1000000; i++) {
    // Safely increment the shared value.
    Atomics.add(int32Array, 0, 1);
  }

  console.log("Worker finished incrementing.");

  // Signal back to the main thread that we are done.
  self.postMessage({ done: true });
};

ধাপ ৪: একটি আরও উন্নত উদাহরণ - সিঙ্ক্রোনাইজেশন সহ প্যারালাল সামেশন

আসুন একটি আরও বাস্তবসম্মত সমস্যা সমাধান করি: একাধিক ওয়ার্কার ব্যবহার করে একটি খুব বড় সংখ্যার অ্যারের যোগফল বের করা। আমরা কার্যকর সিঙ্ক্রোনাইজেশনের জন্য Atomics.wait() এবং Atomics.notify() ব্যবহার করব।

আমাদের শেয়ার্ড বাফারের তিনটি অংশ থাকবে:

main.js:


if (self.crossOriginIsolated) {
  const NUM_WORKERS = 4;
  const DATA_SIZE = 10_000_000;

  // [status, workers_finished, result_low, result_high]
  // We use two 32-bit integers for the result to avoid overflow for large sums.
  const sharedBuffer = new SharedArrayBuffer(4 * 4); // 4 integers
  const sharedArray = new Int32Array(sharedBuffer);

  // Generate some random data to process
  const data = new Uint8Array(DATA_SIZE);
  for (let i = 0; i < DATA_SIZE; i++) {
    data[i] = Math.floor(Math.random() * 10);
  }

  const chunkSize = Math.ceil(DATA_SIZE / NUM_WORKERS);

  for (let i = 0; i < NUM_WORKERS; i++) {
    const worker = new Worker('sum_worker.js');
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, DATA_SIZE);
    
    // Create a non-shared view for the worker's chunk of data
    const dataChunk = data.subarray(start, end);

    worker.postMessage({ 
      sharedBuffer,
      dataChunk // This is copied
    });
  }

  console.log('Main thread is now waiting for workers to finish...');

  // Wait for the status flag at index 0 to become 1
  // This is much better than a while loop!
  Atomics.wait(sharedArray, 0, 0); // Wait if sharedArray[0] is 0

  console.log('Main thread woken up!');
  const finalSum = Atomics.load(sharedArray, 2);
  console.log(`The final parallel sum is: ${finalSum}`);

} else {
  console.error('Page is not cross-origin isolated.');
}

sum_worker.js:


self.onmessage = ({ data }) => {
  const { sharedBuffer, dataChunk } = data;
  const sharedArray = new Int32Array(sharedBuffer);

  // Calculate the sum for this worker's chunk
  let localSum = 0;
  for (let i = 0; i < dataChunk.length; i++) {
    localSum += dataChunk[i];
  }

  // Atomically add the local sum to the shared total
  Atomics.add(sharedArray, 2, localSum);

  // Atomically increment the 'workers finished' counter
  const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;

  // If this is the last worker to finish...
  const NUM_WORKERS = 4; // Should be passed in a real app
  if (finishedCount === NUM_WORKERS) {
    console.log('Last worker finished. Notifying main thread.');

    // 1. Set the status flag to 1 (complete)
    Atomics.store(sharedArray, 0, 1);

    // 2. Notify the main thread, which is waiting on index 0
    Atomics.notify(sharedArray, 0, 1);
  }
};

বাস্তব-জগতের ব্যবহার এবং অ্যাপ্লিকেশন

এই শক্তিশালী কিন্তু জটিল প্রযুক্তি আসলে কোথায় পার্থক্য তৈরি করে? এটি এমন অ্যাপ্লিকেশনগুলিতে সেরা যা বড় ডেটাসেটে ভারী, সমান্তরালযোগ্য গণনা প্রয়োজন।

চ্যালেঞ্জ এবং চূড়ান্ত বিবেচনা

যদিও SharedArrayBuffer রূপান্তরকারী, এটি কোনো ম্যাজিক সমাধান নয়। এটি একটি নিম্ন-স্তরের সরঞ্জাম যা সতর্কতার সাথে পরিচালনা করা প্রয়োজন।

  1. জটিলতা: কনকারেন্ট প্রোগ্রামিং কুখ্যাতভাবে কঠিন। রেস কন্ডিশন এবং ডেডলক ডিবাগ করা অবিশ্বাস্যভাবে চ্যালেঞ্জিং হতে পারে। আপনার অ্যাপ্লিকেশন স্টেট কীভাবে পরিচালিত হয় সে সম্পর্কে আপনাকে ভিন্নভাবে চিন্তা করতে হবে।
  2. ডেডলক: একটি ডেডলক ঘটে যখন দুই বা ততোধিক থ্রেড চিরতরে ব্লক হয়ে যায়, প্রত্যেকে অন্যের একটি রিসোর্স ছেড়ে দেওয়ার জন্য অপেক্ষা করে। আপনি যদি জটিল লকিং মেকানিজম ভুলভাবে প্রয়োগ করেন তবে এটি ঘটতে পারে।
  3. নিরাপত্তা ওভারহেড: ক্রস-অরিজিন আইসোলেশন প্রয়োজনীয়তা একটি উল্লেখযোগ্য বাধা। এটি তৃতীয় পক্ষের পরিষেবা, বিজ্ঞাপন এবং পেমেন্ট গেটওয়েগুলির সাথে ইন্টিগ্রেশন ভেঙে দিতে পারে যদি তারা প্রয়োজনীয় CORS/CORP হেডার সমর্থন না করে।
  4. প্রতিটি সমস্যার জন্য নয়: সাধারণ ব্যাকগ্রাউন্ড টাস্ক বা I/O অপারেশনের জন্য, postMessage() সহ ঐতিহ্যবাহী ওয়েব ওয়ার্কার মডেল প্রায়শই সহজ এবং যথেষ্ট। শুধুমাত্র যখন আপনার কাছে বড় পরিমাণে ডেটা জড়িত একটি স্পষ্ট, সিপিইউ-বাউন্ড বাধা থাকে তখনই SharedArrayBuffer ব্যবহার করুন।

উপসংহার

SharedArrayBuffer, Atomics এবং ওয়েব ওয়ার্কার্সের সাথে মিলিত হয়ে, ওয়েব ডেভেলপমেন্টের জন্য একটি প্যারাডাইম শিফট উপস্থাপন করে। এটি সিঙ্গল-থ্রেডেড মডেলের সীমানা ভেঙে দেয়, ব্রাউজারে একটি নতুন শ্রেণীর শক্তিশালী, পারফরম্যান্ট এবং জটিল অ্যাপ্লিকেশনকে আমন্ত্রণ জানায়। এটি ওয়েব প্ল্যাটফর্মকে কম্পিউটেশনালি ইন্টেনসিভ কাজের জন্য নেটিভ অ্যাপ্লিকেশন ডেভেলপমেন্টের সাথে আরও সমান অবস্থানে রাখে।

কনকারেন্ট জাভাস্ক্রিপ্টের যাত্রা চ্যালেঞ্জিং, যা স্টেট ম্যানেজমেন্ট, সিঙ্ক্রোনাইজেশন এবং নিরাপত্তার জন্য একটি কঠোর পদ্ধতির দাবি করে। কিন্তু যে ডেভেলপাররা ওয়েবে যা সম্ভব তার সীমা ঠেলে দিতে চান—রিয়েল-টাইম অডিও সিন্থেসিস থেকে জটিল 3D রেন্ডারিং এবং বৈজ্ঞানিক কম্পিউটিং পর্যন্ত—তাদের জন্য SharedArrayBuffer আয়ত্ত করা আর কেবল একটি বিকল্প নয়; এটি পরবর্তী প্রজন্মের ওয়েব অ্যাপ্লিকেশন তৈরির জন্য একটি অপরিহার্য দক্ষতা।