What does runtime.LockOSThread do, and when does a Go program actually need it?
answer
- goroutines do not stay on one thread
- some state lives in the thread, not the process
- C libraries and UI toolkits care which thread
- pin this goroutine to this thread
- nesting counter, paired with UnlockOSThread
basics
~20 sruntime.LockOSThread wires the calling goroutine to the OS thread it is running on: that goroutine runs only there, and no other goroutine runs on that thread. You need it for APIs that keep state per OS thread.
solid answer
~40 sGoroutines are multiplexed over a pool of OS threads, so a goroutine can be parked and resumed on a different thread at any point. That is invisible until something stores state in the thread rather than the process: a C library using thread-local storage, Linux kernel state such as namespace membership, or a UI or graphics API that insists on the thread that created a context. `runtime.LockOSThread()` pins the calling goroutine to its current thread and stops any other goroutine from running there, so those calls all land on the same thread. `runtime.UnlockOSThread()` releases it, and the calls nest, so N locks need N unlocks. Calling `LockOSThread` from an `init` function is the standard way to keep `main.main` on the process's main thread, because initialization already runs there.
code
go · 10 linesfunc init() {
// Package init runs on the main goroutine, which the runtime
// has locked to the process's main thread. Locking here keeps
// the count above zero, so main.main runs there too.
runtime.LockOSThread()
}
func main() {
// UI or graphics calls that demand the main thread go here.
}go deeper
Be ready to say in one sentence that goroutines move between OS threads, and that runtime.LockOSThread stops that for the calling goroutine. Naming one reason it is needed, such as a C library or a UI toolkit that keeps state per thread, is enough.
Explain the two guarantees precisely: the goroutine runs only on that thread and no other goroutine runs there. Mention that the calls nest, that the lock is never inherited by goroutines you start, and that it is not CPU affinity.
Show where you would place the lock in real code: the smallest region that covers every call which must see the thread state, on a goroutine created for the job, and with a plan for a thread whose state you cannot restore.
Own the consequence for an interface other teams use. Pinning consumes an OS thread for its duration and constrains where callbacks may run, so decide whether it stays hidden inside your package or becomes a documented obligation on callers.
## A goroutine is not a thread The Go runtime multiplexes many goroutines onto a much smaller set of operating-system threads. A goroutine that parks on a channel, blocks in a system call, is preempted, or stops at a garbage-collection safe point may be resumed later on a completely different thread. Nothing in the language promises otherwise, and ordinary Go code never notices, because everything a goroutine touches lives in the process (the heap, globals) rather than in the thread. ## When the thread suddenly matters Some state is owned by the OS thread, not the process: - **Thread-local storage in a C library.** Many C libraries keep a handle, an error slot or a session context in POSIX thread-local storage. Call one function on thread A and the next on thread B and the second call sees an empty slot. - **Per-thread kernel state on Linux.** Namespace membership set with `setns(2)`, a mount namespace after `unshare(2)`, and some credential and capability state are properties of the calling thread, not of the whole process. - **APIs that demand one specific thread.** On macOS the Cocoa UI APIs must be called from the process's main thread; an OpenGL context is current on exactly one thread; some Windows COM apartments are per-thread. In all three cases, being silently migrated to another thread means the state you set is simply not there any more. ## What the call actually guarantees `runtime.LockOSThread()` wires the calling goroutine to the OS thread it is currently running on. It gives two guarantees at once: 1. that goroutine will execute **only** on that thread, and 2. **no other goroutine** will execute on that thread, until the goroutine has called `runtime.UnlockOSThread()` as many times as it called `LockOSThread`. The counter is per goroutine, so nesting is safe: a helper that locks and unlocks around its own work does not disturb an outer lock. Calling `UnlockOSThread` when the count is already zero is a no-op. ## Three things people get wrong **It is not CPU affinity.** Nothing pins the thread to a particular core; the OS scheduler still moves the thread around. If you need core affinity you are outside Go's runtime API entirely. **It is not inherited.** Writing `go work()` inside a locked goroutine produces an ordinary goroutine that will run on any thread. Every call that must observe the per-thread state has to happen on the locked goroutine itself. **It is not a performance tool.** Locking does not reduce scheduling overhead. If anything it costs the scheduler more, because a runnable locked goroutine can only be run by one specific thread, and the runtime may have to hand that thread the scheduling context and park whichever thread found the goroutine. ## The main-thread idiom Package initialization runs on the main goroutine, and the runtime keeps that goroutine locked to the process's main thread while initialization runs. That is why the conventional way to keep `main.main` on the main thread is: func init() { runtime.LockOSThread() } The count is still above zero when initialization finishes, so the runtime never unwires the main goroutine and everything `main` does stays on the process's original thread. UI toolkits that must own the main thread rely on exactly this. ## The cost of holding the lock While a goroutine holds the lock, its thread serves that one goroutine and nothing else, so the runtime may create another thread to keep the other scheduling contexts busy. If the locked goroutine blocks for a long time, that thread is idle but still allocated, with its own kernel stack. Pinning is therefore something you do for a bounded operation or for one long-lived dedicated goroutine, not something you sprinkle over a hot path. ## The rule of thumb Lock when a sequence of calls must be observed by one thread; keep the locked region as small as the requirement allows; unlock symmetrically unless the thread's state cannot be restored, in which case the goroutine should simply exit while still locked and let the runtime destroy the thread.
- Why does calling runtime.LockOSThread inside an init function keep main.main on the process's main thread?Package initialization runs on the main goroutine, and the runtime keeps that goroutine locked to the original OS thread while init code runs. A `LockOSThread` call in `init` raises the count, so when the runtime drops its own lock at the end of initialization the count is still above zero and the main goroutine stays wired to that thread for the whole of `main`.
- Does a goroutine started inside a locked goroutine inherit the pinning?No. The lock is a property of the goroutine that called it and is never inherited. `go work()` inside a locked goroutine produces an ordinary goroutine that the scheduler may run on any thread, so it will not see thread-local state the parent set up. Anything that must observe that state has to run on the locked goroutine itself.
- What happens if you call runtime.UnlockOSThread more times than you locked?The extra calls are no-ops once the count reaches zero, so nothing breaks in isolation. It is still dangerous inside a library: the counter belongs to the goroutine, so an unbalanced unlock on a goroutine you did not create can release pinning that your caller established for its own per-thread state.
It is like insisting that one clerk handle your whole case at one desk, because the paperwork you need is in that desk's drawer rather than in the office filing room.
saying these in an interview costs you the question
- Says it pins the goroutine to a specific CPU core
- Claims it speeds code up by avoiding scheduling
- Assumes goroutines started inside inherit the pinned thread
- Believes goroutines never migrate between threads anyway
- Confuses it with setting GOMAXPROCS to 1