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↓ runGo 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 ProcessGoroutine 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 AThread 2 → Goroutine BTime 2:Thread 1 → Goroutine CThread 2 → Goroutine A
Goroutine A moved.
6. OS threads are tied to CPU execution
The OS schedules threads onto logical CPUs.
Goroutine↓ Go schedulerOS Thread↓ OS schedulerLogical CPU↓Physical Core
At one instant:
Logical CPU 1 → Thread ALogical CPU 2 → Thread BLogical CPU 3 → Thread CLogical 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 cores16 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 Cgoroutine goroutine goroutine goroutineall 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 ThreadsNEW 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 CPUs1 Go certgen process100 goroutines20 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↓ runProcess↓ containsOS Threads↓ OS schedules ontoLogical CPUs↓ provided byPhysical CPU cores
And for Go, insert one extra layer:
Go Process↓ containsGoroutines↓ Go schedules ontoOS Threads↓ OS schedules ontoLogical 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