İçeriğe geç
JavaScript

JavaScript Web Workers: Paralel İşlem Rehberi

Tarık Tunç
JavaScript Web Workers: Paralel İşlem Rehberi

JavaScript Web Workers: Paralel İşlem Rehberi

JavaScript tek iş parçacıklı (single-threaded) çalışır ve ağır hesaplamalar ana thread'i bloklar, kullanıcı arayüzünü donuklaştırır. Web Workers, arka plan thread'lerinde JavaScript çalıştırarak ana thread'i serbest bırakır. Bu rehberde Dedicated Workers, Shared Workers, mesaj iletişimi, veri transfer yöntemleri ve performans optimizasyonlarını inceleyeceğiz.

Web Worker Türleri

  • Dedicated Worker: Tek bir script tarafından kullanılır. En yaygın kullanılan türdür
  • Shared Worker: Birden fazla script/sekme tarafından paylaşılabilir
  • Service Worker: Ağ isteklerini yakalayan proxy worker (ayrı bir konu)

Dedicated Worker Oluşturma

// main.js (Ana thread)
const worker = new Worker('worker.js');

// Worker'a mesaj gönder
worker.postMessage({
  type: 'CALCULATE',
  data: { numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] },
});

// Worker'dan mesaj al
worker.onmessage = (event) => {
  console.log('Sonuç:', event.data);
};

// Hata yakalama
worker.onerror = (error) => {
  console.error('Worker hatası:', error.message);
};

// Worker'ı sonlandır
// worker.terminate();

// worker.js (Worker thread)
self.onmessage = (event) => {
  const { type, data } = event.data;

  switch (type) {
    case 'CALCULATE': {
      // Ağır hesaplama (ana thread'i bloklamaz)
      const result = data.numbers.reduce((sum, n) => {
        // Simüle edilmiş ağır işlem
        let val = n;
        for (let i = 0; i < 1000000; i++) {
          val = Math.sqrt(val * val + i);
        }
        return sum + val;
      }, 0);

      self.postMessage({ type: 'RESULT', result });
      break;
    }
  }
};

Inline Worker (Blob URL)

Ayrı dosya oluşturmadan worker tanımlamak için Blob URL kullanabilirsiniz:

function createInlineWorker(fn) {
  const blob = new Blob(
    [`self.onmessage = ${fn.toString()}`],
    { type: 'application/javascript' }
  );
  const url = URL.createObjectURL(blob);
  const worker = new Worker(url);

  // Cleanup
  worker.addEventListener('close', () => URL.revokeObjectURL(url));
  return worker;
}

const sortWorker = createInlineWorker((event) => {
  const { array } = event.data;
  const sorted = array.sort((a, b) => a - b);
  self.postMessage({ sorted });
});

sortWorker.postMessage({
  array: Array.from({ length: 1000000 }, () => Math.random()),
});

sortWorker.onmessage = (event) => {
  console.log('Sıralama tamamlandı:', event.data.sorted.length, 'eleman');
};

Transferable Objects

Büyük verileri kopyalamak yerine sahipliği transfer etmek çok daha performanslıdır:

// Kötü: Veri kopyalanır (yavaş, çift bellek kullanımı)
const largeArray = new Float64Array(1000000);
worker.postMessage({ data: largeArray });
// largeArray hala ana thread'de kullanılabilir

// İyi: Veri transfer edilir (hızlı, sıfır kopya)
const buffer = new Float64Array(1000000);
worker.postMessage({ data: buffer }, [buffer.buffer]);
// buffer artık ana thread'de kullanılamaz (boş)
console.log(buffer.byteLength); // 0

// Worker tarafında
self.onmessage = (event) => {
  const data = event.data.data; // Float64Array, transfer edilmiş

  // İşlem yap
  for (let i = 0; i < data.length; i++) {
    data[i] = Math.sqrt(data[i]);
  }

  // Sonucu geri transfer et
  self.postMessage({ result: data }, [data.buffer]);
};
Bilgi: Transferable Objects, veriyi kopyalamak yerine sahipliğini aktarır. Bu işlem O(1) sürede gerçekleşir ve büyük ArrayBuffer'lar için dramatik performans artışı sağlar.

SharedArrayBuffer ile Paylaşımlı Bellek

// Ana thread
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);

// Worker'a paylaşımlı bellek gönder
worker.postMessage({ buffer: sharedBuffer });

// Atomics ile thread-safe okuma/yazma
Atomics.store(sharedArray, 0, 42);
console.log(Atomics.load(sharedArray, 0)); // 42

// Worker tarafında
self.onmessage = (event) => {
  const sharedArray = new Int32Array(event.data.buffer);

  // Atomics ile güvenli erişim
  Atomics.add(sharedArray, 0, 10);
  const value = Atomics.load(sharedArray, 0); // 52

  // Bekleme ve bildirim
  Atomics.wait(sharedArray, 1, 0); // index 1 = 0 olduğu sürece bekle
  // Ana thread'den Atomics.notify(sharedArray, 1) çağrılınca devam eder
};
Uyarı: SharedArrayBuffer kullanmak için sunucu Cross-Origin-Opener-Policy: same-origin ve Cross-Origin-Embedder-Policy: require-corp başlıklarını göndermelidir.

Worker Pool Pattern

// WorkerPool.js
class WorkerPool {
  constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
    this.workers = [];
    this.queue = [];
    this.activeWorkers = new Set();

    for (let i = 0; i < poolSize; i++) {
      const worker = new Worker(workerScript);
      this.workers.push(worker);
    }
  }

  exec(data) {
    return new Promise((resolve, reject) => {
      const task = { data, resolve, reject };

      const availableWorker = this.workers.find(
        (w) => !this.activeWorkers.has(w)
      );

      if (availableWorker) {
        this._runTask(availableWorker, task);
      } else {
        this.queue.push(task);
      }
    });
  }

  _runTask(worker, task) {
    this.activeWorkers.add(worker);

    worker.onmessage = (event) => {
      task.resolve(event.data);
      this.activeWorkers.delete(worker);

      // Kuyrukta bekleyen iş varsa çalıştır
      if (this.queue.length > 0) {
        const nextTask = this.queue.shift();
        this._runTask(worker, nextTask);
      }
    };

    worker.onerror = (error) => {
      task.reject(error);
      this.activeWorkers.delete(worker);
    };

    worker.postMessage(task.data);
  }

  terminate() {
    this.workers.forEach((w) => w.terminate());
  }
}

// Kullanım
const pool = new WorkerPool('compute-worker.js', 4);

async function processImages(images) {
  const results = await Promise.all(
    images.map((img) => pool.exec({ type: 'RESIZE', image: img }))
  );
  return results;
}
// npm install comlink
// worker.js
import * as Comlink from 'comlink';

const api = {
  async fibonacci(n) {
    if (n <= 1) return n;
    return api.fibonacci(n - 1) + api.fibonacci(n - 2);
  },

  async processData(items) {
    return items.map((item) => ({
      ...item,
      processed: true,
      hash: hashFunction(item.data),
    }));
  },
};

Comlink.expose(api);

// main.js
import * as Comlink from 'comlink';

const worker = new Worker('worker.js');
const api = Comlink.wrap(worker);

// Doğrudan fonksiyon çağrısı gibi kullanılır
const result = await api.fibonacci(40);
console.log('Fibonacci(40):', result);

Web Worker Kullanım Senaryoları

  1. Resim işleme: Canvas manipülasyonu, filtre uygulama, boyutlandırma
  2. Veri sıralama/filtreleme: Büyük veri setlerinde arama ve sıralama
  3. Kriptografi: Hash hesaplama, şifreleme/çözme
  4. JSON parsing: Büyük JSON dosyalarının parse edilmesi
  5. Syntax highlighting: Kod editörlerinde sözdizimi renklendirme
  6. PDF/CSV oluşturma: Dosya üretimi

Web Worker Sınırlamaları

  • DOM'a erişemez (document, window yok)
  • localStorage ve sessionStorage erişimi yok
  • Ana thread ile iletişim sadece mesajlaşma yoluyla
  • Same-origin politikası geçerli
  • fetch, IndexedDB, WebSocket kullanılabilir
İpucu: Worker'larda DOM manipülasyonu yapamadığınız için, hesaplama mantığını UI mantığından temiz şekilde ayırmak zorunda kalırsınız. Bu, doğal olarak daha iyi bir mimari tasarıma yol açar.

Sonuç

Web Workers, JavaScript'in tek thread sınırlamasını aşmanın ve kullanıcı deneyimini iyileştirmenin güçlü bir yoludur. Ağır hesaplamaları arka plan thread'lerine taşıyarak ana thread'in sorunsuz çalışmasını sağlayın. Transferable Objects ile veri aktarım performansını optimize edin, Worker Pool pattern ile kaynak kullanımını kontrol edin ve Comlink gibi kütüphanelerle geliştirici deneyimini artırın.

Kaynaklar

İlgili Yazılar