A CPU with many cores does not make application code parallel by itself. The operating system can schedule multiple threads at the same time, but the language and runtime decide how application work reaches those threads. That distinction explains why Go, Rust, C++, Java, JavaScript, and PHP can all use a multicore machine even though their programming models look very different.
The useful question is not simply whether a language is “multithreaded.” It is how a unit of application work becomes something the operating system can schedule.
Native threads map work directly to OS threads
C++ and Rust expose native threads through their standard libraries. A new std::thread in C++ or std::thread::spawn in Rust creates a separate thread of execution backed by the platform threading facilities.
In C++:
#include <thread>
void work() {
// CPU-bound work
}
int main() {
std::thread t1(work);
std::thread t2(work);
t1.join();
t2.join();
}In Rust:
use std::thread;
fn main() {
let a = thread::spawn(|| {
// CPU-bound work
});
let b = thread::spawn(|| {
// CPU-bound work
});
a.join().unwrap();
b.join().unwrap();
}If two runnable threads are available and the machine has at least two schedulable CPU cores, the operating system may run them in parallel. The language does not need a user-space scheduler between these application threads and the kernel scheduler.
That directness gives the program strong control, but an OS thread is not free. Each thread requires kernel bookkeeping, stack space, scheduling state, and synchronization when data is shared. A design that creates one native thread for every small task can spend significant resources managing threads instead of doing useful work.
Go inserts a runtime scheduler between goroutines and threads
Go exposes goroutines rather than asking application code to create an OS thread for every concurrent function.
go taskA()
go taskB()Those two goroutines are application-level units of work. The Go runtime schedules runnable goroutines onto a smaller or changing set of OS threads, which the operating system then schedules onto CPU cores.
The relationship is therefore:
goroutines
|
Go scheduler
|
OS threads
|
CPU coresThis matters because a program can have far more goroutines than available CPU cores. Thousands of goroutines can wait on network I/O, timers, channels, or locks while only the runnable subset needs execution time.
A Go process is therefore not “single-threaded because goroutines are not threads.” The runtime can use multiple OS threads, and several goroutines can execute in parallel when the scheduler has multiple execution resources available.
The opposite is also important: merely running a Go program on a 16-core machine does not split one sequential function across 16 cores. Parallelism appears only when independent runnable work exists.
Java has both platform threads and virtual threads
Java traditionally exposes platform threads through java.lang.Thread. A platform thread is associated with an operating-system thread while it runs Java code.
Java also provides virtual threads. A virtual thread is still a Thread from the application’s point of view, but it is not permanently tied to one OS thread. The Java runtime can suspend a virtual thread that blocks and let another virtual thread use the underlying carrier thread.
That makes virtual threads useful for applications with many concurrent tasks that spend substantial time waiting for I/O. It does not turn a CPU-intensive calculation into a faster calculation automatically.
For CPU-bound work, the limiting resource remains CPU execution capacity. If a machine can execute eight CPU-heavy threads at once, creating one million virtual threads does not create one million cores.
This distinction between concurrency and parallelism is central:
concurrency = many tasks can make progress over time
parallelism = multiple tasks execute at the same instantA runtime can make concurrency cheap without changing the physical amount of parallel CPU capacity.
JavaScript keeps the main execution model simple but can add workers
Browser JavaScript and Node.js are commonly described as single-threaded because ordinary JavaScript callbacks execute on a main event-loop thread.
That description is incomplete if it is interpreted as “JavaScript can use only one CPU core.”
Node.js can create Worker Threads for CPU-intensive JavaScript work. Browsers provide Web Workers for a similar purpose. Each worker has its own JavaScript execution context and can run independently of the main thread.
A Node.js program can therefore look conceptually like this:
main event-loop thread
|
+-- worker 1
+-- worker 2
+-- worker 3The event loop is especially effective for I/O-heavy applications because the main thread does not need one blocking application thread for every socket operation. CPU-heavy JavaScript is different. A long computation on the main thread blocks other JavaScript callbacks until it yields or completes, so workers or additional processes become relevant when true CPU parallelism is required.
“Single-threaded main loop” and “single-core application” are not equivalent statements.
PHP commonly scales across cores with processes
Traditional PHP web deployments often use PHP-FPM. The common execution model is not one permanently running PHP process with many application threads. Instead, a pool contains multiple worker processes, and different workers can handle different requests.
On an eight-core server, the layout may resemble:
request A -> PHP-FPM worker process 1
request B -> PHP-FPM worker process 2
request C -> PHP-FPM worker process 3
request D -> PHP-FPM worker process 4The operating system can schedule those processes on different CPU cores. Multiple CPU cores are therefore useful even when one ordinary PHP request executes its PHP code sequentially.
The limitation appears when one request contains one large CPU-bound job. A single sequential job does not automatically spread itself across every core merely because the server has many PHP-FPM workers. To use more cores for that job, the work must be divided among processes, specialized workers, extensions, or another execution system.
This is why process-based concurrency can scale a web server very well while still behaving differently from in-process multithreading.
A multicore CPU is useful even when one request is sequential
The assumption that a single-threaded request wastes the rest of the CPU ignores server-level concurrency.
Suppose a server has 16 logical CPUs and receives 100 independent HTTP requests. Even if each request executes sequential code, the server can run several request handlers at once when they are placed in different processes or threads.
What matters is the amount of independent runnable work:
one sequential task
-> limited parallelism
many independent tasks
-> scheduler can distribute them across cores
one task split into independent pieces
-> the pieces may execute in parallelA busy PHP-FPM server, a Node.js cluster, a Java server, and a Go service can all keep many cores busy. They reach that result through different runtime structures.
Compilation model and threading model are separate concerns
Whether a language compiles directly to native machine code does not determine whether it supports parallel execution.
C++, Rust, and Go commonly produce native executables, but their concurrency models differ. Java normally executes bytecode through the JVM and can still use many native threads and CPU cores. JavaScript runs through an engine such as V8 and can use worker threads. PHP is usually executed through a runtime and can use multiple worker processes.
These are separate layers:
source language
|
compiler / VM / runtime
|
concurrency model
|
OS scheduler
|
CPU coresA native executable may remain almost entirely single-threaded. A VM-based application may keep dozens of cores busy. The binary format does not decide the concurrency topology.
The right model depends on the workload boundary
Native threads fit workloads that need explicit parallel execution and direct control over shared memory. Lightweight runtime tasks such as goroutines or virtual threads make large numbers of concurrent operations easier to manage. Event-loop systems avoid dedicating one application thread to every waiting I/O operation. Multi-process systems isolate workers and let the operating system distribute separate requests across cores.
The practical boundary is whether the workload contains independent execution units.
For a server handling many unrelated requests, process-based PHP can use a multicore CPU effectively. For a Go service with thousands of concurrent network operations, goroutines let the runtime multiplex work across threads. For CPU-heavy algorithms in Rust or C++, native threads or a thread pool can divide computation directly. For JavaScript, workers are needed when CPU-heavy work must run in parallel with the main event loop.
The hardware only supplies execution capacity. The runtime model determines how application work is exposed to that capacity, and the application architecture determines whether enough independent work exists to use it.