Understanding Concurrency Scheduling Models: From User Threads to Go’s M:P:G Scheduler
This article explains the main concurrency scheduling models—including user‑level, kernel‑level, two‑level (M:N), and coroutine approaches—covers common scheduling algorithms, and details Go’s unique M:P:G scheduler, highlighting its principles, workflow, and advantages for high‑performance parallel execution.
In concurrent programming, the scheduling model determines how multiple tasks (such as threads or coroutines) are assigned to limited compute resources like CPU cores.
User‑Level Thread Model (1:N)
Principle: The user program creates, destroys, and switches threads entirely in user space; the kernel only sees the process and schedules it.
Advantages:
Low thread‑switch overhead because no kernel transition is needed.
Can create a large number of threads without kernel limits.
Disadvantages:
If one thread blocks, the whole process blocks, losing multi‑core benefits.
The application must implement complex thread‑management logic itself.
Typical examples: Early Java thread libraries, early POSIX thread implementations.
Kernel‑Level Thread Model (1:1)
Principle: The operating system kernel directly manages threads, with each user‑level thread mapped to a kernel thread.
Advantages:
True parallel execution; a blocked thread does not affect others.
Full utilization of multi‑core CPUs.
Thread management is handled by the kernel, simplifying user code.
Disadvantages:
Higher overhead for thread creation, destruction, and context switching due to kernel involvement.
The number of threads is limited by kernel resources.
Typical examples: Threads in modern operating systems such as Linux and Windows.
Two‑Level Thread Model (M:N)
Principle: Combines the benefits of user‑level and kernel‑level threads. The application maintains many user‑level threads that are mapped onto a smaller set of kernel threads.
Advantages:
Low switching cost because most switches occur in user space.
Ability to create many user‑level threads, increasing concurrency.
When some user threads block, others can continue on different kernel threads, improving utilization.
Disadvantages:
Complex implementation; must manage both user‑level and kernel‑level threads.
Typical examples: Go language’s goroutine scheduler.
Coroutine Scheduling Model
Principle: Coroutines are lightweight concurrency units scheduled by the runtime or user‑level library, with context switches performed entirely in user mode.
Advantages:
Extremely low switching overhead.
Can create a massive number of coroutines, enabling high concurrency.
Simplifies asynchronous programming by allowing synchronous‑style code.
Disadvantages:
Requires language or library support.
If a coroutine performs a blocking system call, the underlying thread may block (special handling needed, e.g., Go’s network library).
Typical examples:
Go’s goroutine.
Python’s asyncio.
Kotlin coroutines.
C++20 coroutines.
Common Scheduling Algorithms
First‑Come‑First‑Served (FCFS): Executes tasks in the order they arrive.
Shortest Job First (SJF): Prioritizes tasks with the smallest estimated execution time.
Priority Scheduling: Assigns priorities to tasks and runs higher‑priority tasks first.
Round Robin: Gives each task a time slice before moving to the next.
Multilevel Feedback Queue: Combines several algorithms, dynamically adjusting priorities and time slices based on task behavior.
Go’s M:P:G Scheduling Model
Go’s goroutine scheduler implements an M:N model that maps M goroutines onto N operating‑system threads. The core components are:
M (Machine): An OS thread scheduled by the operating system.
P (Processor): A logical processor representing the resources needed to run goroutines (e.g., a CPU core). The number of Ps usually equals the number of CPU cores and can be set via the GOMAXPROCS environment variable.
G (Goroutine): A Go coroutine scheduled by the Go runtime.
Scheduling Process
When a goroutine is created, it is placed into a P’s local run queue or the global run queue.
An M binds to a P and pulls goroutines from that P’s local or global queue for execution.
If a goroutine performs a blocking operation (e.g., I/O), the M moves the goroutine to a waiting queue and tries to run another goroutine.
When the blocking operation completes, the goroutine is re‑queued for execution by an M.
Key Characteristics of the Go Scheduler
Cooperative scheduling: Goroutines voluntarily yield the CPU by calling runtime.Gosched() or when they block.
Preemptive scheduling: The runtime monitors execution time; if a goroutine runs too long, it is preempted to give others a chance.
Work stealing: When a P’s local run queue is empty, it steals goroutines from other Ps or the global queue to keep CPUs busy.
Conclusion
Different scheduling models suit different scenarios. Choosing the right model requires considering concurrency level, switching overhead, resource utilization, and programming model. Go’s M:P:G scheduler blends the advantages of user‑level and kernel‑level threads, delivering high concurrency, low overhead, and efficient resource usage.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Ops Development & AI Practice
DevSecOps engineer sharing experiences and insights on AI, Web3, and Claude code development. Aims to help solve technical challenges, improve development efficiency, and grow through community interaction. Feel free to comment and discuss.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
