3.2 Default threading model

Overview

In JavaScript, there is a notable concept: single-threaded execution. JavaScript is known for running on a single thread, which handles all tasks. Think of this thread as a dedicated worker responsible for everything within the program's operation. Node.js popularized this approach, calling it the "single-threaded event loop." This term describes how the single thread efficiently manages and cycles through various events and actions. Despite being a single thread, it keeps things moving by effectively handling its tasks. This unique approach has become a characteristic of JavaScript, and its efficiency is notable.

Single-threaded execution

This single thread is responsible for managing JavaScript code and handling various tasks. To better understand this concept, consider a simple JavaScript HTTP server that echoes back what it receives from clients. The sole thread handles multiple responsibilities, including:

  • Running JavaScript code

  • Monitoring the socket for new TCP connections

  • Managing multiple incoming requests from the socket simultaneously

  • Creating request and response objects

  • Parsing JSON data

  • Assigning a unique request-id to each request

  • Converting responses to JSON format

  • Sending JSON-encoded responses over the TCP connection

  • Closing the socket

It's essential to note that despite its small footprint, this single thread plays a vital role in efficiently managing these complex tasks.

A single thread manages all concurrent tasks, creating a competitive environment where tasks vie for processing time. Referring to the diagram above, you'll see all requests being processed within this solitary thread. It's a competitive struggle for these requests to be included in the thread's schedule.

In contrast, programming languages like Java often employ a different approach. They use a manager with a fixed-sized thread pool, where incoming requests are received and delegated to available threads for processing. The manager assigns a free thread to handle each request. If all threads are occupied, the manager queues the request until a thread becomes available.

When comparing the single thread approach with the thread pool approach, it may seem like JavaScript is inefficient. However, this is not entirely accurate. Although JavaScript executes in a single thread, it has a clever way of managing this limitation. This thread handles incoming requests from multiple clients, and its efficient management enables it to cope with the workload effectively. The key to its efficiency lies in its use of asynchronous processing and callbacks, which allows it to manage multiple tasks simultaneously.

JavaScript uses asynchronous programming, which means it can start a task, move on to another task while the first one is processing in the background, and then return to handle the results when ready. This approach allows JavaScript to handle multiple tasks concurrently without being limited by a single thread. While it may not be the fastest, JavaScript's approach enables effective concurrency management.

Does this mean only one thread is operating within the Deno process? The answer is no. We will explore this further shortly.

Additional threads

Deno, like other JavaScript runtimes, operates in a single-threaded environment. This means one thread handles all tasks, as discussed earlier. However, Deno can create additional application threads when needed, known as web workers.

Web workers enable Deno to perform concurrent tasks efficiently. Think of separate workers handling different tasks, like data processing or user interactions, independently. These workers operate separately from the main thread, optimizing system resource usage and performance. Each web worker has its own isolated execution context, preventing potential conflicts in a multi-threaded scenario.

Web workers operate independently but maintain a connection with their parent thread, enabling bidirectional message exchange. Apart from this communication channel, web workers have complete autonomy.

Each web worker is assigned a fresh V8 snapshot, serving as a unique starting point for execution. Additionally, each worker has its own event loop or Tokio runtime, allowing separate task management and enhancing efficiency.

The significance of web workers lies in their ability to effectively manage CPU-intensive tasks, which could otherwise overwhelm the main thread. Delegating these tasks to web workers ensures the main thread can continue its operations without disruption, enhancing overall performance and responsiveness. This separation of duties optimizes computational resource utilization, making web workers a valuable tool. By leveraging web workers, developers can build more efficient and scalable applications.

Deno's default threading model

Deno executes JavaScript programs in a single loop, similar to other JavaScript runtimes. At first, it may seem like Deno operates with only one thread. However, the reality is more complex. Deno runs JavaScript code using a single thread, but it functions as a multithreaded process underneath. This dual behavior may seem contradictory at first, but it's a crucial aspect of Deno's architecture. Let's explore this in more detail.

When you run a JavaScript program in Deno, it enters a single-threaded environment, executing sequentially, step by step. Deno then leverages its multithreaded nature to enhance efficiency and resource management. The Deno process uses two types of threads: the primary thread for executing JavaScript code and additional V8 threads for garbage collection (GC) purposes.

Deno uses additional threads from the V8 engine by default. The V8 engine executes JavaScript code in the main thread's context. To boost JavaScript execution speed, V8 uses extra threads for CPU-intensive tasks like garbage collection and ahead-of-time compilation.

While it's possible to configure V8 to run on a single thread, this would significantly impact performance. With only one thread, resource-intensive tasks would overload the main thread, hindering overall performance and delaying program execution. This would limit the main thread's availability for running user programs, potentially causing disruptions.

Using a single-threaded V8 configuration could have severe consequences. It could lead to slow responsiveness, longer execution times, and a poor user experience. Therefore, V8's current use of multiple threads is crucial for efficient and smooth JavaScript execution within the Deno runtime environment.

When you start a Deno process with its default settings, it uses 8 threads by default. This applies when no web workers are present. Web workers, as we know, are specialized scripts that run in the background, performing tasks without blocking the main program's execution.

Deno's thread allocation is organized as follows:

  • Main Worker Thread (Main Thread): This is the central thread in every Deno application, responsible for coordinating tasks and facilitating communication between program parts.

  • V8 Threads: Deno uses seven additional V8 threads to enhance performance and responsiveness. These threads work together to execute JavaScript code efficiently and manage runtime operations.

By leveraging multiple threads, Deno improves task execution and resource utilization.

The main thread

This section focuses on the primary thread, which is under the ownership and supervision of the Tokio Runtime. This particular thread is responsible for overseeing the entire operation. Below, you'll find the stack trace for the main thread, offering insights into its sequence of actions and functions. This stack trace provides a clear view of how tasks are executed and controlled within this central thread.

    2731 Thread_8461543   DispatchQueue_1: com.apple.main-thread  (serial)
    + 2731 start  (in libdyld.dylib) + 1  [0x7fff6dd53cc9]
    +   2731 main  (in deno) + 418  [0x108ecfada]
    +     2731 std::sys_common::backtrace::__rust_begin_short_backtrace::h4e8d5235f9254db6  (in deno) + 10  [0x108d368d1]
    +       2731 deno::main::h7d1b5a97f8aef853  (in deno) + 10706  [0x108ec8676]
    +         2731 tokio::runtime::Runtime::block_on::h9f5c6c3dddbd431f  (in deno) + 1768  [0x108d9fa5c]
    +           2731 _$LT$tokio..park..either..Either$LT$A$C$B$GT$$u20$as$u20$tokio..park..Park$GT$::park::h123a854f0de101c3  (in deno) + 204  [0x109439078]
    +             2731 _$LT$tokio..park..either..Either$LT$A$C$B$GT$$u20$as$u20$tokio..park..Park$GT$::park_timeout::h6834b5b57b34a394  (in deno) + 78  [0x109439308]
    +               2731 tokio::io::driver::Driver::turn::h1e669b6b05307d9f  (in deno) + 72  [0x1094394df]
    +                 2731 mio::poll::Poll::poll::hbd211acf9552cbd4  (in deno) + 763  [0x109148978]
    +                   2731 kevent  (in libsystem_kernel.dylib) + 10  [0x7fff6de99766]

As evident from the stack trace, the primary thread is under the management of the tokio runtime. This thread is responsible for executing tokio's event loop, a crucial mechanism that drives the flow of operations. In this setup, the tokio runtime takes charge of coordinating tasks and ensuring efficient execution.

V8's worker threads

Deno has seven v8 worker threads by default. These threads handle essential tasks like garbage collection, runtime optimizations, and Just-In-Time (JIT) compilation. However, they do not directly execute JavaScript code.

To understand their role in Deno's operation, let's explore what these worker threads do when your Deno application runs. They handle behind-the-scenes tasks, such as memory cleanup through garbage collection, which improves code efficiency. They also contribute to runtime optimizations, ensuring smooth and fast code execution. Additionally, they perform Just-In-Time compilation, translating high-level JavaScript code into lower-level machine code for faster execution.

The stack trace of a worker thread provides a snapshot of its activity. The stack trace, similar for all worker threads, offers insight into the processes and functions managed by these threads.

    2731 Thread_8461546: V8 DefaultWorke
    + 2731 thread_start  (in libsystem_pthread.dylib) + 15  [0x7fff6df53b8b]
    +   2731 _pthread_start  (in libsystem_pthread.dylib) + 148  [0x7fff6df58109]
    +     2731 v8::base::ThreadEntry(void*)  (in deno) + 87  [0x10948c547]
    +       2731 v8::platform::DefaultWorkerThreadsTaskRunner::WorkerThread::Run()  (in deno) + 31  [0x109491e6f]
    +         2731 v8::platform::DelayedTaskQueue::GetNext()  (in deno) + 708  [0x109492644]
    +           2731 _pthread_cond_wait  (in libsystem_pthread.dylib) + 698  [0x7fff6df58425]
    +             2731 __psynch_cvwait  (in libsystem_kernel.dylib) + 10  [0x7fff6de97882]

Deno's threading model with web workers

If the application has generated a web worker, the allocation of threads functions in the following manner:

The situation becomes more interesting when a web worker is added. The total thread count increases to 9, distributed as follows:

  • 1 main worker thread (also known as the main thread)

  • 1 web worker thread

  • 7 V8 threads

This adds up to 9 threads in total. Notably, the number of V8 worker threads remains the same even when an additional web worker is introduced. However, it's important to note that users can increase the number of V8 worker threads if needed, depending on their specific use case requirements.

Now, let's examine the stack trace of this new worker thread:

    2718 Thread_8525355: deno-worker-0
      2718 thread_start  (in libsystem_pthread.dylib) + 15  [0x7fff6df53b8b]
        2718 _pthread_start  (in libsystem_pthread.dylib) + 148  [0x7fff6df58109]
          2718 std::sys::unix::thread::Thread::new::thread_start::hd4805e9612a32deb  (in deno) + 45  [0x10a2e1b9d]
            2718 core::ops::function::FnOnce::call_once$u7b$$u7b$vtable.shim$u7d$$u7d$::h7e30e5a55a4af9fa  (in deno) + 116  [0x109d60e85]
              2718 std::sys_common::backtrace::__rust_begin_short_backtrace::h30b57608d94f7821  (in deno) + 2856  [0x109d507e6]
                2718 tokio::runtime::Runtime::block_on::h9f5c6c3dddbd431f  (in deno) + 1768  [0x109dbaa5c]
                  2718 _$LT$tokio..park..either..Either$LT$A$C$B$GT$$u20$as$u20$tokio..park..Park$GT$::park::h123a854f0de101c3  (in deno) + 204  [0x10a454078]
                    2718 _$LT$tokio..park..either..Either$LT$A$C$B$GT$$u20$as$u20$tokio..park..Park$GT$::park_timeout::h6834b5b57b34a394  (in deno) + 78  [0x10a454308]
                      2718 tokio::io::driver::Driver::turn::h1e669b6b05307d9f  (in deno) + 72  [0x10a4544df]
                        2718 mio::poll::Poll::poll::hbd211acf9552cbd4  (in deno) + 763  [0x10a163978]
                          2718 kevent  (in libsystem_kernel.dylib) + 10  [0x7fff6de99766]

The web worker's stack trace is similar to the main thread's stack trace, with the only difference being the starting point within Deno. In the main thread, the thread starts in deno::main, while in the web worker, it starts in core::ops. The core::ops is where the web worker is created.

Now, let's look at an example that shows how threads are distributed when an application creates five web worker threads:

The pattern is clear. When using five web workers, the system uses a total of 13 threads, distributed as follows:

  • 1 primary worker thread (main thread)

  • 5 web worker threads

  • 7 V8 threads

This adds up to 13 threads working together to execute tasks and processes in the system.

More on threads

In the previous section, we explored Deno's threading model, with and without web workers. However, Deno uses additional threads beyond these. The threads we've discussed so far are OS-aware static threads, created at the program's start.

Note that applications can create web workers at runtime, but this can be costly. Moreover, Deno may introduce new threads in the future to handle asynchronous operations. These dynamic threads, or green threads, are created through tokio. It's essential to understand that these threads operate differently from the previous ones, as they are managed by tokio, not the operating system's scheduler.

Last updated