Published
- 6 min read
Inter-thread communication and parallelism in javascript
Let’s say a thread delegated a task to another thread. (Assuming this is done by placing the task in a shared memory location such as a queue). How does the background thread notify the primary thread once the task is completed/failed?
The background thread can notify the primary thread using several common methods:
- Shared Memory with Synchronization: The threads use shared data (e.g., status flags) protected by synchronization mechanisms like mutexes, condition variables, or semaphores. The background thread updates the shared status and signals the primary thread, which was waiting for this signal.
- Callbacks: The primary thread passes a function reference to the background thread. The background thread invokes this function upon completion/failure.
- Futures/Promises/Tasks: Higher-level abstractions where the primary thread receives a
Futureobject immediately. The background thread sets the result/exception on thisFuture, which the primary thread can thenget()or attach callbacks to. - Message Queues: The background thread places a result/status message on a queue that the primary thread monitors.
- Event Systems: Threads publish events (e.g., “TaskCompleted”) that other threads subscribe to.
Wait, what are the differences between mutexes and condition variables again
- Mutexes (
threading.Lockin Python) ensure only one thread accesses the shared data at a time. - Condition Variables are used with mutexes to allow threads to efficiently wait for a specific condition (e.g., task completion) without constantly polling. The background thread signals the condition variable when the state changes.
- Semaphores are counting mechanisms that can be used for simple signaling (a count of 0 means “wait,” releasing increments the count, potentially waking a waiting thread).
On a high level, can I say that if the primary thread doesn’t want to blocked waiting for the completion of background thread task, it has to have a event loop to check the status of the task
Yes, on a high level, if the primary thread needs to remain unblocked while waiting for a background task, it generally requires a non-blocking mechanism for notification. An event loop is a powerful pattern to achieve this. Instead of blocking, the primary thread continues its work and either:
- Periodically polls for status (less efficient).
- Receives callbacks (often managed by a higher-level event system or thread pool) that are dispatched to its context.
- Utilizes a full-fledged event loop (like
asyncio) that orchestrates asynchronous operations and dispatches their completion callbacks when the main thread is free.
Well how can a callback triggered by background thread run in main thread context if main thread is doing something. It has to put that in a completion queue I guess
You are absolutely correct. A callback triggered by a background thread cannot directly execute in the primary (main) thread’s context if the main thread is busy. To achieve this, it relies on an intermediary mechanism, often a message queue or event queue (your “completion queue”).
- The background thread places an “event” or “message” (containing the callback and its arguments) onto a thread-safe queue.
- The primary thread, as part of its own event loop, periodically checks this queue.
- When a message is found, the primary thread dequeues it and then executes the callback function itself, within its own context. This ensures thread safety (especially for UI updates) and keeps the main thread responsive.
So, technically even a callback is also a completion queue underneath
Yes, you are very accurate. In scenarios where a callback needs to run in a different thread’s context (like a background thread notifying the main/UI thread), that callback is indeed queued underneath by an underlying mechanism. The background thread doesn’t directly call it, but rather puts a “request” for the main thread to execute the callback into a queue, which the main thread processes within its event loop.
Is this how things work in JS land with async/await, callbacks and promises?
Yes, this is exactly how it works in JavaScript. JavaScript is single-threaded, so it achieves concurrency through an Event Loop and queues, not through true parallelism with multiple CPU cores.
- Delegation: When
setTimeoutorWorkspaceare called, JavaScript delegates these long-running tasks to Host APIs (browser Web APIs or Node.js C++ APIs), which run in the background. - Queuing: Once the Host API completes its task, it places the associated callback function
(or Promise
.then()handler, orasync/awaitcontinuation) into either the Microtask Queue (for Promises/async/await, higher priority) or the Macrotask Queue (forsetTimeout, I/O). - Event Loop: The single JavaScript Event Loop continuously checks if the Call Stack (where synchronous JS runs) is empty. If it is, it first drains the entire Microtask Queue, and then picks one task from the Macrotask Queue to push onto the Call Stack for execution. This ensures non-blocking behavior.
If js in single thread, how can async functions like setTimeout and api requests are executed.
simple technical explainations please(no analogies)
JavaScript has a single Call Stack for code execution. Asynchronous operations are handled by:
- Delegation to Host Environments: When
setTimeoutorWorkspaceare called, the JavaScript engine delegates these tasks to Host APIs (browser Web APIs or Node.js C++ APIs). These APIs are implemented in lower-level languages (like C++) and can perform operations concurrently outside the JavaScript engine. The JS function immediately returns. - Background Execution: The Host API performs the task (e.g., timer countdown, network request) in the background.
- Completion & Enqueueing: Upon completion, the Host API places the associated JavaScript
callback function into either the Microtask Queue (for Promises, async/await) or the Macrotask
Queue (for
setTimeout, I/O). - Event Loop: The Event Loop constantly monitors if the Call Stack is empty. If it is, it first drains the entire Microtask Queue, then picks one task from the Macrotask Queue, and moves it to the Call Stack for the JavaScript engine to execute. This ensures sequential execution on the single thread while maintaining non-blocking behavior.
So host apis have different javascript interpreter. is that what you are saying
No, that’s incorrect. Host APIs do NOT have different JavaScript interpreters.
- The JavaScript Engine (e.g., V8) is the only component that interprets and executes your JavaScript code. It has a single Call Stack.
- Host APIs (Web APIs in browsers, C++ APIs in Node.js) are functionalities written in other languages (like C++) that operate outside the JavaScript engine.
- When JavaScript calls an async Host API, it’s a delegation of a task to that external C++ code. The C++ code performs its operation. Once done, it doesn’t execute JavaScript; instead, it queues the relevant JavaScript callback function to be executed by the same single JavaScript engine when the Event Loop determines the Call Stack is free.
Web APIs (like
This is done for:setTimeout,Workspace, DOM manipulation) in browsers, and core modules in Node.js (likefs,http), are predominantly implemented in low-level languages such as C or C++. - Performance: To execute these I/O-bound or computationally intensive tasks as fast as possible.
- Access to System Resources: To directly interact with the operating system for things like network sockets, file I/O, or hardware.
- Security and Stability: To abstract complex system-level operations behind a safe JavaScript API.
When JavaScript calls Workspace(), the JS engine delegates the actual network request to this underlying C++ implementation. This C++ code runs in the background. Once it receives a response, it doesn’t execute JavaScript. Instead, it places the associated JavaScript callback (e.g., the .then() handler) into the JavaScript runtime’s queues. The JavaScript Event Loop then picks up that callback when the single JavaScript thread is free to execute it.