This project demonstrates how to process an image in the browser using Web Workers and the Atomics API with a SharedArrayBuffer. The application applies a grayscale filter to an image, utilizing parallel processing for improved performance and efficient memory handling.
- Processes images in parallel using multiple Web Workers.
- Demonstrates the use of SharedArrayBuffer for sharing memory between threads.
- Uses the Atomics API for thread synchronization and coordination.
- Implements advanced parallel processing patterns like barriers and progress monitoring.
- Displays the original image and the processed grayscale image side-by-side in a clean, centered layout.
- Shows real-time progress of each worker and overall processing.
This project is a practical example of modern web development techniques that leverage multithreading in JavaScript. By utilizing SharedArrayBuffer and Atomics, it showcases:
- Efficient Parallel Processing:
- Web Workers process different chunks of the image simultaneously, reducing processing time.
- Memory Optimization:
- Shared memory (via
SharedArrayBuffer
) eliminates the need to copy data between threads.
- Shared memory (via
- Thread-Safe Synchronization:
- The
Atomics
API ensures thread-safe operations across workers without race conditions.
- The
This approach is particularly useful for computationally intensive tasks like image manipulation, video processing, or simulations that can benefit from parallel execution.
git clone https://github.com/erickwendel/grayscale-image-processor-workers-atomics.git
cd grayscale-image-processor-workers-atomics
Ensure you have Node.js installed, then run:
npm install
This project uses browser-sync to serve the files and enable the required headers for SharedArrayBuffer
.
To start the application:
npm start
This will:
- Serve the app at
http://localhost:3000
. - Enable the necessary HTTP headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Visit http://localhost:3000
in a modern browser (e.g., Chrome, Edge) that supports SharedArrayBuffer
and the required security policies.
+-----------------------------------------------------+
| Main Thread |
+---------------------+-----------------------------+-+
| |
| SharedArrayBuffer |
| +-------------------------+ |
| | Image Data (RGBA) | |
| +-------------------------+ |
| |
| Control Buffer (Int32Array) |
| +-------------------------+ |
| | [0]: Completed workers | | <-+ Atomics.add(control, 0, 1)
| | [1]: Notification flag | | <-+ Atomics.store/notify
| | [2]: Barrier counter | | <-+ Atomics.add/wait
| | [8+]: Worker progress | | <-+ Atomics.store
| +-------------------------+ |
| |
+-------------v-------------+ |
| | |
+-------v-------+ +-------v-------+ +-------v-------+
| ImageWorker 0 | | ImageWorker 1 | ... | ImageWorker N |
+---------------+ +---------------+ +---------------+
| Process Chunk | | Process Chunk | | Process Chunk |
| | | | | |
+---------------+ +---------------+ +---------------+
| | |
+-------------------+---------------------+
|
+-------v-------+
| ProgressMonitor|
+-------+--------+
|
v
+-----------------------------------------------------+
| User Interface |
| |
| +-------------+ +-----------+ +-----------------+ |
| | Original | | Processed | | Progress Bars | |
| | Image | | Image | | | |
| +-------------+ +-----------+ +-----------------+ |
+-----------------------------------------------------+
-
Image Loading:
- Users upload an image or a default image is loaded.
- The image is drawn on a
<canvas>
and pixel data is extracted.
-
Shared Memory Setup:
- A
SharedArrayBuffer
is created for image data (zero-copy between threads). - A control buffer is created for thread coordination using Atomics.
- A
-
Parallel Processing:
- The image is divided into chunks, each processed by a separate Web Worker.
- Workers use barrier synchronization via Atomics to start together.
- Each worker processes its chunk and updates progress atomically.
-
Progress Monitoring:
- A dedicated monitor worker tracks progress using Atomics.load
- Updates the UI with real-time progress information
-
Completion:
- Workers mark completion with Atomics.add
- The last worker notifies main thread with Atomics.notify
- The processed grayscale image is rendered on the result canvas
This project demonstrates advanced usage of the JavaScript Atomics API to ensure thread-safe operations and efficient coordination between worker threads. Here's a detailed explanation of the specific Atomics functions used and their purpose:
- Usage in Project: Used to initialize values in the shared buffer and to update worker progress.
- How it works:
Atomics.store(typedArray, index, value)
writes a value to the shared array at the specified index in a way that's visible to all threads. - Purpose: Ensures all worker threads see consistent data when initializing or updating shared values.
- Example:
Atomics.store(control, workerIndex, 0)
to initialize worker progress.
- Usage in Project: Used to read the current state of worker progress and completion.
- How it works:
Atomics.load(typedArray, index)
reads a value from the shared array in a way that respects the ordering of operations across threads. - Purpose: Gets current state information without risk of reading partially-updated values.
- Example:
const completed = Atomics.load(control, 0)
to check how many workers have completed.
- Usage in Project: Used to increment the completion counter atomically when a worker finishes processing.
- How it works:
Atomics.add(typedArray, index, value)
atomically adds a value to an existing value in the array and returns the old value. - Purpose: Thread-safe way to increment counters without race conditions.
- Example:
const completedWorkers = Atomics.add(control, 0, 1) + 1
to increment and track completed workers.
- Usage in Project: Used to wake up waiting threads when processing is complete.
- How it works:
Atomics.notify(typedArray, index, count)
wakes up threads waiting on the specified index. - Purpose: Efficient thread notification without polling, allowing threads to sleep until needed.
- Example:
Atomics.notify(control, 1)
to notify the main thread that processing is complete.
- Usage in Project: Used in workers to implement a barrier pattern, ensuring all workers start processing at approximately the same time.
- How it works:
Atomics.wait(typedArray, index, value, timeout)
blocks the thread until the value at the index changes from the expected value. - Purpose: Coordinated thread synchronization without busy waiting.
- Example:
Atomics.wait(control, 2, waitingAt, 100)
for barrier synchronization in workers.
The project uses a carefully designed shared memory layout:
- Index 0: Total completed workers counter
- Index 1: Main thread notification flag
- Index 2: Barrier synchronization counter
- Index 3-7: Reserved for future use
- Index 8+: Individual worker progress (one slot per worker)
- Zero-Copy Processing: Workers operate directly on shared memory without data duplication.
- Lock-Free Synchronization: Avoids traditional locks while maintaining thread safety.
- Efficient Coordination: Workers can coordinate with minimal overhead.
- Progress Monitoring: Thread-safe tracking of individual and overall progress.
- Barrier Synchronization: Ensures workers start processing in a coordinated way.
- JavaScript: For client-side logic and multithreading with Web Workers.
- Web Workers API: For creating and managing background threads.
- Atomics API: For thread-safe operations and synchronization between workers.
Atomics.store
: For writing values visible to all threadsAtomics.load
: For reading values with memory ordering guaranteesAtomics.add
: For atomic counter incrementationAtomics.notify
: For waking waiting threadsAtomics.wait
: For thread blocking until notification (in worker contexts)
- SharedArrayBuffer: For efficient zero-copy shared memory between threads.
- Canvas API: For image manipulation and display.
- browser-sync: To serve the app with proper security headers required for SharedArrayBuffer.
npm start
: Starts the app using browser-sync with headers enabled.
This project follows a clean architecture with a focus on the Single Responsibility Principle:
-
Components Layer:
Controller
: Handles user input and orchestrates the application flowView
: Manages canvas rendering and image displayProgressView
: Handles progress UI updates and visualization
-
Services Layer:
ImageProcessor
: Coordinates the image processing workflowWorkerCoordinator
: Manages worker creation, coordination, and termination
-
Workers Layer:
imageWorker.js
: Processes image chunks with thread-safe operationsprogressMonitorWorker.js
: Monitors and reports worker progress
This separation of concerns allows for better maintainability and extensibility of the codebase.
The project implements several optimizations for performance:
- Parallelism: Distributes work across multiple workers based on CPU core count
- Zero-Copy Architecture: Uses SharedArrayBuffer to avoid costly data transfers between threads
- Canvas Optimization: Sets
willReadFrequently
for canvas contexts when reading pixel data - Atomic Operations: Uses lock-free synchronization for minimal overhead
- Barrier Synchronization: Ensures workers start simultaneously for maximum parallelism
- Chunk-Based Processing: Each worker processes only its assigned chunk, reducing memory pressure
- Start the app:
npm start
- Upload an image or view the default image.
- Watch as the grayscale filter is applied in real-time using parallel processing.
- Observe the individual worker progress bars showing the processing status.
This project requires a browser that supports:
- SharedArrayBuffer and Atomics API
- Proper security policies (required for SharedArrayBuffer access):
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Due to speculative execution attacks like Spectre, browsers require these security headers to be present for SharedArrayBuffer usage. The application uses browser-sync to automatically set these headers.
- Cross-Origin-Opener-Policy: same-origin: Ensures the page can only share its browsing context group with same-origin pages.
- Cross-Origin-Embedder-Policy: require-corp: Ensures all resources loaded by the page explicitly allow being loaded.
These security measures prevent potential data leakage through timing attacks on shared memory.
- Google Chrome (version 92+)
- Firefox (version 89+)
- Edge (version 92+)
- Safari (version 15.2+)