Tuesday, 18 August 2026

OS core, OS thread, process, go process, go routine

 

 Binary vs process

A Go binary sitting on disk is just a file:

certgen-service

When you run it:

./certgen-service

the OS creates a process.

Go binary
↓ run
Go process

A process is mainly a container for things like memory, files, sockets, and threads.


2. Every running process needs at least one OS thread

A generic process can be as simple as:

Process
└── OS Thread
↓
Logical CPU
↓
Physical CPU Core

The thread is the actual execution unit that the OS schedules.

The process itself is not what directly runs on a core.


3. Go adds another layer: goroutines

A Go process has the Go runtime inside it.

Go Process
│
├── Go Runtime
│
├── main goroutine
├── Goroutine A
├── Goroutine B
├── Goroutine C
├── runtime goroutines
│
└── OS Threads

Important point:

main() itself runs as the main goroutine.

Even if you never write:

go someFunction()

Go still has internal goroutines for runtime work such as GC and scheduling.


4. Goroutine is not an OS thread

A goroutine is a lightweight user-space execution unit.

You can casually think:

goroutine ≈ lightweight thread

But technically:

Goroutine != OS Thread

Many goroutines are scheduled onto a smaller number of OS threads:

Go Process

Goroutine main ─┐
Goroutine A ────┤
Goroutine B ────┤
Goroutine C ────┤
Goroutine D ────┤
Goroutine E ────┘
↓
Go Scheduler
↓
┌───────────────┐
│ OS Thread 1 │
│ OS Thread 2 │
│ OS Thread 3 │
└───────────────┘

Creating:

go foo()

does not mean:

new goroutine
=
new OS thread

Usually it just creates another goroutine for the Go scheduler to manage.


5. One OS thread executes one goroutine at a time

Suppose several goroutines can use Thread 1:

OS Thread 1
├── main goroutine
├── Goroutine A
├── Goroutine B
└── Goroutine C

That does not mean all four run simultaneously on Thread 1.

It means over time:

Thread 1
↓
main runs
↓
A runs
↓
B runs
↓
main runs again
↓
C runs

Go switches which goroutine is running.

And goroutines are generally not permanently attached to a specific OS thread.

For example:

Time 1:

Thread 1 → Goroutine A
Thread 2 → Goroutine B


Time 2:

Thread 1 → Goroutine C
Thread 2 → Goroutine A

Goroutine A moved.


6. OS threads are tied to CPU execution

The OS schedules threads onto logical CPUs.

Goroutine
↓ Go scheduler
OS Thread
↓ OS scheduler
Logical CPU
↓
Physical Core

At one instant:

Logical CPU 1 → Thread A
Logical CPU 2 → Thread B
Logical CPU 3 → Thread C
Logical CPU 4 → Thread D

So if you have 4 logical CPUs, roughly 4 OS threads can actually execute instructions simultaneously.


7. Physical core vs logical CPU

Without SMT / Hyper-Threading:

Physical Core 1
└── Logical CPU 1
↓
one OS thread executing

With SMT / Hyper-Threading:

Physical Core 1
├── Logical CPU 1 → Thread A
└── Logical CPU 2 → Thread B

So a machine might say:

8 physical cores
16 logical CPUs

The OS generally schedules against those 16 logical CPUs.


Full Go picture

Putting everything together:

MACHINE
│
┌───────────┴───────────┐
│ │
Physical Core 1 Physical Core 2
├─ Logical CPU 1 ├─ Logical CPU 3
└─ Logical CPU 2 └─ Logical CPU 4
↑ ↑
│ OS Scheduler │
│ │
OS Thread 1 OS Thread 2
↑ ↑
│ Go Scheduler │
│ │
┌───────┴───────┐ ┌──────┴───────┐
│ │ │ │
main A B C
goroutine goroutine goroutine goroutine

all inside one Go process

The two schedulers are important:

Goroutines
↓
Go scheduler
↓
OS Threads
↓
OS scheduler
↓
Logical CPUs / cores

8. What happens when a goroutine launches OpenSSL

This is different because OpenSSL becomes a separate process.

Suppose you write:

go func() {
exec.Command("openssl", ...).Run()
}()

Conceptually:

Go Process
│
├── main goroutine
│
├── Goroutine A
│ │
│ └── exec.Command("openssl")
│ │
│ │ asks OS to launch
│ ↓
│
└── Go OS Threads


NEW PROCESS
↓

OpenSSL Process
└── OS Thread
↓
Logical CPU

The OpenSSL process has its own address space and own OS thread(s).

So:

Goroutine
↓
starts
↓
OpenSSL Process
↓
contains
↓
OpenSSL OS Thread
↓
CPU

The OpenSSL process is not an OS thread itself.


9. Your certificate service example

Say you have:

8 logical CPUs
1 Go certgen process
100 goroutines
20 OpenSSL commands running

You could conceptually have:

Machine
│
├── Logical CPU 1
├── Logical CPU 2
├── Logical CPU 3
├── Logical CPU 4
├── Logical CPU 5
├── Logical CPU 6
├── Logical CPU 7
└── Logical CPU 8
↑
│ OS schedules everything here
│
┌────────┴───────────────────────────────┐
│ │
│ Go Process │
│ ├── main goroutine │
│ ├── goroutine 1 │
│ ├── goroutine 2 │
│ ├── ... │
│ ├── goroutine 100 │
│ │ │
│ └── several Go OS threads │
│ │
├── OpenSSL Process #1 │
│ └── OS thread │
│ │
├── OpenSSL Process #2 │
│ └── OS thread │
│ │
├── OpenSSL Process #3 │
│ └── OS thread │
│ │
└── ... │
OpenSSL Process #20 │
└── OS thread │

All of those threads compete for the same CPU resources.

That's why creating:

1000 goroutines
+
1000 OpenSSL processes

does not mean you get 1000-way CPU parallelism.

If you only have 8 logical CPUs:

thousands of runnable threads/work
↓
OS scheduler
↓
8 logical CPUs

Only a limited amount can actually execute at once.


The shortest version to remember

Binary
↓ run
Process
↓ contains
OS Threads
↓ OS schedules onto
Logical CPUs
↓ provided by
Physical CPU cores

And for Go, insert one extra layer:

Go Process
↓ contains
Goroutines
↓ Go schedules onto
OS Threads
↓ OS schedules onto
Logical CPUs
↓
Physical CPU cores

And for your OpenSSL case:

Go goroutine
↓
exec.Command()
↓
NEW OpenSSL process
↓
OpenSSL OS thread(s)
↓
CPU

That entire model is the foundation for understanding why goroutine count, GOMAXPROCS, CPU cores, semaphores, and spawning many OpenSSL processes affect the performance of your certificate service.

No comments:

Post a Comment