<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ThinkerQAQ]]></title><description><![CDATA[Technical writing on backend engineering, Go, concurrency, distributed systems, and AI. Original articles and more at thinkerqaq.com/en/.]]></description><link>https://thinkerqaq.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ad35f2b8a03d6436a78e29a/e907078f-45c4-456d-bc6b-22cb3912b5c1.jpg</url><title>ThinkerQAQ</title><link>https://thinkerqaq.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 10:10:34 GMT</lastBuildDate><atom:link href="https://thinkerqaq.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Concurrency Programming (7): volatile — From Language Semantics to the CPU]]></title><description><![CDATA[The previous two articles traced atomic operations from language-level semantics down to the CPU, showing how Atomicity, Visibility, and Ordering are…]]></description><link>https://thinkerqaq.hashnode.dev/concurrency-programming-7-volatile-from-language-semantics-to-the-cpu</link><guid isPermaLink="true">https://thinkerqaq.hashnode.dev/concurrency-programming-7-volatile-from-language-semantics-to-the-cpu</guid><category><![CDATA[Python]]></category><category><![CDATA[Java]]></category><category><![CDATA[golang]]></category><category><![CDATA[concurrency]]></category><dc:creator><![CDATA[ThinkerQAQ]]></dc:creator><pubDate>Tue, 03 Nov 2026 14:16:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/10e0bf8c-72b9-4c43-b54f-35870f928490.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Table of Contents</h2>
<ul>
<li><a href="#0-what-does-this-article-answer-next">0. What Does This Article Answer Next?</a></li>
<li><a href="#1-what-guarantees-does-java-volatile-provide-at-the-language-level">1. What Guarantees Does Java volatile Provide at the Language Level?</a><ul>
<li><a href="#11-visibility-and-ordering">1.1 Visibility and Ordering</a></li>
<li><a href="#12-volatile-does-not-make-compound-operations-atomic">1.2 volatile Does Not Make Compound Operations Atomic</a></li>
</ul>
</li>
<li><a href="#2-how-does-hotspot-implement-volatile">2. How Does HotSpot Implement volatile?</a><ul>
<li><a href="#21-layers">2.1 Layers</a></li>
<li><a href="#22-end-to-end-implementation-path-for-a-volatile-write-and-read">2.2 End-to-End Implementation Path for a volatile Write and Read</a></li>
<li><a href="#23-guarantees-at-the-runtime--language-implementation-layer">2.3 Guarantees at the Runtime / Language Implementation Layer</a><ul>
<li><a href="#231-visibility">2.3.1 Visibility</a></li>
<li><a href="#232-ordering">2.3.2 Ordering</a></li>
</ul>
</li>
<li><a href="#24-guarantees-at-the-hardware-layer">2.4 Guarantees at the Hardware Layer</a></li>
</ul>
</li>
<li><a href="#3-why-dont-go-and-cpython-have-java-style-volatile">3. Why Don't Go and CPython Have Java-style volatile?</a><ul>
<li><a href="#31-go-a-language-design-choice">3.1 Go: A Language Design Choice</a></li>
<li><a href="#32-cpython-synchronization-remains-a-runtime-responsibility">3.2 CPython: Synchronization Remains a Runtime Responsibility</a></li>
</ul>
</li>
<li><a href="#4-how-volatile-atomics-and-mutexes-differ">4. How volatile, Atomics, and Mutexes Differ</a></li>
<li><a href="#5-next-from-mutexes-to-read-write-locks">5. Next: From Mutexes to Read-Write Locks</a></li>
</ul>
<hr />
<h2>0. What Does This Article Answer Next?</h2>
<p>The previous two articles traced atomic operations from language-level semantics down to the CPU, showing how Atomicity, Visibility, and Ordering are implemented.</p>
<p>This article follows the same path for <code>volatile</code>.</p>
<p><code>volatile</code> does not make a compound update such as <code>counter++</code> atomic. It is better suited to publication and observation:</p>
<pre><code class="language-text">Thread A                         Thread B

counter = 1                     if (ready) {
ready = true                        print(counter)
                                }
</code></pre>
<p>The required guarantee is:</p>
<blockquote>
<p>If Thread B observes <code>ready == true</code>, a subsequent read of <code>counter</code> must observe <code>1</code>.</p>
</blockquote>
<p>We first establish this guarantee in the Java Memory Model, then trace how HotSpot preserves it down to the CPU.</p>
<hr />
<h2>1. What Guarantees Does Java volatile Provide at the Language Level?</h2>
<h3>1.1 Visibility and Ordering</h3>
<p>Declare <code>ready</code> as <code>volatile</code>:</p>
<pre><code class="language-java">int counter = 0;
volatile boolean ready = false;

// Thread A
counter = 1;
ready = true;

// Thread B
if (ready) {
    System.out.println(counter);
}
</code></pre>
<p><code>counter</code> remains an ordinary field.</p>
<p><a href="https://docs.oracle.com/javase/specs/jls/se25/html/jls-17.html#jls-17.4.4">JLS §17.4.4</a> states:</p>
<blockquote>
<p><strong>“A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread.”</strong></p>
</blockquote>
<p>So if Thread B's volatile read observes the value <code>true</code> written by Thread A, the relationship is:</p>
<pre><code class="language-text">Thread A                              Thread B

counter = 1
    │
    │ program order
    ▼
volatile ready = true ── synchronizes-with ──► read volatile ready == true
                                                   │
                                                   │ program order
                                                   ▼
                                               read counter
</code></pre>
<p>By transitivity of happens-before:</p>
<pre><code class="language-text">write counter
    ↓ happens-before
write volatile ready
    ↓ synchronizes-with
read volatile ready
    ↓ happens-before
read counter
</code></pre>
<p>Once B observes <code>ready == true</code>, the earlier <code>counter = 1</code> write must be visible to B. The following outcome is therefore forbidden:</p>
<pre><code class="language-text">ready == true
counter == 0
</code></pre>
<p>That gives us two guarantees:</p>
<ul>
<li><strong>Visibility</strong>: writes completed by A before the volatile write become visible to B after B observes that write;</li>
<li><strong>Ordering</strong>: the operations must be observed in a way that is consistent with the happens-before relationship.</li>
</ul>
<h3>1.2 volatile Does Not Make Compound Operations Atomic</h3>
<p>Consider:</p>
<pre><code class="language-java">volatile int counter = 0;

counter++;
</code></pre>
<p><code>counter++</code> is still a read-modify-write operation:</p>
<pre><code class="language-text">volatile read
      ↓
     add
      ↓
volatile write
</code></pre>
<p><code>volatile</code> constrains the visibility and ordering of the individual read and write, but it does not make the entire <code>read → add → write</code> sequence indivisible.</p>
<p>With multiple threads incrementing concurrently, updates can still be lost.</p>
<p>If you need an atomic RMW, use <code>AtomicInteger</code>, CAS, or a mutex.</p>
<hr />
<h2>2. How Does HotSpot Implement volatile?</h2>
<h3>2.1 Layers</h3>
<p>As in the Mutex and Atomic articles, first establish the implementation stack:</p>
<pre><code class="language-text">Java Source Code
Example: volatile boolean ready
Role: declare a field with volatile memory semantics
        │
        ▼
JVM Bytecode
Example: getfield / putfield (or getstatic / putstatic)
Field metadata: ACC_VOLATILE
Role: perform ordinary field access; volatility is carried by field metadata
        │
        ▼
JVM Implementation (HotSpot)
Example: field-&gt;is_volatile() / MO_SEQ_CST / MemBar
Role: preserve and implement volatile memory-ordering semantics
        │
        ▼
x86-64 Hardware
Example: Load / Store / Cache Coherence / Fence
Role: provide the hardware basis for Visibility and Ordering
</code></pre>
<p>This differs from both <code>synchronized</code> and <code>AtomicInteger</code>:</p>
<pre><code class="language-text">synchronized
  → dedicated monitorenter / monitorexit bytecodes

AtomicInteger
  → ordinary invokevirtual
  → no Atomic-specific bytecode

volatile
  → still uses getfield / putfield
  → no dedicated volatile-load / volatile-store bytecode
  → the volatile property is recorded in ACC_VOLATILE field metadata
</code></pre>
<h3>2.2 End-to-End Implementation Path for a volatile Write and Read</h3>
<p>Continue with the same <code>counter / ready</code> example:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/381076ab-d7e3-4326-a907-019d748a925e.png" alt="How the ordinary write to counter and the volatile write/read of ready pass through JVM Bytecode and HotSpot before reaching x86-64 Hardware." style="display:block;margin:0 auto" />

<p>The key point is not a particular machine instruction. HotSpot must recognize that <code>ready</code> is volatile and preserve its memory semantics all the way to the target architecture.</p>
<h3>2.3 Guarantees at the Runtime / Language Implementation Layer</h3>
<h4>2.3.1 Visibility</h4>
<p>HotSpot distinguishes ordinary fields from volatile fields while parsing field accesses. In the current C2 path, volatile accesses carry memory-order information such as <code>MO_SEQ_CST</code>, while ordinary fields use unordered access semantics.</p>
<p>Conceptually:</p>
<pre><code class="language-text">ordinary field
  → ordinary load / store

volatile field
  → volatile load / store
  → preserve cross-thread observable memory semantics
</code></pre>
<p>HotSpot therefore cannot optimize a volatile access as if it were an ordinary field access—for example, by eliminating, merging, or moving it across synchronization boundaries that matter.</p>
<h4>2.3.2 Ordering</h4>
<p>HotSpot must also constrain reordering around volatile accesses.</p>
<p>In C2's IR, the typical structure includes:</p>
<pre><code class="language-text">volatile store
  ← ordering constraints such as MemBarRelease

volatile load
  → ordering constraints such as MemBarAcquire
</code></pre>
<p>Cases that require stronger ordering—such as volatile store → volatile load—also require the corresponding StoreLoad / volatile-barrier constraint.</p>
<p>These are IR nodes and compiler-ordering constraints, not a one-to-one promise that every barrier becomes a separate CPU fence instruction.</p>
<p>Relevant OpenJDK sources include <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/opto/parse3.cpp"><code>parse3.cpp</code></a>, <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/opto/memnode.hpp"><code>memnode.hpp</code></a>, and <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/cpu/x86/templateTable_x86.cpp"><code>templateTable_x86.cpp</code></a>.</p>
<h3>2.4 Guarantees at the Hardware Layer</h3>
<p>At the hardware layer, these guarantees reduce to the same two capabilities introduced earlier in the series:</p>
<pre><code class="language-text">Visibility
  → Cache Coherence
  → other CPU cores cannot keep using an invalidated old cache line indefinitely

Ordering
  → CPU Memory Ordering + Fence / equivalent ordering constraints
  → preserve the memory-access order required at volatile boundaries
</code></pre>
<p>x86-64 already provides relatively strong memory ordering, so many Acquire / Release constraints do not require a standalone hardware fence instruction.</p>
<p>When a StoreLoad fence is required, HotSpot's Linux x86 path can use:</p>
<pre><code class="language-asm">lock; addl $0, 0(%rsp)
</code></pre>
<p>to provide a full-fence effect.</p>
<p>See OpenJDK <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/os_cpu/linux_x86/orderAccess_linux_x86.hpp"><code>orderAccess_linux_x86.hpp</code></a>.</p>
<hr />
<h2>3. Why Don't Go and CPython Have Java-style volatile?</h2>
<p>Go and Python do not expose Java's field-level <code>volatile</code> mechanism. The reasons are different, so consider them separately.</p>
<h3>3.1 Go: A Language Design Choice</h3>
<p>Effective Go summarizes one of Go's central concurrency design principles with a well-known rule:</p>
<blockquote>
<p><strong>“Do not communicate by sharing memory; instead, share memory by communicating.”</strong></p>
</blockquote>
<p>That reflects a language-design choice: <strong>synchronization should be expressed through explicit concurrency primitives rather than implicitly through field modifiers.</strong></p>
<p>Accordingly, Go does not provide a Java-style field-level <code>volatile</code> modifier. It expresses the same kinds of synchronization through channels, mutexes, and atomic operations.</p>
<p>For the use case closest to Java <code>volatile</code>, Go uses <code>sync/atomic</code>. The Go Memory Model states:</p>
<blockquote>
<p><strong>“This definition provides the same semantics as C++'s sequentially consistent atomics and Java's volatile variables.”</strong></p>
</blockquote>
<p>Use the same <code>counter / ready</code> example:</p>
<pre><code class="language-go">var counter int
var ready atomic.Bool

// Goroutine A
counter = 1
ready.Store(true)

// Goroutine B
if ready.Load() {
    fmt.Println(counter)
}
</code></pre>
<p>If <code>ready.Load()</code> observes the value written by <code>ready.Store(true)</code>, the two atomic operations establish a synchronized-before relationship:</p>
<pre><code class="language-text">Goroutine A                          Goroutine B

counter = 1
    │
    │ sequenced-before
    ▼
ready.Store(true) ── synchronized-before ──► ready.Load() == true
                                                 │
                                                 │ sequenced-before
                                                 ▼
                                             read counter
</code></pre>
<p>So once B observes <code>ready == true</code>, the earlier <code>counter = 1</code> write must be visible to B.</p>
<h3>3.2 CPython: Synchronization Remains a Runtime Responsibility</h3>
<p>Python does not expose Java-style <code>volatile</code>. The key reason is not syntax; synchronization has historically been a responsibility of the CPython runtime.</p>
<p>There are two main points.</p>
<p><strong>1. Traditional CPython uses the GIL to cover many Visibility / Ordering concerns</strong></p>
<p>The CPython C API documentation states:</p>
<blockquote>
<p><strong>“only a thread that holds the GIL may operate on Python objects or invoke Python’s C API.”</strong></p>
</blockquote>
<p>In traditional CPython, the GIL serializes many operations on Python objects.</p>
<p>Releasing and reacquiring the GIL also forms a thread-synchronization boundary, so in practice it addresses many of the Visibility and Ordering problems that <code>volatile</code> is used to solve.</p>
<p>The abstractions are still different:</p>
<pre><code class="language-text">Java volatile
  → field-level language semantics

CPython GIL
  → runtime-level synchronization mechanism
</code></pre>
<p><strong>2. Free-threaded CPython still uses runtime locks and atomics</strong></p>
<p>Making the GIL optional did not introduce field-level <code>volatile</code> semantics.</p>
<p>PEP 703 instead says:</p>
<blockquote>
<p><strong>“This PEP proposes using per-object locks...”</strong></p>
</blockquote>
<p>The runtime also uses atomic operations internally.</p>
<p>So the implementation shifts from:</p>
<pre><code class="language-text">GIL
</code></pre>
<p>to:</p>
<pre><code class="language-text">Per-object lock + atomic
</code></pre>
<p>Synchronization therefore remains a runtime responsibility rather than becoming part of Python's field-level language semantics.</p>
<p>At the application level, publication and observation are expressed through synchronization objects. The documentation for <code>threading.Event</code> says:</p>
<blockquote>
<p><strong>“one thread signals an event and other threads wait for it.”</strong></p>
</blockquote>
<p>Use the same <code>counter / ready</code> example:</p>
<pre><code class="language-python">counter = 0
ready = threading.Event()

# Thread A
counter = 1
ready.set()

# Thread B
ready.wait()
print(counter)
</code></pre>
<p>Here, <code>Event</code> plays the synchronization role that <code>ready</code> played in the Java example:</p>
<pre><code class="language-text">Thread A                         Thread B

counter = 1
    │
    ▼
ready.set()  ───────────────►  ready.wait() returns
                                  │
                                  ▼
                              read counter
</code></pre>
<p>So Python application code uses Event / Lock for synchronization, while lower-level atomics and memory ordering remain implementation details of the CPython runtime.</p>
<hr />
<h2>4. How volatile, Atomics, and Mutexes Differ</h2>
<p>The three abstractions operate at different granularities:</p>
<table>
<thead>
<tr>
<th></th>
<th><code>volatile</code></th>
<th>Atomic</th>
<th>Mutex</th>
</tr>
</thead>
<tbody><tr>
<td>Core problem</td>
<td>publish / observe shared state</td>
<td>one shared-state operation</td>
<td>a critical section</td>
</tr>
<tr>
<td>Atomicity</td>
<td>does not make compound RMW atomic</td>
<td>one atomic operation is indivisible</td>
<td>critical section is mutually exclusive</td>
</tr>
<tr>
<td>Visibility</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Ordering</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Typical use</td>
<td>initialization / configuration / stop flags; state publication</td>
<td>counters / Add / CAS / Swap</td>
<td>multi-step logic / multi-field updates</td>
</tr>
</tbody></table>
<p>A compact way to view the relationship is:</p>
<pre><code class="language-text">volatile
  → establish Visibility / Ordering between a publication and a later observation

Atomic
  → make one shared-state operation indivisible
  → also provide the corresponding Visibility / Ordering

Mutex
  → use lower-level primitives such as atomics to arbitrate lock state
  → then protect an entire critical section
</code></pre>
<p>These are not simply stronger and weaker versions of the same tool. They solve different problems at different granularities.</p>
<hr />
<h2>5. Next: From Mutexes to Read-Write Locks</h2>
<p>A Mutex allows only one execution unit to enter a critical section at a time.</p>
<p>If most operations only read shared state, making readers exclude one another is unnecessary.</p>
<p>The next article looks at read-write locks: multiple readers can proceed concurrently, while writers still retain exclusive access.</p>
<hr />
<blockquote>
<p>This article was first published on <a href="https://thinkerqaq.com/en/articles/concurrency-series-07-volatile/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=article_syndication">ThinkerQAQ's personal blog</a> and syndicated here by the author. The original article may be revised over time; please refer to the personal blog for the latest version.</p>
<p>© ThinkerQAQ. Original articles, notes and accompanying technical diagrams use <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>: noncommercial reuse and adaptations require attribution, change notices and ShareAlike; commercial use requires permission. Photographs and standalone illustrations are All Rights Reserved; third-party rights remain with their owners.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Concurrency Programming (6): How Atomic Operations Are Implemented — From Runtime to CPU]]></title><description><![CDATA[The previous article examined how atomic operations provide Atomicity, Visibility, and Ordering through the rules of language memory models. This article…]]></description><link>https://thinkerqaq.hashnode.dev/concurrency-programming-6-how-atomic-operations-are-implemented-from-runtime-to-cpu</link><guid isPermaLink="true">https://thinkerqaq.hashnode.dev/concurrency-programming-6-how-atomic-operations-are-implemented-from-runtime-to-cpu</guid><category><![CDATA[Python]]></category><category><![CDATA[Java]]></category><category><![CDATA[golang]]></category><category><![CDATA[concurrency]]></category><dc:creator><![CDATA[ThinkerQAQ]]></dc:creator><pubDate>Mon, 02 Nov 2026 11:52:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/4b2c5623-e2cb-44ce-9457-c79cbfbe2634.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Table of Contents</h2>
<ul>
<li><a href="#0-what-does-this-article-answer-next">0. What Does This Article Answer Next?</a></li>
<li><a href="#1-how-does-hotspot-implement-atomicinteger">1. How Does HotSpot Implement AtomicInteger?</a><ul>
<li><a href="#11-layers">1.1 Layers</a></li>
<li><a href="#12-end-to-end-implementation-path-for-atomicinteger">1.2 End-to-End Implementation Path for AtomicInteger</a></li>
<li><a href="#13-guarantees-at-the-runtime--language-implementation-layer">1.3 Guarantees at the Runtime / Language Implementation Layer</a><ul>
<li><a href="#131-atomicity">1.3.1 Atomicity</a></li>
<li><a href="#132-visibility-and-ordering">1.3.2 Visibility and Ordering</a></li>
</ul>
</li>
<li><a href="#14-guarantees-at-the-hardware-layer">1.4 Guarantees at the Hardware Layer</a></li>
</ul>
</li>
<li><a href="#2-how-is-syncatomic-implemented-in-go">2. How Is sync/atomic Implemented in Go?</a><ul>
<li><a href="#21-layers">2.1 Layers</a></li>
<li><a href="#22-end-to-end-implementation-path-for-atomicint64">2.2 End-to-End Implementation Path for atomic.Int64</a></li>
<li><a href="#23-guarantees-at-the-runtime--language-implementation-layer">2.3 Guarantees at the Runtime / Language Implementation Layer</a><ul>
<li><a href="#231-atomicity">2.3.1 Atomicity</a></li>
<li><a href="#232-visibility-and-ordering">2.3.2 Visibility and Ordering</a></li>
</ul>
</li>
<li><a href="#24-guarantees-at-the-hardware-layer">2.4 Guarantees at the Hardware Layer</a></li>
</ul>
</li>
<li><a href="#3-how-does-cpython-implement-internal-atomics">3. How Does CPython Implement Internal Atomics?</a><ul>
<li><a href="#31-layers">3.1 Layers</a></li>
<li><a href="#32-end-to-end-implementation-path-for-cpython-atomics">3.2 End-to-End Implementation Path for CPython Atomics</a></li>
<li><a href="#33-guarantees-at-the-runtime--language-implementation-layer">3.3 Guarantees at the Runtime / Language Implementation Layer</a><ul>
<li><a href="#331-atomicity">3.3.1 Atomicity</a></li>
<li><a href="#332-visibility-and-ordering">3.3.2 Visibility and Ordering</a></li>
</ul>
</li>
<li><a href="#34-guarantees-at-the-hardware-layer">3.4 Guarantees at the Hardware Layer</a></li>
</ul>
</li>
<li><a href="#4-the-common-pattern-across-the-three-implementations">4. The Common Pattern Across the Three Implementations</a><ul>
<li><a href="#41-how-does-a-single-variable-update-become-indivisible--atomic-rmw">4.1 How Does a Single-variable Update Become Indivisible? — Atomic RMW</a></li>
<li><a href="#42-what-happens-when-a-conditional-update-fails--cas--retry">4.2 What Happens When a Conditional Update Fails? — CAS / Retry</a></li>
<li><a href="#43-why-can-atomics-also-establish-visibility-and-ordering">4.3 Why Can Atomics Also Establish Visibility and Ordering?</a></li>
<li><a href="#44-how-do-atomics-differ-from-mutexes">4.4 How Do Atomics Differ from Mutexes?</a><ul>
<li><a href="#441-one-state-update-two-approaches">4.4.1 One State Update, Two Approaches</a></li>
<li><a href="#442-why-cant-multiple-atomics-replace-one-mutex">4.4.2 Why Can't Multiple Atomics Replace One Mutex?</a></li>
<li><a href="#443-which-should-you-use">4.4.3 Which Should You Use?</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#5-next-volatile">5. Next: volatile</a></li>
</ul>
<hr />
<h2>0. What Does This Article Answer Next?</h2>
<p>The previous article examined how atomic operations provide Atomicity, Visibility, and Ordering through the rules of language memory models.</p>
<p>This article goes deeper: how do Java, Go, and CPython implement these guarantees at the runtime and CPU layers?</p>
<hr />
<h2>1. How Does HotSpot Implement AtomicInteger?</h2>
<h3>1.1 Layers</h3>
<pre><code class="language-text">Java Source Code
Example: AtomicInteger.incrementAndGet() / compareAndSet() / get()
Role: express atomic operations
        │
        ▼
JVM Bytecode
Example: invokevirtual AtomicInteger.incrementAndGet() / compareAndSet() / get()
Role: invoke AtomicInteger methods; there is no dedicated bytecode for atomic operations
        │
        ▼
JVM Implementation (HotSpot)
Example: Intrinsic / C1 / C2
Role: lower atomic operations to the target architecture
        │
        ▼
x86-64 Hardware
Example: Atomic Instruction / Cache Coherence / Fence
Role: provide the hardware basis for Atomicity, Visibility, and Ordering
</code></pre>
<h3>1.2 End-to-End Implementation Path for AtomicInteger</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/769e8cf4-86b1-4d38-a3c3-7880b1984db7.png" alt="How Atomic Add, CAS, and Load move from Java Source Code through ordinary method-call bytecode into HotSpot, where Unsafe intrinsics are recognized and lowered to x86-64 Hardware." style="display:block;margin:0 auto" />

<p>The main atomic operations can be expanded as follows:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/bfb6db30-76bd-41b2-9729-8463c1dbf053.png" alt="Atomic Add and CAS enter HotSpot intrinsics through Unsafe and are lowered by C1 or C2 to x86-64 atomic instructions; Load follows the volatile-read path." style="display:block;margin:0 auto" />

<h3>1.3 Guarantees at the Runtime / Language Implementation Layer</h3>
<h4>1.3.1 Atomicity</h4>
<p>At the Runtime / Language Implementation layer, Atomicity is preserved through two kinds of indivisible operations:</p>
<pre><code class="language-text">Atomic Add
  → Unsafe.getAndAddInt
  → HotSpot Intrinsic / C1 / C2
  → Atomic Get-And-Add

CAS
  → Unsafe.compareAndSetInt
  → HotSpot Intrinsic / C1 / C2
  → Atomic CAS
</code></pre>
<p>HotSpot preserves their atomic semantics through compilation and lowering; the target architecture then provides the concrete machine instructions.</p>
<h4>1.3.2 Visibility and Ordering</h4>
<p>Visibility and Ordering depend on HotSpot preserving these memory effects:</p>
<pre><code class="language-text">Write side: Atomic Add / CAS
Read side: volatile read
</code></pre>
<p>The JMM / VarHandle memory effects define the memory semantics of these operations. HotSpot must preserve the corresponding ordering constraints during compilation and optimization.</p>
<h3>1.4 Guarantees at the Hardware Layer</h3>
<p>At the hardware layer, CPU cache coherence and memory ordering implement these guarantees.</p>
<p>For Java, the mapping to the hardware model in the first article is:</p>
<pre><code class="language-text">Atomicity
  → Atomic Instruction
  → HotSpot / x86-64: LOCK XADD / LOCK CMPXCHG

Visibility
  → Cache Coherence
  → Typical x86-64 CPUs: MESI-family (e.g. MESIF / MOESI)

Ordering
  → Fence / equivalent ordering constraints
  → x86-64 memory ordering + LOCKed RMW / Load rules
</code></pre>
<p>For implementation details, see OpenJDK's <a href="https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/concurrent/atomic/AtomicInteger.java"><code>AtomicInteger.java</code></a>, <a href="https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/jdk/internal/misc/Unsafe.java"><code>Unsafe.java</code></a>, <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/classfile/vmIntrinsics.hpp"><code>vmIntrinsics.hpp</code></a>, <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/c1/c1_GraphBuilder.cpp"><code>c1_GraphBuilder.cpp</code></a>, and <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/opto/c2compiler.cpp"><code>c2compiler.cpp</code></a>.</p>
<hr />
<h2>2. How Is sync/atomic Implemented in Go?</h2>
<h3>2.1 Layers</h3>
<pre><code class="language-text">Go Source Code
Example: atomic.Int64.Add() / CompareAndSwap() / Load()
Role: express atomic operations
        │
        ▼
Go Implementation
Example: sync/atomic + internal/runtime/atomic
Role: implement the atomic API and target-specific atomic primitives
        │
        ▼
x86-64 Hardware
Example: Atomic Instruction / Cache Coherence / Fence
Role: provide the hardware basis for Atomicity, Visibility, and Ordering
</code></pre>
<h3>2.2 End-to-End Implementation Path for atomic.Int64</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/e60a9592-3ae9-4dab-bc30-d3fa41dd74a4.png" alt="How Atomic Add, CAS, and Load move from Go Source Code through Go Implementation to x86-64 Hardware." style="display:block;margin:0 auto" />

<p>The main atomic operations can be expanded as follows:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/2c7bdc14-7b41-4ead-8ac2-2792e1175801.png" alt="Atomic Add, CAS, and Load pass from atomic.Int64 through sync/atomic and internal/runtime/atomic to amd64 instructions." style="display:block;margin:0 auto" />

<h3>2.3 Guarantees at the Runtime / Language Implementation Layer</h3>
<h4>2.3.1 Atomicity</h4>
<p>At the Runtime / Language Implementation layer, Atomicity is represented by Go's atomic primitives:</p>
<pre><code class="language-text">Atomic Add
  → sync/atomic
  → internal/runtime/atomic.Xadd64

CAS
  → sync/atomic
  → internal/runtime/atomic.Cas64
</code></pre>
<p>These operations already carry indivisible atomic semantics at this layer; the target architecture provides the concrete machine instructions.</p>
<h4>2.3.2 Visibility and Ordering</h4>
<p>Visibility and Ordering depend on the Go implementation preserving atomic semantics:</p>
<pre><code class="language-text">Write side: Xadd64 / Cas64
Read side: Load64
</code></pre>
<p>The Go Memory Model defines the synchronization and ordering semantics of atomic operations. The compiler and Runtime must preserve those semantics.</p>
<h3>2.4 Guarantees at the Hardware Layer</h3>
<p>At the hardware layer, CPU cache coherence and memory ordering implement these guarantees.</p>
<p>For Go, the mapping to the hardware model in the first article is:</p>
<pre><code class="language-text">Atomicity
  → Atomic Instruction
  → Go / x86-64: LOCK XADDQ / LOCK CMPXCHGQ

Visibility
  → Cache Coherence
  → Typical x86-64 CPUs: MESI-family (e.g. MESIF / MOESI)

Ordering
  → Fence / equivalent ordering constraints
  → x86-64 memory ordering + LOCKed RMW / Load rules
</code></pre>
<p>For implementation details, see <a href="https://github.com/golang/go/blob/master/src/sync/atomic/type.go"><code>sync/atomic/type.go</code></a>, <a href="https://github.com/golang/go/blob/master/src/sync/atomic/asm.s"><code>sync/atomic/asm.s</code></a>, and <a href="https://github.com/golang/go/blob/master/src/internal/runtime/atomic/atomic_amd64.s"><code>internal/runtime/atomic/atomic_amd64.s</code></a>.</p>
<hr />
<h2>3. How Does CPython Implement Internal Atomics?</h2>
<p>Python's standard library has no general-purpose integer atomic API equivalent to <code>AtomicInteger</code> or <code>atomic.Int64</code>, so this section starts inside the CPython runtime.</p>
<h3>3.1 Layers</h3>
<pre><code class="language-text">CPython Runtime Source Code
Example: internal shared-state updates
Role: call internal atomic operations
        │
        ▼
CPython Implementation
Example: _Py_atomic_* + GCC / Clang __atomic_*
Role: implement atomic operations and memory-order semantics, then lower them to the target architecture
        │
        ▼
x86-64 Hardware
Example: Atomic Instruction / Cache Coherence / Fence
Role: provide the hardware basis for Atomicity, Visibility, and Ordering
</code></pre>
<h3>3.2 End-to-End Implementation Path for CPython Atomics</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/72f666f7-f18d-4bcf-b757-bd818a9934d2.png" alt="How Atomic Add, CAS, and Load move from CPython Runtime Source Code through CPython Implementation to x86-64 Hardware." style="display:block;margin:0 auto" />

<p>The main internal atomic operations can be expanded as follows:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/96194fab-b257-4736-977a-30ed7328a7bd.png" alt="Atomic Add, Compare-Exchange, and Load map from _Py_atomic to GCC / Clang __atomic builtins, which the compiler lowers to target-architecture operations." style="display:block;margin:0 auto" />

<h3>3.3 Guarantees at the Runtime / Language Implementation Layer</h3>
<h4>3.3.1 Atomicity</h4>
<p>At the Runtime / Language Implementation layer, CPython expresses Atomicity through its internal atomic abstraction and the compiler's atomic builtins:</p>
<pre><code class="language-text">Atomic Add
  → _Py_atomic_add_*
  → __atomic_fetch_add

CAS
  → _Py_atomic_compare_exchange_*
  → __atomic_compare_exchange_n
</code></pre>
<p>These operations carry explicit atomic semantics before the compiler selects the final instruction sequence for the target architecture.</p>
<h4>3.3.2 Visibility and Ordering</h4>
<p>Visibility and Ordering correspond to the atomic write and read boundaries in the flowchart:</p>
<pre><code class="language-text">Write side: Atomic Add / CAS
Read side: Atomic Load
</code></pre>
<p><code>_Py_atomic_*</code> passes the memory order to GCC / Clang's <code>__atomic_*</code> builtins, preserving the corresponding ordering semantics at the implementation layer.</p>
<h3>3.4 Guarantees at the Hardware Layer</h3>
<p>The compiler then maps these atomic operations and memory orders to the target architecture.</p>
<p>For CPython, the mapping to the hardware model in the first article is:</p>
<pre><code class="language-text">Atomicity
  → Atomic Instruction
  → x86-64: LOCK XADD / LOCK CMPXCHG

Visibility
  → Cache Coherence
  → Typical x86-64 CPUs: MESI-family (e.g. MESIF / MOESI)

Ordering
  → Fence / equivalent ordering constraints
  → x86-64 ordering rules + compiler-selected instruction sequence
</code></pre>
<p>This describes the CPython runtime's implementation; it does not turn those details into a memory model for Python application code.</p>
<p>For implementation details, see <a href="https://github.com/python/cpython/blob/main/Include/cpython/pyatomic.h"><code>pyatomic.h</code></a> and <a href="https://github.com/python/cpython/blob/main/Include/cpython/pyatomic_gcc.h"><code>pyatomic_gcc.h</code></a>.</p>
<hr />
<h2>4. The Common Pattern Across the Three Implementations</h2>
<p>After examining Java, Go, and CPython, we can set aside the specific APIs: atomic operations perform essentially the same tasks.</p>
<h3>4.1 How Does a Single-Variable Update Become Indivisible? — Atomic RMW</h3>
<p>An ordinary <code>read → modify → write</code> consists of three separate steps:</p>
<pre><code class="language-text">Load
  ↓
Modify
  ↓
Store
</code></pre>
<p>Other execution units can interleave with those steps. For example, both CPUs can read the old value and overwrite one another's increment:</p>
<pre><code class="language-text">CPU A                       CPU B
Load 0                      Load 0
Add 1                       Add 1
Store 1                     Store 1
</code></pre>
<p>Atomic Add, Exchange, CAS, and other atomic RMW operations make the shared-state update indivisible:</p>
<pre><code class="language-text">Atomic RMW
   ├── Add
   ├── Swap
   └── CAS
</code></pre>
<p>At the lowest level, CPU atomic instructions carry out these operations.</p>
<h3>4.2 What Happens When a Conditional Update Fails? — CAS / Retry</h3>
<p>With Atomic Add, two competing increments both take effect, regardless of their order:</p>
<pre><code class="language-text">counter = 0
CPU A                       CPU B
Atomic Add
0 → 1
                            Atomic Add
                            1 → 2
</code></pre>
<p>CAS behaves differently: when both CPUs compare against the same expected value, only one can succeed:</p>
<pre><code class="language-text">CPU A                       CPU B
CAS 0 → 1                   CAS 0 → 1
    │                           │
    ▼                           ▼
  success                     failed
</code></pre>
<p>When CAS fails, the higher-level code decides what to do:</p>
<pre><code class="language-text">CAS
 │
 ├── success ──&gt; done
 │
 └── failed
       │
       ├── return false
       │
       └── reload → retry
</code></pre>
<p>Atomic operations therefore do not follow the fixed <code>Spin → Park → Wakeup</code> path of a mutex. Add completes an atomic update directly; CAS returns failure when it fails, leaving the decision to retry to higher-level code.</p>
<h3>4.3 Why Can Atomics Also Establish Visibility and Ordering?</h3>
<p>Atomicity guarantees only that this update cannot be split apart.</p>
<p>Ordinary reads and writes around an atomic operation must also satisfy the memory ordering specified by the higher layers:</p>
<pre><code class="language-text">earlier ordinary writes
      │
      ▼
Atomic Update
      │
      ▼
Atomic Read
      │
      ▼
later ordinary reads
</code></pre>
<p>Three layers implement this relationship:</p>
<pre><code class="language-text">Language / Runtime Memory Semantics
        ↓
Compiler Ordering
        ↓
Hardware Memory Ordering + Cache Coherence
</code></pre>
<p>Despite their different APIs, all three implementations ultimately address the same three concerns:</p>
<pre><code class="language-text">make each update indivisible
        +
handle conflicts
        +
preserve the required order of surrounding memory accesses
</code></pre>
<hr />
<h3>4.4 How Do Atomics Differ from Mutexes?</h3>
<p>The simplest distinction is:</p>
<blockquote>
<p><strong>An atomic operation makes one shared-state operation indivisible; a Mutex protects a block of code.</strong></p>
</blockquote>
<h4>4.4.1 One State Update, Two Approaches</h4>
<p>Suppose all we need is to increment one counter.</p>
<p>With a Mutex:</p>
<pre><code class="language-text">Lock
  ↓
counter++
  ↓
Unlock
</code></pre>
<p>With an Atomic:</p>
<pre><code class="language-text">Atomic Add(counter, 1)
</code></pre>
<p>The Mutex approach is:</p>
<pre><code class="language-text">let one execution unit enter
        ↓
run the protected code
        ↓
leave the critical section
</code></pre>
<p>The Atomic approach is:</p>
<pre><code class="language-text">make this update itself
indivisible
</code></pre>
<p>If the problem is truly one Add, CAS, Swap, Load, or Store, there is no need to wrap that atomic operation in an additional critical section.</p>
<h4>4.4.2 Why Can't Multiple Atomics Replace One Mutex?</h4>
<p>Suppose one logical operation must update two pieces of state:</p>
<pre><code class="language-text">balance -= 100
count++
</code></pre>
<p>Making both variables atomic gives us:</p>
<pre><code class="language-text">Atomic balance -= 100
        ↓
        ← another thread may observe this intermediate state
        ↓
Atomic count++
</code></pre>
<p>Each update is indivisible on its own, but the two updates are not atomic as a group.</p>
<p>A Mutex can protect both updates as one critical section:</p>
<pre><code class="language-text">Lock
  ↓
balance -= 100
count++
  ↓
Unlock
</code></pre>
<p>Until that critical section completes, another contender cannot enter the same protected region.</p>
<p>So once the requirement changes from “update one state” to “these steps must complete together,” a Mutex usually expresses the intent more directly.</p>
<h4>4.4.3 Which Should You Use?</h4>
<p>Look at what needs protection:</p>
<pre><code class="language-text">one state operation
Add / CAS / Swap / Load / Store
        ↓
      Atomic
</code></pre>
<pre><code class="language-text">a block of logic
read → decide → update multiple states
        ↓
      Mutex
</code></pre>
<p>The two are not independent. A Mutex commonly uses atomic operations or CAS internally to decide who acquires the lock:</p>
<pre><code class="language-text">Atomic / CAS
    ↓
compete for lock state
    ↓
Mutex
    ↓
protect a critical section
</code></pre>
<p>So the relationship can be summarized as:</p>
<pre><code class="language-text">Atomic: one shared-state operation
Mutex:  a critical section built on lower-level primitives such as atomic operations / CAS
</code></pre>
<hr />
<h2>5. Next: volatile</h2>
<p>Atomic operations make RMW updates such as <code>counter++</code> atomic.</p>
<p>If no RMW is needed, and the goal is simply to make one thread's writes visible to another in the correct order, we reach the problem addressed by <code>volatile</code>.</p>
<p>The next article returns to language-level rules to discuss Visibility and Ordering.</p>
<hr />
<blockquote>
<p>This article was first published on <a href="https://thinkerqaq.com/en/articles/concurrency-series-06-atomic-implementation/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=article_syndication">ThinkerQAQ's personal blog</a> and syndicated here by the author. The original article may be revised over time; please refer to the personal blog for the latest version.</p>
</blockquote>
<blockquote>
<p>© ThinkerQAQ. Original articles, notes and accompanying technical diagrams use CC BY-NC-SA 4.0 (<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">https://creativecommons.org/licenses/by-nc-sa/4.0/</a>): noncommercial reuse and adaptations require attribution, change notices and ShareAlike; commercial use requires permission. Photographs and standalone illustrations are All Rights Reserved; third-party rights remain with their owners. <a href="https://thinkerqaq.com/en/articles/concurrency-series-06-atomic-implementation/">Original article</a></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Concurrency Programming (5): Atomics — Atomicity, Visibility, and Ordering at the Language Level]]></title><description><![CDATA[This article discusses Atomics directly from the rules defined by language memory models and public APIs. Start with the rules…]]></description><link>https://thinkerqaq.hashnode.dev/concurrency-programming-5-atomics-atomicity-visibility-and-ordering-at-the-language-level</link><guid isPermaLink="true">https://thinkerqaq.hashnode.dev/concurrency-programming-5-atomics-atomicity-visibility-and-ordering-at-the-language-level</guid><category><![CDATA[Python]]></category><category><![CDATA[Java]]></category><category><![CDATA[golang]]></category><category><![CDATA[concurrency]]></category><dc:creator><![CDATA[ThinkerQAQ]]></dc:creator><pubDate>Thu, 29 Oct 2026 10:57:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/7592dbad-4b1f-4e90-a797-86c38bf53399.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Table of Contents</h2>
<ul>
<li><a href="#0-continue-from-the-previous-article">0. Continue from the Previous Article</a></li>
<li><a href="#1-what-guarantees-do-the-atomic-rules-in-each-language-provide">1. What Guarantees Do the Atomic Rules in Each Language Provide?</a><ul>
<li><a href="#11-java-atomicinteger">1.1 Java: AtomicInteger</a></li>
<li><a href="#12-go-syncatomic">1.2 Go: sync/atomic</a></li>
<li><a href="#13-cpython-no-symmetric-application-level-atomic-api">1.3 CPython: No Symmetric Application-level Atomic API</a></li>
</ul>
</li>
<li><a href="#2-next-how-are-atomics-implemented">2. Next: How Are Atomics Implemented?</a></li>
</ul>
<hr />
<h2>0. Continue from the Previous Article</h2>
<p>This article discusses Atomics directly from the rules defined by language memory models and public APIs.</p>
<hr />
<h2>1. What Guarantees Do the Atomic Rules in Each Language Provide?</h2>
<h3>1.1 Java: AtomicInteger</h3>
<p>Start with the rules.</p>
<p><a href="https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/atomic/AtomicInteger.html#incrementAndGet()"><code>AtomicInteger.incrementAndGet()</code></a> states:</p>
<blockquote>
<p><strong>“Atomically increments the current value, with memory effects as specified by</strong> <code>VarHandle.getAndAdd</code><strong>.”</strong></p>
</blockquote>
<p>This is the Atomicity guarantee: reading the old value, adding one, and writing the result back happen as one indivisible atomic read-modify-write operation.</p>
<p>For Visibility and Ordering, ordinary reads and writes through <code>AtomicInteger</code> have volatile semantics: <code>get()</code> uses <code>VarHandle.getVolatile</code>, while <code>set()</code> uses <code>VarHandle.setVolatile</code>.</p>
<p><a href="https://docs.oracle.com/javase/specs/jls/se25/html/jls-17.html#jls-17.4.4">JLS §17.4.4 Synchronization Order</a> states:</p>
<blockquote>
<p><strong>“A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread.”</strong></p>
</blockquote>
<p>So a volatile write to an Atomic variable can establish a synchronizes-with relationship with a later volatile read in another thread that observes that write.</p>
<p><a href="https://docs.oracle.com/javase/specs/jls/se25/html/jls-17.html#jls-17.4.5">JLS §17.4.5 Happens-before Order</a> further states:</p>
<blockquote>
<p><strong>“If one action happens-before another, then the first is visible to and ordered before the second.”</strong></p>
</blockquote>
<p>That happens-before relationship gives us both Visibility and Ordering: earlier writes become visible to later reads, and those operations must be observed in the order required by happens-before.</p>
<p>Now apply those rules to the same two examples used earlier in the series.</p>
<p><strong>Atomicity</strong></p>
<p>With an ordinary increment:</p>
<pre><code class="language-text">Thread A                    Thread B

count++                     count++
</code></pre>
<p>both threads can read the old value <code>0</code>, causing a lost update and leaving the final value at <code>count = 1</code>.</p>
<p>Now make the same update atomic:</p>
<pre><code class="language-java">AtomicInteger count = new AtomicInteger(0);

// Thread A
count.incrementAndGet();

// Thread B
count.incrementAndGet();
</code></pre>
<p>The two updates are applied to the same value one after the other:</p>
<pre><code class="language-text">Thread A                    Thread B

incrementAndGet()           incrementAndGet()
        │                           │
        ▼                           ▼
      0 → 1                       1 → 2
</code></pre>
<p>The final result is:</p>
<pre><code class="language-text">count = 2
</code></pre>
<p>Here, Atomicity comes from <code>incrementAndGet()</code> itself being an atomic RMW operation. Unlike a Mutex, it does not serialize an entire critical section.</p>
<p><strong>Visibility / Ordering</strong></p>
<p>Now return to the same <code>counter / ready</code> example. With plain reads and writes, observing <code>ready == true</code> alone does not establish the ordering needed to guarantee that the earlier <code>counter = 1</code> write is visible.</p>
<p>Make only <code>ready</code> atomic:</p>
<pre><code class="language-java">int counter = 0;
AtomicInteger ready = new AtomicInteger(0);

// Thread A
counter = 1;        // A1
ready.set(1);       // A2

// Thread B
if (ready.get() == 1) {     // B1
    System.out.println(counter); // B2
}
</code></pre>
<p>If B's <code>ready.get()</code> observes the value written by A's <code>ready.set(1)</code>, then:</p>
<pre><code class="language-text">Thread A                              Thread B

A1  counter = 1
        │
        │ program order
        ▼
A2  ready.set(1)
        │
        │ synchronizes-with
        ▼
                                    B1  ready.get() == 1
                                        │
                                        │ program order
                                        ▼
                                    B2  read counter
</code></pre>
<p>By happens-before transitivity:</p>
<pre><code class="language-text">A1 happens-before A2
A2 happens-before B1
B1 happens-before B2

        ↓

A1 happens-before B2
</code></pre>
<p>So once B observes <code>ready == 1</code> through the Atomic variable, the earlier <code>counter = 1</code> write is visible to B, and this result is not allowed:</p>
<pre><code class="language-text">ready == 1
counter == 0
</code></pre>
<p>That is how the same Atomic operation provides Visibility and Ordering in this example.</p>
<h3>1.2 Go: sync/atomic</h3>
<p>Start with the rules.</p>
<p><a href="https://pkg.go.dev/sync/atomic#Int64.Add"><code>atomic.Int64.Add</code></a> states:</p>
<blockquote>
<p><strong>“Add atomically adds delta to x and returns the new value.”</strong></p>
</blockquote>
<p>This is the Atomicity guarantee: <code>Add(1)</code> performs the read, increment, and write-back as one atomic update.</p>
<p>For Visibility and Ordering, <a href="https://go.dev/ref/mem#atomic">The Go Memory Model - Atomic Values</a> states:</p>
<blockquote>
<p><strong>“If the effect of an atomic operation A is observed by atomic operation B, then A is synchronized before B.”</strong></p>
</blockquote>
<p>In other words, if B's atomic operation observes the result of A's atomic operation, A is synchronized-before B.</p>
<p>The same section also states that all atomic operations behave as though they were in:</p>
<blockquote>
<p><strong>“some sequentially consistent order”</strong></p>
</blockquote>
<p>The Go Memory Model defines happens-before as the transitive closure of sequenced-before and synchronized-before.</p>
<p>So atomic operations do more than update values indivisibly. When one atomic operation observes another, they can connect the ordinary memory operations around them into a happens-before chain, providing Visibility and Ordering as well.</p>
<p>Now apply those rules to the same two examples.</p>
<p><strong>Atomicity</strong></p>
<p>A plain <code>count++</code> can lose an update. Replace it with:</p>
<pre><code class="language-go">var count atomic.Int64

// Goroutine A
count.Add(1)

// Goroutine B
count.Add(1)
</code></pre>
<p>Neither update is lost:</p>
<pre><code class="language-text">Goroutine A                 Goroutine B

Add(1)                      Add(1)
  │                           │
  ▼                           ▼
0 → 1                       1 → 2
</code></pre>
<p>The final result is:</p>
<pre><code class="language-text">count = 2
</code></pre>
<p>Here, Atomicity comes directly from the atomic semantics of <code>Add(1)</code>.</p>
<p><strong>Visibility / Ordering</strong></p>
<p>Now return to the same <code>counter / ready</code> example, making only <code>ready</code> atomic:</p>
<pre><code class="language-go">var counter int
var ready atomic.Bool

// Goroutine A
counter = 1          // A1
ready.Store(true)    // A2

// Goroutine B
if ready.Load() {    // B1
    fmt.Println(counter) // B2
}
</code></pre>
<p>If B's <code>ready.Load()</code> observes the value written by A's <code>ready.Store(true)</code>, then:</p>
<pre><code class="language-text">Goroutine A                           Goroutine B

A1  counter = 1
        │
        │ sequenced-before
        ▼
A2  ready.Store(true)
        │
        │ synchronized-before
        ▼
                                    B1  ready.Load() == true
                                        │
                                        │ sequenced-before
                                        ▼
                                    B2  read counter
</code></pre>
<p>Therefore:</p>
<pre><code class="language-text">A1 happens-before A2
A2 happens-before B1
B1 happens-before B2

        ↓

A1 happens-before B2
</code></pre>
<p>So once B observes <code>ready == true</code> through the Atomic variable, the earlier <code>counter = 1</code> write is visible to B, and this result is not allowed:</p>
<pre><code class="language-text">ready == true
counter == 0
</code></pre>
<p>That is how Go Atomics provide Visibility and Ordering in this example.</p>
<h3>1.3 CPython: No Symmetric Application-level Atomic API</h3>
<p>Python's standard library currently has no general-purpose integer Atomic API equivalent to Java's <code>AtomicInteger</code> or Go's <code>atomic.Int64</code>.</p>
<p>Application code therefore has no corresponding set of public Atomic rules to cite. An ordinary <code>counter += 1</code> is not a language-defined Atomic API. The existence of the GIL in a particular CPython configuration, or the use of <code>_Py_atomic_*</code> inside the Runtime, does not turn it into a cross-implementation guarantee of Atomicity, Visibility, or Ordering for application code.</p>
<p>CPython itself needs Atomics for shared Runtime state. How <code>_Py_atomic_*</code> reaches the compiler and CPU is an implementation question for the next article, not part of Python's public application-level Atomic semantics.</p>
<hr />
<h2>2. Next: How Are Atomics Implemented?</h2>
<p>This article stops at the language level: Java and Go define Atomicity, Visibility, and Ordering through public Atomic APIs and their memory models, while Python application code has no equivalent general-purpose Atomic API.</p>
<p>The next article turns those language-level rules into implementation-level problems by following Java <code>AtomicInteger</code>, Go <code>sync/atomic</code>, and CPython's internal <code>_Py_atomic_*</code> path from the Runtime down to the CPU.</p>
<hr />
<blockquote>
<p>This article was first published on <a href="https://thinkerqaq.com/en/articles/concurrency-series-05-atomic-cas/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=article_syndication">ThinkerQAQ's personal blog</a> and syndicated here by the author. The original article may be revised over time; please refer to the personal blog for the latest version.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Concurrency Programming (4): How Mutexes Are Implemented — From Runtime to CPU]]></title><description><![CDATA[The previous article used language-memory-model rules to explain how a mutex provides Atomicity, Visibility, and Ordering. This article goes one layer…]]></description><link>https://thinkerqaq.hashnode.dev/concurrency-programming-4-how-mutexes-are-implemented-from-runtime-to-cpu</link><guid isPermaLink="true">https://thinkerqaq.hashnode.dev/concurrency-programming-4-how-mutexes-are-implemented-from-runtime-to-cpu</guid><category><![CDATA[Python]]></category><category><![CDATA[Java]]></category><category><![CDATA[golang]]></category><category><![CDATA[concurrency]]></category><category><![CDATA[mutex]]></category><dc:creator><![CDATA[ThinkerQAQ]]></dc:creator><pubDate>Tue, 27 Oct 2026 10:39:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/eefae8a2-0749-4840-b144-adad5fa79555.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Table of Contents</h2>
<ul>
<li><a href="#0-what-does-this-article-answer-next">0. What Does This Article Answer Next?</a></li>
<li><a href="#1-how-does-hotspot-implement-synchronized">1. How Does HotSpot Implement synchronized?</a><ul>
<li><a href="#11-layers">1.1 Layers</a></li>
<li><a href="#12-end-to-end-implementation-path-for-synchronized">1.2 End-to-End Implementation Path for synchronized</a></li>
<li><a href="#13-guarantees-at-the-runtime--language-implementation-layer">1.3 Guarantees at the Runtime / Language Implementation Layer</a><ul>
<li><a href="#131-atomicity">1.3.1 Atomicity</a></li>
<li><a href="#132-visibility-and-ordering">1.3.2 Visibility and Ordering</a></li>
</ul>
</li>
<li><a href="#14-guarantees-at-the-hardware-layer">1.4 Guarantees at the Hardware Layer</a></li>
</ul>
</li>
<li><a href="#2-how-is-syncmutex-implemented-in-go">2. How Is sync.Mutex Implemented in Go?</a><ul>
<li><a href="#21-layers">2.1 Layers</a></li>
<li><a href="#22-end-to-end-implementation-path-for-syncmutex">2.2 End-to-End Implementation Path for sync.Mutex</a></li>
<li><a href="#23-guarantees-at-the-runtime--language-implementation-layer">2.3 Guarantees at the Runtime / Language Implementation Layer</a><ul>
<li><a href="#231-atomicity">2.3.1 Atomicity</a></li>
<li><a href="#232-visibility-and-ordering">2.3.2 Visibility and Ordering</a></li>
</ul>
</li>
<li><a href="#24-guarantees-at-the-hardware-layer">2.4 Guarantees at the Hardware Layer</a></li>
</ul>
</li>
<li><a href="#3-how-does-cpython-implement-threadinglock">3. How Does CPython Implement threading.Lock?</a><ul>
<li><a href="#31-layers">3.1 Layers</a></li>
<li><a href="#32-end-to-end-implementation-path-for-threadinglock">3.2 End-to-End Implementation Path for threading.Lock</a></li>
<li><a href="#33-guarantees-at-the-runtime--language-implementation-layer">3.3 Guarantees at the Runtime / Language Implementation Layer</a><ul>
<li><a href="#331-atomicity">3.3.1 Atomicity</a></li>
<li><a href="#332-visibility-and-ordering">3.3.2 Visibility and Ordering</a></li>
</ul>
</li>
<li><a href="#34-guarantees-at-the-hardware-layer">3.4 Guarantees at the Hardware Layer</a></li>
</ul>
</li>
<li><a href="#4-the-common-pattern-across-the-three-implementations">4. The Common Pattern Across the Three Implementations</a><ul>
<li><a href="#41-who-gets-in--atomically-updating-lock-state">4.1 Who Gets In? — Atomically Updating Lock State</a></li>
<li><a href="#42-what-happens-if-you-dont-get-the-lock--spin--park--wakeup">4.2 What Happens If You Don't Get the Lock? — Spin / Park / Wakeup</a></li>
<li><a href="#43-after-acquiring-the-lock-why-are-the-previous-holders-writes-visible">4.3 After Acquiring the Lock, Why Are the Previous Holder's Writes Visible?</a></li>
</ul>
</li>
<li><a href="#5-next-atomic">5. Next: Atomic</a></li>
</ul>
<hr />
<h2>0. What Does This Article Answer Next?</h2>
<p>The previous article used language-memory-model rules to explain how a mutex provides Atomicity, Visibility, and Ordering.</p>
<p>This article goes one layer deeper and follows how Java, Go, and CPython turn those guarantees into concrete behavior across the Runtime, OS Thread, and CPU layers.</p>
<hr />
<h2>1. How Does HotSpot Implement synchronized?</h2>
<h3>1.1 Layers</h3>
<p>First, establish the implementation stack for Java:</p>
<pre><code class="language-text">Java Source Code
Example: synchronized
Role: mark a synchronized region
        │
        ▼
JVM Bytecode
Example: monitorenter / monitorexit (synchronized blocks)
Role: encode Monitor entry / exit
        │
        ▼
JVM Implementation (HotSpot)
Example: Synchronization Runtime
Role: implement the Monitor semantics defined by the JVM
        │
        ▼
OS Thread / Thread Parking
Example: Platform Thread / ParkEvent
Role: carry Park / Wakeup when contention persists
        │
        ▼
x86-64 Hardware
Example: Atomic Instruction / Cache Coherence / Fence
Role: provide the hardware foundation for Atomicity, Visibility, and Ordering
</code></pre>
<h3>1.2 End-to-End Implementation Path for synchronized</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/da5b110c-afb2-4a7f-ac6e-f61b5c716fbc.png" alt="The main lock, contention, critical-section, and unlock path from Java Source Code through JVM Bytecode, HotSpot, OS Thread, and x86-64 Hardware." style="display:block;margin:0 auto" />

<p>The sequence diagram above shows only the main cross-layer path. Here, OS Thread represents the path that actually blocks a platform thread. Current HotSpot can first try to unmount a virtual thread, so not every contended <code>synchronized</code> operation blocks an OS thread.</p>
<p>The contention path inside the Runtime can be expanded further:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/6c031a69-f3b6-44b9-a200-c1811fb903ef.png" alt="monitorenter first tries the Lightweight Locking fast path. On failure it performs fast-lock spinning before inflating to ObjectMonitor, where contention continues through CAS, Spin, and Park." style="display:block;margin:0 auto" />

<h3>1.3 Guarantees at the Runtime / Language Implementation Layer</h3>
<h4>1.3.1 Atomicity</h4>
<p>Atomicity corresponds to the two atomic lock-acquisition attempts in the flowchart above:</p>
<pre><code class="language-text">Fast Path: CAS markWord lock bits [Acquire]
Inflated Monitor: CAS _owner [Acquire]
</code></pre>
<p>Both update lock state atomically. When multiple threads contend for the lock, only one can acquire it and enter the critical section.</p>
<h4>1.3.2 Visibility and Ordering</h4>
<p>Visibility and Ordering correspond to the lock boundaries in the flowchart:</p>
<pre><code class="language-text">Acquire lock: CAS ... [Acquire]
Release lock: release lock state [Release]
</code></pre>
<p>Together, Release and Acquire connect the two critical sections, ensuring that writes from the previous lock holder are observed by the next holder in the required order.</p>
<h3>1.4 Guarantees at the Hardware Layer</h3>
<p>At the hardware layer, these guarantees are realized through CPU Cache Coherence and memory-ordering constraints.</p>
<p>For Java, this corresponds to the hardware model introduced in the first article:</p>
<pre><code class="language-text">Atomicity
  → Atomic Instruction
  → HotSpot / x86-64: LOCK CMPXCHG

Visibility
  → Cache Coherence
  → Typical x86-64 CPUs: MESI-family (e.g. MESIF / MOESI)

Ordering
  → Fence
  → HotSpot / x86-64: LOCK ADDL $0, 0(%rsp)
</code></pre>
<p><code>LOCK ADDL $0, 0(%rsp)</code> is a concrete path HotSpot uses to implement a full fence on Linux/x86. This does not mean every <code>synchronized</code> operation executes an extra instance of that instruction: x86 memory-ordering rules and LOCKed RMW operations also contribute to the required ordering guarantees.</p>
<p>For implementation details, see OpenJDK's <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/oops/markWord.hpp"><code>markWord.hpp</code></a>, <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/objectMonitor.cpp"><code>objectMonitor.cpp</code></a>, <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/objectMonitor.inline.hpp"><code>objectMonitor.inline.hpp</code></a>, <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/opto/graphKit.cpp"><code>graphKit.cpp</code></a>, and <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/os_cpu/linux_x86/orderAccess_linux_x86.hpp"><code>orderAccess_linux_x86.hpp</code></a>.</p>
<hr />
<h2>2. How Is sync.Mutex Implemented in Go?</h2>
<h3>2.1 Layers</h3>
<pre><code class="language-text">Go Source Code
Example: sync.Mutex / Lock() / Unlock()
Role: define a mutual-exclusion boundary
        │
        ▼
Go Implementation
Example: sync / internal/sync
Role: implement the Mutex state machine and fast / slow paths
        │
        ▼
Go Runtime / Scheduler
Example: runtime_SemacquireMutex / gopark / goready
Role: park / wake goroutines
        │
        ▼
OS Thread (M)
Example: M in the G-M-P model
Role: execute goroutines; gopark does not block M
        │
        ▼
x86-64 Hardware
Example: Atomic Instruction / Cache Coherence / Fence
Role: provide the hardware foundation for Atomicity, Visibility, and Ordering
</code></pre>
<h3>2.2 End-to-End Implementation Path for sync.Mutex</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/3f90f5a7-8abf-4dc6-afd0-7aae1bfc0d75.png" alt="How Lock and Unlock move through sync/internal/sync, the Go Runtime / Scheduler, OS Thread, and x86-64 Hardware for atomic lock acquisition, waiting, wakeup, and release." style="display:block;margin:0 auto" />

<p>The sequence diagram above shows only the main cross-layer path. The key difference from Java and CPython is that <code>sync.Mutex</code> waits through <code>gopark</code>: it parks the goroutine (G), not the OS thread (M), so that M can continue running another runnable goroutine.</p>
<p>The contention path inside <code>sync.Mutex</code> can be expanded further:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/42e89069-3532-4535-93bb-08d2bba0498a.png" alt="Lock first tries the fast-path CAS. On failure it enters lockSlow, may spin, update state, and wait on the runtime semaphore. Unlock atomically releases the lock and wakes a waiting goroutine when needed." style="display:block;margin:0 auto" />

<h3>2.3 Guarantees at the Runtime / Language Implementation Layer</h3>
<h4>2.3.1 Atomicity</h4>
<p>Atomicity corresponds to the atomic lock-state modifications in the flow above:</p>
<pre><code class="language-text">Acquire lock: CompareAndSwapInt32(&amp;state, 0, mutexLocked)
Release lock: AddInt32(&amp;state, -mutexLocked)
</code></pre>
<p>When multiple goroutines contend for the mutex, only one can atomically change <code>state</code> from unlocked to locked and enter the critical section.</p>
<p>On amd64, these atomic operations are implemented with <code>LOCK CMPXCHGL</code> and <code>LOCK XADDL</code>.</p>
<h4>2.3.2 Visibility and Ordering</h4>
<p>Visibility and Ordering come from the synchronization boundary formed by <code>Lock / Unlock</code>. After one holder completes <code>Unlock</code>, a goroutine that subsequently acquires the mutex with <code>Lock</code> can observe writes from the previous critical section in the required order.</p>
<h3>2.4 Guarantees at the Hardware Layer</h3>
<p>At the hardware layer, these guarantees are realized through CPU Cache Coherence, x86 memory ordering, and the ordering constraints of the atomic instructions themselves.</p>
<p>For Go, this corresponds to the hardware model introduced in the first article:</p>
<pre><code class="language-text">Atomicity
  → Atomic Instruction
  → Go / x86-64: LOCK CMPXCHGL / LOCK XADDL

Visibility
  → Cache Coherence
  → Typical x86-64 CPUs: MESI-family (e.g. MESIF / MOESI)

Ordering
  → Fence / equivalent ordering constraint
  → Go / x86-64: LOCK CMPXCHGL / LOCK XADDL
</code></pre>
<p>This amd64 Mutex path does not need an additional <code>MFENCE</code>; the LOCKed RMW operations already provide both the atomic state update and the ordering constraints required here.</p>
<p>For implementation details, see <a href="https://go.dev/src/internal/sync/mutex.go"><code>internal/sync/mutex.go</code></a>, <a href="https://go.dev/src/runtime/sema.go"><code>runtime/sema.go</code></a>, <a href="https://github.com/golang/go/blob/master/src/runtime/HACKING.md"><code>runtime/HACKING.md</code></a>, and <a href="https://go.dev/src/internal/runtime/atomic/atomic_amd64.s"><code>internal/runtime/atomic/atomic_amd64.s</code></a>.</p>
<hr />
<h2>3. How Does CPython Implement threading.Lock?</h2>
<p>This section focuses on the current CPython implementation.</p>
<h3>3.1 Layers</h3>
<pre><code class="language-text">Python Source Code
Example: threading.Lock / acquire() / release()
Role: declare a mutual-exclusion boundary
        │
        ▼
CPython Binding
Example: _thread.lock
Role: map the Python Lock API to CPython's lock implementation
        │
        ▼
CPython Implementation
Example: PyMutex / Parking Lot
Role: implement lock state and waiter queues
        │
        ▼
OS Thread / OS Wait Primitive
Example: WaitForMultipleObjects / sem_wait / pthread_cond_wait
Role: block / wake the thread when the Parking Lot waits
        │
        ▼
x86-64 Hardware
Example: Atomic Instruction / Cache Coherence / Fence
Role: provide the hardware foundation for Atomicity, Visibility, and Ordering
</code></pre>
<h3>3.2 End-to-End Implementation Path for threading.Lock</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/00b25f83-e2a6-42b1-b9c5-be0884554107.png" alt="How Lock acquire and release move through _thread, CPython Implementation, OS Thread, and x86-64 Hardware for atomic lock acquisition, waiting, wakeup, and release." style="display:block;margin:0 auto" />

<p>The sequence diagram above shows only the main cross-layer path. CPython's internal contention path can be expanded further:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/2b4d4dc4-cf59-424e-960e-0362766fee32.png" alt="acquire first tries the PyMutex fast path. On failure it may briefly spin when supported, then enters the Parking Lot. release either unlocks directly or wakes a waiting thread according to waiter state." style="display:block;margin:0 auto" />

<h3>3.3 Guarantees at the Runtime / Language Implementation Layer</h3>
<h4>3.3.1 Atomicity</h4>
<p>Atomicity comes from the atomic state changes that <code>PyMutex</code> performs on <code>_bits</code>:</p>
<pre><code class="language-text">Acquire lock: _Py_atomic_compare_exchange_uint8(..., _Py_LOCKED)
Release lock (no waiter): _Py_atomic_compare_exchange_uint8(..., _Py_UNLOCKED)
</code></pre>
<p>When waiters exist, the release path enters <code>_PyParkingLot_Unpark()</code>, whose callback atomically updates <code>_bits</code>.</p>
<p>When multiple threads contend for the lock, only one can successfully set <code>_Py_LOCKED</code> and enter the critical section.</p>
<h4>3.3.2 Visibility and Ordering</h4>
<p>Visibility and Ordering come from the synchronization boundary formed by <code>acquire() / release()</code>. In current CPython, the default atomic compare-exchange and store operations use <code>__ATOMIC_SEQ_CST</code> in the GCC / Clang implementation.</p>
<h3>3.4 Guarantees at the Hardware Layer</h3>
<p>At the hardware layer, these guarantees are realized through CPU Cache Coherence, x86 memory ordering, and those sequentially consistent atomic operations.</p>
<p>For current CPython, this corresponds to the hardware model introduced in the first article:</p>
<pre><code class="language-text">Atomicity
  → Atomic Instruction
  → CPython / x86-64: LOCK CMPXCHGB

Visibility
  → Cache Coherence
  → Typical x86-64 CPUs: MESI-family (e.g. MESIF / MOESI)

Ordering
  → Fence / equivalent ordering constraint
  → CPython / x86-64: SEQ_CST atomic
    (typically LOCK CMPXCHGB / memory XCHGB)
</code></pre>
<p>This likewise does not require every Lock operation to execute an additional <code>MFENCE</code>: on x86-64, LOCKed RMW operations and sequentially consistent atomics already provide the required ordering constraints.</p>
<p>For implementation details, see <a href="https://github.com/python/cpython/blob/main/Modules/_threadmodule.c"><code>Modules/_threadmodule.c</code></a>, <a href="https://github.com/python/cpython/blob/main/Python/lock.c"><code>Python/lock.c</code></a>, <a href="https://github.com/python/cpython/blob/main/Python/parking_lot.c"><code>Python/parking_lot.c</code></a>, and <a href="https://github.com/python/cpython/blob/main/Include/cpython/pyatomic_gcc.h"><code>Include/cpython/pyatomic_gcc.h</code></a>.</p>
<hr />
<h2>4. The Common Pattern Across the Three Implementations</h2>
<p>With Java, Go, and CPython in view, we can strip away the implementation-specific details and focus on the three problems every mutex implementation must solve.</p>
<h3>4.1 Who Gets In? — Atomically Updating Lock State</h3>
<p>Suppose a lock is represented by a single state variable:</p>
<pre><code class="language-text">0 = unlocked
1 = locked
</code></pre>
<p>A and B must not both read <code>0</code> and then both write <code>1</code>; otherwise, each would believe it had acquired the lock.</p>
<p>Therefore, checking the lock state and changing it to <code>locked</code> must be one atomic operation, such as Compare-And-Swap:</p>
<pre><code class="language-text">Compare-And-Swap(lock_state, 0, 1)
</code></pre>
<p>When two execution units compete:</p>
<pre><code class="language-text">CPU A                         CPU B

CAS 0 -&gt; 1                   CAS 0 -&gt; 1
    │                             │
    ▼                             ▼
  success                       failed
</code></pre>
<p>Only one contender can successfully modify the lock state. A mutex builds on this small hardware atomic primitive to protect an arbitrary critical section such as <code>counter++</code>.</p>
<h3>4.2 What Happens If You Don't Get the Lock? — Spin / Park / Wakeup</h3>
<p>After CAS fails, an execution unit should not spin on the lock state forever.</p>
<p>A common path is:</p>
<pre><code class="language-text">try atomic lock acquisition
        │
        ├── success ──&gt; enter critical section
        │
        └── failure
              │
              ├── Spin briefly
              │      │
              │      └── retry
              │
              └── Park / wait
                         │
                         └── wake after lock release
</code></pre>
<p>Spinning works well when the expected wait is very short because it avoids immediately entering a blocking path. If contention persists, parking prevents the waiter from continuously consuming CPU time.</p>
<h3>4.3 After Acquiring the Lock, Why Are the Previous Holder's Writes Visible?</h3>
<p>The first two questions answer who gets the lock and what happens when acquisition fails. The third is about memory visibility: why are writes from the previous holder's critical section visible to the next holder?</p>
<p>Across all three implementations, the common mechanism is a <strong>Release / Acquire</strong> synchronization boundary between lock release and the next lock acquisition:</p>
<pre><code class="language-text">previous lock holder
critical-section writes
    │
    ▼
unlock [Release]
    │
    ▼
lock [Acquire]
    │
    ▼
next lock holder
critical-section reads
</code></pre>
<p>Release constrains memory operations before the unlock, while Acquire constrains memory operations after the subsequent lock.</p>
<p>Therefore, when the next execution unit acquires the same lock, writes from the earlier critical section are ordered before reads in the later one. This is the memory-ordering guarantee that every mutex implementation must establish.</p>
<p>The first three sections showed how each Runtime and CPU pair realizes this Release / Acquire boundary.</p>
<hr />
<h2>5. Next: Atomic</h2>
<p>A mutex only needs a small amount of atomic state to enforce mutual exclusion over a critical section of arbitrary size.</p>
<p>Atomic narrows the scope further: instead of protecting an entire critical section, it makes a single read, write, or update of shared state indivisible.</p>
<p>The next article examines how this smaller unit of synchronization is implemented.</p>
<hr />
<blockquote>
<p>This article was first published on <a href="https://thinkerqaq.com/en/articles/concurrency-series-04-mutex-implementation/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=article_syndication">ThinkerQAQ's personal blog</a> and syndicated here by the author. The original article may be revised over time; please refer to the personal blog for the latest version.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Concurrency Programming (3): Mutexes — Atomicity, Visibility, and Ordering at the Language Level]]></title><description><![CDATA[This article examines Mutexes through the rules defined by language memory models. Java's synchronized locks the monitor associated with an object. JLS…]]></description><link>https://thinkerqaq.hashnode.dev/concurrency-programming-3-mutexes-atomicity-visibility-and-ordering-at-the-language-level</link><guid isPermaLink="true">https://thinkerqaq.hashnode.dev/concurrency-programming-3-mutexes-atomicity-visibility-and-ordering-at-the-language-level</guid><category><![CDATA[Python]]></category><category><![CDATA[Java]]></category><category><![CDATA[golang]]></category><category><![CDATA[concurrency]]></category><dc:creator><![CDATA[ThinkerQAQ]]></dc:creator><pubDate>Mon, 26 Oct 2026 11:01:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/fffbf501-95a3-41e2-ac80-9f12a08796ae.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Table of Contents</h2>
<ul>
<li><a href="#0-continue-from-the-previous-article">0. Continue from the Previous Article</a></li>
<li><a href="#1-what-guarantees-do-the-lock-rules-in-each-language-provide">1. What Guarantees Do the Lock Rules in Each Language Provide?</a><ul>
<li><a href="#11-java-synchronized-and-monitors">1.1 Java: synchronized and Monitors</a></li>
<li><a href="#12-go-syncmutex">1.2 Go: sync.Mutex</a></li>
<li><a href="#13-cpythonthreadinglock">1.3 CPython: threading.Lock</a></li>
</ul>
</li>
<li><a href="#2-next-how-is-a-mutex-implemented">2. Next: How Is a Mutex Implemented?</a></li>
</ul>
<hr />
<h2>0. Continue from the Previous Article</h2>
<p>This article examines Mutexes through the rules defined by language memory models.</p>
<hr />
<h2>1. What Guarantees Do the Lock Rules in Each Language Provide?</h2>
<h3>1.1 Java: synchronized and Monitors</h3>
<p>Java's <code>synchronized</code> locks the monitor associated with an object. <a href="https://docs.oracle.com/javase/specs/jls/se25/html/jls-17.html#jls-17.1">JLS §17.1</a> states:</p>
<blockquote>
<p><strong>“Only one thread at a time may hold a lock on a monitor.”</strong></p>
</blockquote>
<p>This rule provides the exclusion needed for atomicity. At most one thread can hold the monitor, so only that thread may execute the protected critical section while competitors wait.</p>
<p><a href="https://docs.oracle.com/javase/specs/jls/se25/html/jls-17.html#jls-17.4.5">JLS §17.4.5</a> also states:</p>
<blockquote>
<p><strong>“An unlock on a monitor happens-before every subsequent lock on that monitor.”</strong></p>
</blockquote>
<p>This rule provides both visibility and ordering.</p>
<p>For visibility, A's writes are before its <code>unlock</code>, while B's reads are after the subsequent <code>lock</code>. Through happens-before transitivity, the writes in A's critical section become visible to B.</p>
<p>For ordering, A's program order, the <code>unlock → lock</code> relationship, and B's program order form one happens-before chain. B therefore cannot observe a result such as <code>ready = true, counter = 0</code> when that would violate the established ordering.</p>
<p>Now reuse the same two examples from the previous article.</p>
<p><strong>Atomicity</strong></p>
<p>Previously, two threads executing:</p>
<pre><code class="language-text">Thread A                    Thread B

count++                     count++
</code></pre>
<p>could both read the old value <code>0</code> and leave the final value at <code>count = 1</code>.</p>
<p>Now place the same <code>count++</code> inside a critical section guarded by the same monitor:</p>
<pre><code class="language-java">synchronized (lock) {
    count++;
}
</code></pre>
<p>The execution is serialized:</p>
<pre><code class="language-text">initial: count = 0

Thread A                         Thread B

acquire lock
                                 try to acquire lock
                                 wait
read count -&gt; 0
compute count + 1 -&gt; 1
write count -&gt; 1
release lock
                                 acquire lock
                                 read count -&gt; 1
                                 compute count + 1 -&gt; 2
                                 write count -&gt; 2
                                 release lock
</code></pre>
<p>The final result is:</p>
<pre><code class="language-text">count = 2
</code></pre>
<p>The ordinary <code>count++</code> statement has not become an atomic CPU instruction. The monitor makes the entire critical section execute with mutual exclusion relative to other code using the same monitor.</p>
<p><strong>Visibility / Ordering</strong></p>
<p>The other example from the previous article was:</p>
<pre><code class="language-text">Thread A                    Thread B

counter = 1
ready = true                read ready == true
                                  │
                                  ▼
                             read counter
</code></pre>
<p>Now access that state through the same monitor:</p>
<pre><code class="language-java">// Thread A
synchronized (lock) {
    counter = 1;
    ready = true;
}

// Thread B
synchronized (lock) {
    if (ready) {
        System.out.println(counter);
    }
}
</code></pre>
<p>If Thread B acquires <code>lock</code> after Thread A releases it:</p>
<pre><code class="language-text">counter = 1
ready = true
    │
    ▼
unlock
    │
    │ happens-before
    ▼
lock
    │
    ▼
read ready == true
read counter == 1
</code></pre>
<p>So once B observes <code>ready == true</code> after that lock acquisition, it also observes the earlier <code>counter = 1</code>; it cannot observe:</p>
<pre><code class="language-text">ready == true
counter == 0
</code></pre>
<p>If B acquires the lock before A, it may of course observe <code>ready == false</code>; that does not violate the rule.</p>
<h3>1.2 Go: sync.Mutex</h3>
<p>Go's <code>sync.Mutex</code> is not owned by a particular goroutine and is not reentrant. <a href="https://pkg.go.dev/sync#Mutex.Lock"><code>sync.Mutex.Lock</code></a> states:</p>
<blockquote>
<p><strong>“If the lock is already in use, the calling goroutine blocks until the mutex is available.”</strong></p>
</blockquote>
<p>This gives the exclusion needed for atomicity. While one goroutine holds the mutex, other goroutines attempting to acquire the same mutex wait rather than entering the critical section.</p>
<p><a href="https://go.dev/ref/mem#Locks">The Go Memory Model - Locks</a> further states:</p>
<blockquote>
<p><strong>“For any</strong> <code>sync.Mutex</code> <strong>or</strong> <code>sync.RWMutex</code> <strong>variable</strong> <code>l</code> <strong>and n &lt; m, call n of</strong> <code>l.Unlock()</code> <strong>is synchronized before call m of</strong> <code>l.Lock()</code> <strong>returns.”</strong></p>
</blockquote>
<p>This rule provides visibility and ordering.</p>
<p>For visibility, A's writes are sequenced-before <code>Unlock</code>; <code>Unlock</code> is synchronized-before B's <code>Lock</code> returns; B's reads are sequenced after that return. Together these relationships form happens-before, so A's writes are visible to B.</p>
<p>For ordering, the same happens-before chain connects A's writes, <code>Unlock → Lock</code>, and B's reads. B likewise cannot observe <code>ready = true, counter = 0</code> when the synchronization relationship requires the earlier <code>counter</code> write to be visible.</p>
<p>Use the same two examples again.</p>
<p><strong>Atomicity</strong></p>
<p>Place the previous <code>count++</code> inside a critical section guarded by the same <code>sync.Mutex</code>:</p>
<pre><code class="language-go">mu.Lock()
count++
mu.Unlock()
</code></pre>
<p>When two goroutines execute it concurrently:</p>
<pre><code class="language-text">initial: count = 0

Goroutine A                      Goroutine B

Lock()
                                 Lock()
                                 wait
read count -&gt; 0
compute count + 1 -&gt; 1
write count -&gt; 1
Unlock()
                                 Lock() returns
                                 read count -&gt; 1
                                 compute count + 1 -&gt; 2
                                 write count -&gt; 2
                                 Unlock()
</code></pre>
<p>The final result is:</p>
<pre><code class="language-text">count = 2
</code></pre>
<p>Again, the atomicity comes from serializing the critical section with the mutex, not from ordinary <code>count++</code> itself.</p>
<p><strong>Visibility / Ordering</strong></p>
<p>Now reuse the same <code>counter / ready</code> example:</p>
<pre><code class="language-go">// Goroutine A
mu.Lock()
counter = 1
ready = true
mu.Unlock()

// Goroutine B
mu.Lock()
if ready {
    fmt.Println(counter)
}
mu.Unlock()
</code></pre>
<p>If B's <code>Lock()</code> returns after A's <code>Unlock()</code>:</p>
<pre><code class="language-text">counter = 1
ready = true
    │
    ▼
Unlock()
    │
    │ synchronized-before
    ▼
Lock() returns
    │
    ▼
read ready == true
read counter == 1
</code></pre>
<p>So if B observes <code>ready == true</code>, it also observes <code>counter == 1</code>; the result <code>ready = true, counter = 0</code> is not allowed by this synchronization chain.</p>
<h3>1.3 CPython: threading.Lock</h3>
<p>Python's official <a href="https://docs.python.org/3/library/threading.html#lock-objects"><code>threading.Lock</code></a> documentation states:</p>
<blockquote>
<p><strong>“A primitive lock is a synchronization primitive that is not owned by a particular thread when locked.”</strong></p>
<p><strong>“All methods are executed atomically.”</strong></p>
</blockquote>
<p>The second rule corresponds to Atomicity. When the lock is already held, other threads calling <code>acquire()</code> wait; only one thread at a time can therefore enter the critical section protected by that lock.</p>
<p>For Visibility and Ordering, Python differs from Java and Go: it does not define a formal language-level <code>release → acquire</code> happens-before rule, so these guarantees cannot be derived from a language memory model in the same way.</p>
<p>Now put the same examples behind <code>threading.Lock</code>.</p>
<p><strong>Atomicity</strong></p>
<pre><code class="language-python">with lock:
    count += 1
</code></pre>
<p>When two threads use the same Lock:</p>
<pre><code class="language-text">initial: count = 0

Thread A                         Thread B

acquire()
                                 acquire()
                                 wait
read count -&gt; 0
compute count + 1 -&gt; 1
write count -&gt; 1
release()
                                 acquire() returns
                                 read count -&gt; 1
                                 compute count + 1 -&gt; 2
                                 write count -&gt; 2
                                 release()
</code></pre>
<p>The final result is:</p>
<pre><code class="language-text">count = 2
</code></pre>
<p><strong>Visibility / Ordering</strong></p>
<p>Protect the shared <code>counter / ready</code> state with the same Lock as well:</p>
<pre><code class="language-python"># Thread A
with lock:
    counter = 1
    ready = True

# Thread B
with lock:
    if ready:
        print(counter)
</code></pre>
<p>In CPython, the same Lock serializes these two critical sections: A writes <code>counter = 1</code> and <code>ready = True</code>, releases the Lock, and B then acquires that same Lock before reading the shared state.</p>
<p>So in this example, if B reads <code>ready == True</code>, the corresponding <code>counter</code> value is already <code>1</code>.</p>
<p>The next article explores how CPython implements <code>release → acquire</code> to establish visibility and ordering across critical sections.</p>
<hr />
<h2>2. Next: How Is a Mutex Implemented?</h2>
<p>This article stops at the language level: it looks at what guarantees a Mutex provides to programmers from the perspectives of Atomicity, Visibility, and Ordering.</p>
<p>The next article turns those language-level guarantees into implementation-level problems, following Java <code>synchronized</code>, Go <code>sync.Mutex</code>, and CPython <code>threading.Lock</code> from the Runtime down to the CPU.</p>
<hr />
<blockquote>
<p>This article was first published on <a href="https://thinkerqaq.com/en/articles/concurrency-series-03-mutex/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=article_syndication">ThinkerQAQ's personal blog</a> and syndicated here by the author. The original article may be revised over time; please refer to the personal blog for the latest version.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Concurrency Programming (2): Language Memory Models — Rules Programmers Can Rely On]]></title><description><![CDATA[The previous article on runtime scheduling explained how execution units get CPU time. Here, we turn to the language rules governing concurrent memory…]]></description><link>https://thinkerqaq.hashnode.dev/concurrency-programming-2-language-memory-models-rules-programmers-can-rely-on</link><guid isPermaLink="true">https://thinkerqaq.hashnode.dev/concurrency-programming-2-language-memory-models-rules-programmers-can-rely-on</guid><category><![CDATA[Python]]></category><category><![CDATA[Java]]></category><category><![CDATA[golang]]></category><category><![CDATA[concurrency]]></category><dc:creator><![CDATA[ThinkerQAQ]]></dc:creator><pubDate>Fri, 23 Oct 2026 12:21:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/5ab60fb6-45cd-4369-b8d9-11998d670914.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Table of Contents</h2>
<ul>
<li><a href="#0-continue-from-the-previous-article">0. Continue from the Previous Article</a></li>
<li><a href="#1-why-does-the-language-need-a-memory-model-layer">1. Why Does the Language Need a Memory Model Layer?</a></li>
<li><a href="#2-how-should-programmers-understand-memory-model-rules">2. How Should Programmers Understand Memory Model Rules?</a><ul>
<li><a href="#21-java-memory-model">2.1 Java Memory Model</a></li>
<li><a href="#22-go-memory-model">2.2 Go Memory Model</a></li>
<li><a href="#23-cpython-concurrency-semantics">2.3 CPython Concurrency Semantics</a><ul>
<li><a href="#231-gil-mode">2.3.1 GIL Mode</a></li>
<li><a href="#232-free-threaded-mode">2.3.2 Free-threaded Mode</a></li>
<li><a href="#233-so-what-should-python-programs-rely-on">2.3.3 So What Should Python Programs Rely On?</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#3-next-mutexes">3. Next: Mutexes</a></li>
</ul>
<hr />
<h2>0. Continue from the Previous Article</h2>
<p>The <a href="https://thinkerqaq.com/en/articles/concurrency-series-01-5-thread-scheduling/">previous article on runtime scheduling</a> explained how execution units get CPU time. Here, we turn to the language rules governing concurrent memory accesses.</p>
<hr />
<h2>1. Why Does the Language Need a Memory Model Layer?</h2>
<p>Source code passes through a compiler and Runtime before it finally executes on a CPU. Compilers optimize code, and processors such as x86-64 and ARM64 do not allow exactly the same memory-access orderings.</p>
<p>If programmers had to study every compiler, Runtime, and CPU separately, portable concurrent programming would be difficult to sustain.</p>
<p>We therefore also need a language memory model layer. It defines the rules programmers need to understand, while the underlying Compiler / Runtime implements those rules differently for each platform:</p>
<pre><code class="language-text">Java                                                Go                                                Python
 │                                                   │                                                   │
 ▼                                                   ▼                                                   ▼
synchronized / volatile / AtomicInteger             sync.Mutex / sync/atomic / channel                 threading.Lock / queue.Queue
 │                                                   │                                                   │
 ▼                                                   ▼                                                   ▼
Java Memory Model                                   Go Memory Model                                     CPython Concurrency Semantics
 │                                                   │                                                   │
 └───────────────────────────────────────────────────┼───────────────────────────────────────────────────┘
                                                     │
                                       Compiler / Runtime implementation
                                       abstracts away hardware differences
                                                     │
                                            ┌────────┴────────┐
                                            │                 │
                                            ▼                 ▼
                                         x86-64             ARM64
</code></pre>
<hr />
<h2>2. How Should Programmers Understand Memory Model Rules?</h2>
<p>There are three dimensions to look at:</p>
<ul>
<li><strong>Atomicity</strong></li>
<li><strong>Visibility</strong></li>
<li><strong>Ordering</strong></li>
</ul>
<p>In theory, each concurrency primitive has corresponding memory-model rules that describe which of these properties it provides.</p>
<p>We will not go deeper here. Instead, we will reuse the same examples from earlier:</p>
<pre><code class="language-text">count++
</code></pre>
<p>and:</p>
<pre><code class="language-text">counter = 1
ready = true
</code></pre>
<p>We will use the language memory model to answer three questions:</p>
<table>
<thead>
<tr>
<th>Question</th>
<th>Specifically</th>
</tr>
</thead>
<tbody><tr>
<td>Atomicity</td>
<td>Why is <code>count++</code> not atomic?</td>
</tr>
<tr>
<td>Visibility</td>
<td>After A writes <code>counter = 1</code>, why is B not guaranteed to observe <code>counter = 1</code>?</td>
</tr>
<tr>
<td>Ordering</td>
<td>When B observes <code>ready = true</code>, why is B not guaranteed to observe the earlier <code>counter = 1</code>?</td>
</tr>
</tbody></table>
<hr />
<h3>2.1 Java Memory Model</h3>
<p><strong>Atomicity</strong></p>
<p>Suppose a process contains this variable:</p>
<pre><code class="language-text">count = 0
</code></pre>
<p>Now there are two execution units:</p>
<pre><code class="language-text">Thread A                    Thread B

count++                     count++
</code></pre>
<p>If A and B each execute once, is the final result guaranteed to be:</p>
<pre><code class="language-text">count = 2
</code></pre>
<p>No.</p>
<p>Logically, <code>count++</code> can be broken down into three steps:</p>
<pre><code class="language-text">read count
compute count + 1
write count back
</code></pre>
<p>So the following interleaving is possible:</p>
<pre><code class="language-text">initial: count = 0

Thread A                    Thread B

read count -&gt; 0
compute count + 1 -&gt; 1
                            read count -&gt; 0
                            compute count + 1 -&gt; 1
write count -&gt; 1
                            write count -&gt; 1
</code></pre>
<p>The final value becomes:</p>
<pre><code class="language-text">count = 1
</code></pre>
<p>Both threads complete one <code>+1</code>, but because they read the same old value <code>0</code>, both compute <code>1</code>, and one of the updates is overwritten.</p>
<p>Therefore, ordinary <code>count++</code> cannot be treated as atomic.</p>
<hr />
<p><strong>Visibility</strong></p>
<p>Now consider:</p>
<pre><code class="language-java">int counter = 0;

// Thread A
counter = 1;

// Thread B
System.out.println(counter);
</code></pre>
<p>That is:</p>
<pre><code class="language-text">Thread A                    Thread B

counter = 1                 read counter
</code></pre>
<p>In Java, Visibility is commonly expressed in terms of happens-before: whether a write is guaranteed to be observed by another thread depends on whether a happens-before relationship exists between the write and the read.</p>
<p>JLS §17.4.5 gives a very direct description of happens-before:</p>
<blockquote>
<p>“If one action happens-before another, then the first is visible to and ordered before the second.”</p>
</blockquote>
<p>In other words, if we can establish:</p>
<pre><code class="language-text">Thread A                    Thread B

counter = 1                 read counter
     │                           ▲
     │                           │
     └──── happens-before ───────┘
</code></pre>
<p>then Thread A's write is guaranteed to be visible to Thread B's read.</p>
<p>But in the current code:</p>
<pre><code class="language-text">Thread A                    Thread B

counter = 1                 read counter
     │                           ▲
     │                           │
     └── no happens-before ──────┘
</code></pre>
<p>no such cross-thread happens-before relationship has been established.</p>
<p>Therefore, Thread B is not guaranteed to observe <code>1</code>.</p>
<hr />
<p><strong>Ordering</strong></p>
<p>Return to the second example:</p>
<pre><code class="language-java">// Thread A
counter = 1;
ready = true;

// Thread B
if (ready) {
    System.out.println(counter);
}
</code></pre>
<p>That is:</p>
<pre><code class="language-text">Thread A                    Thread B

counter = 1
ready = true                read ready == true
                                  │
                                  ▼
                             read counter
</code></pre>
<p>In Java, Ordering can also be expressed in terms of happens-before. If one action happens-before another, then the first action—and memory writes that precede it in the same thread—are ordered before the second action. Through the transitivity of happens-before, those earlier writes are also visible to reads that follow the second action.</p>
<p>Applied to this example: within Thread A, <code>counter = 1</code> comes before <code>ready = true</code>; within Thread B, reading <code>ready</code> comes before reading <code>counter</code>. But the two threads are still missing the cross-thread happens-before relationship from <code>ready = true</code> to <code>read ready == true</code>.</p>
<p>So the current code still does not establish a complete happens-before chain between Thread A and Thread B:</p>
<pre><code class="language-text">Thread A                    Thread B

counter = 1
ready = true                read ready == true
     │                            │
     │                            ▼
     │                       read counter
     │
     └──── no cross-thread happens-before
</code></pre>
<p>Therefore, even if B observes:</p>
<pre><code class="language-text">ready == true
</code></pre>
<p>B is still not guaranteed to observe:</p>
<pre><code class="language-text">counter == 1
</code></pre>
<hr />
<h3>2.2 Go Memory Model</h3>
<p><strong>Atomicity</strong></p>
<p>In Go, we can approach Atomicity through the Go Memory Model's definition of a data race: it first identifies which concurrent accesses constitute a data race, then excludes atomic accesses provided by <code>sync/atomic</code> from that definition.</p>
<p>The Go Memory Model states:</p>
<blockquote>
<p>“Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.”</p>
</blockquote>
<p>It then explains that synchronization mechanisms such as channels, <code>sync</code>, and <code>sync/atomic</code> can serialize these accesses. Its definition of a data race explicitly excludes atomic accesses provided by <code>sync/atomic</code>.</p>
<p>But:</p>
<pre><code class="language-go">counter++
</code></pre>
<p>is only an ordinary increment operation, not an atomic access provided by <code>sync/atomic</code>.</p>
<p>Therefore, if two goroutines concurrently execute:</p>
<pre><code class="language-go">counter++
</code></pre>
<p>they perform concurrent reads and writes on the same <code>counter</code>, which constitutes a data race.</p>
<p>So ordinary <code>counter++</code> cannot be treated as an atomic concurrent update.</p>
<hr />
<p><strong>Visibility</strong></p>
<p>Continue with:</p>
<pre><code class="language-go">var counter int

// Goroutine A
counter = 1

// Goroutine B
fmt.Println(counter)
</code></pre>
<p>Split into two columns:</p>
<pre><code class="language-text">Goroutine A                 Goroutine B

counter = 1                 read counter
</code></pre>
<p>In Go, Visibility can also be expressed in terms of happens-before. For an ordinary read <code>r</code> to observe a write <code>w</code>, the Go Memory Model requires <code>w</code> to be visible to <code>r</code>; the first condition is:</p>
<blockquote>
<p>“w happens before r.”</p>
</blockquote>
<p>Applied to our example, if we can establish:</p>
<pre><code class="language-text">Goroutine A                 Goroutine B

counter = 1                 read counter
     │                           ▲
     │                           │
     └──── happens-before ───────┘
</code></pre>
<p>then the later read can reliably observe that write.</p>
<p>But in the current code:</p>
<pre><code class="language-text">Goroutine A                 Goroutine B

counter = 1                 read counter
     │                           ▲
     │                           │
     └── no happens-before ──────┘
</code></pre>
<p>no such relationship has been established, and the program contains a data race.</p>
<p>Therefore, the second goroutine cannot rely on always observing:</p>
<pre><code class="language-text">counter == 1
</code></pre>
<hr />
<p><strong>Ordering</strong></p>
<p>Now consider:</p>
<pre><code class="language-go">// Goroutine A
counter = 1
ready = true

// Goroutine B
if ready {
    fmt.Println(counter)
}
</code></pre>
<p>Split into two columns:</p>
<pre><code class="language-text">Goroutine A                 Goroutine B

counter = 1
ready = true                read ready == true
                                  │
                                  ▼
                             read counter
</code></pre>
<p>In Go, Ordering can likewise be expressed in terms of happens-before. This is essentially the same pattern used by the Go Memory Model under <strong>Incorrect synchronization</strong>: one goroutine writes data and then a flag; another goroutine observes the flag and then reads the earlier data.</p>
<p>The Go Memory Model states:</p>
<blockquote>
<p>“Even if this occurs, it does not imply that reads happening after r will observe writes that happened before w.”</p>
</blockquote>
<p>Applied to our example:</p>
<pre><code class="language-text">Goroutine A                 Goroutine B

counter = 1
ready = true                read ready == true
                                  │
                                  ▼
                             read counter

        no happens-before guarantee
</code></pre>
<p>So:</p>
<pre><code class="language-text">observing ready == true
</code></pre>
<p>does not imply:</p>
<pre><code class="language-text">counter must be observed as 1
</code></pre>
<p>This is very similar to the Java example.</p>
<p>Both Java and Go use happens-before to describe important Visibility and Ordering guarantees.</p>
<hr />
<h3>2.3 CPython Concurrency Semantics</h3>
<p>Python differs from Java and Go in one important respect:</p>
<p>Python does not define one unified concurrency memory model for all interpreter implementations at the same level as the JMM or Go Memory Model.</p>
<p>So this article focuses on the most widely used implementation: <strong>CPython</strong>.</p>
<p>We will continue to use the same two examples:</p>
<pre><code class="language-python">counter += 1
</code></pre>
<p>and:</p>
<pre><code class="language-python">counter = 1
ready = True
</code></pre>
<p>and examine Atomicity, Visibility, and Ordering.</p>
<h4>2.3.1 GIL Mode</h4>
<p>Before returning to <code>counter</code>, first clarify one question:</p>
<blockquote>
<p><strong>What does the GIL actually protect?</strong></p>
</blockquote>
<p>The GIL is first and foremost a mechanism used by the CPython Runtime to protect Python objects and interpreter-internal state.</p>
<p>The official Python C API documentation states the requirement directly:</p>
<blockquote>
<p>“only a thread that holds the GIL may operate on Python objects or invoke Python’s C API.”</p>
</blockquote>
<p>In other words, in default GIL-enabled CPython, a thread must hold the GIL before it can operate on Python objects or call the Python C API.</p>
<p>The documentation then uses <strong>Reference Count</strong> to explain why such a lock is needed: if two threads increment the same object's reference count at the same time, the result may be incremented only once instead of twice.</p>
<p>Suppose a Python object currently has:</p>
<pre><code class="language-text">refcount = 10
</code></pre>
<p>If two threads could modify that reference count concurrently:</p>
<pre><code class="language-text">Thread A                    Thread B

read refcount = 10          read refcount = 10

refcount + 1                refcount + 1

write refcount = 11         write refcount = 11
</code></pre>
<p>Both threads increment the reference count once, yet the final result may be:</p>
<pre><code class="language-text">expected: refcount = 12
actual:   refcount = 11
</code></pre>
<p>That would break CPython's management of Python object lifetimes.</p>
<p>So the GIL primarily protects:</p>
<pre><code class="language-text">             GIL
              │
              ▼
      CPython Runtime
              │
      ┌───────┴────────┐
      ▼                ▼
Python Objects     Runtime State
      │
      ├── Reference Count
      └── Object Internals
</code></pre>
<p>There are two different layers here:</p>
<ul>
<li><strong>CPython Runtime layer</strong>: how to safely access and maintain Python objects under concurrency</li>
<li><strong>Application layer</strong>: how to safely access and modify shared state under concurrency</li>
</ul>
<p>The GIL primarily addresses the former. It does not make application code automatically thread-safe.</p>
<hr />
<p><strong>Atomicity</strong></p>
<p>Now return to:</p>
<pre><code class="language-python">counter += 1
</code></pre>
<p>The official Python FAQ directly addresses this kind of operation.</p>
<p>When discussing which operations are atomic in GIL-enabled CPython, it explicitly lists:</p>
<pre><code class="language-python">i = i + 1
</code></pre>
<p>as a non-atomic operation:</p>
<blockquote>
<p>“These aren’t: <code>i = i+1</code>”</p>
</blockquote>
<p>Therefore, even with the GIL, we cannot treat:</p>
<pre><code class="language-python">counter += 1
</code></pre>
<p>as a single atomic concurrent update.</p>
<p>We need to distinguish:</p>
<pre><code class="language-text">GIL
 │
 └── protection for CPython Runtime / Python Objects

counter += 1
 │
 └── a compound state update defined by the application
</code></pre>
<hr />
<p><strong>Visibility</strong></p>
<p>Continue with the same example:</p>
<pre><code class="language-python">counter = 0

# Thread A
counter = 1

# Thread B
print(counter)
</code></pre>
<p>Split into two columns:</p>
<pre><code class="language-text">Thread A                    Thread B

counter = 1                 read counter
</code></pre>
<p>Here the distinction between CPython and Java / Go appears.</p>
<p>With Java and Go, we can continue to ask:</p>
<pre><code class="language-text">write
 │
 │ happens-before ?
 ▼
read
</code></pre>
<p>because those languages define formal Memory Models.</p>
<p>Python, however, does not define a corresponding happens-before model that applies uniformly to all Python implementations.</p>
<p>So we cannot draw the CPython code above as:</p>
<pre><code class="language-text">Thread A                    Thread B

counter = 1                 read counter
     │                           ▲
     │                           │
     └──── Python happens-before ┘
</code></pre>
<p>and then derive a conclusion from some Python Language Memory Model rule, because there is no corresponding unified model of that kind.</p>
<p>Conversely, because no separate rule specifies such a guarantee, we should regard the outcome as uncertain. That uncertainty means it does not satisfy the Visibility requirement.</p>
<hr />
<p><strong>Ordering</strong></p>
<p>Finally, consider:</p>
<pre><code class="language-python"># Thread A
counter = 1
ready = True

# Thread B
if ready:
    print(counter)
</code></pre>
<p>Again, split into two columns:</p>
<pre><code class="language-text">Thread A                    Thread B

counter = 1
ready = True                read ready == True
                                  │
                                  ▼
                             read counter
</code></pre>
<p>Likewise, there is no corresponding rule that specifies such a guarantee here, so we should regard the outcome as uncertain. That uncertainty means it does not satisfy the Ordering requirement.</p>
<hr />
<h4>2.3.2 Free-threaded Mode</h4>
<p>Starting with Python 3.13, CPython provides free-threaded builds in which the GIL can be disabled.</p>
<p>In GIL-enabled CPython:</p>
<pre><code class="language-text">Thread A ──┐
           │
           ├── GIL ───&gt; Python Code
           │
Thread B ──┘
</code></pre>
<p>In free-threaded CPython:</p>
<pre><code class="language-text">Thread A ─────────────&gt; CPU Core 0

Thread B ─────────────&gt; CPU Core 1
</code></pre>
<p>Multiple threads can execute Python code in parallel.</p>
<p>But removing the GIL does not mean CPython no longer needs to protect its objects and Runtime state.</p>
<p>Python's official free-threading documentation explicitly states that built-in types such as <code>dict</code>, <code>list</code>, and <code>set</code> use internal locks to protect against concurrent modification:</p>
<blockquote>
<p>“Built-in types like <code>dict</code>, <code>list</code>, and <code>set</code> use internal locks”</p>
</blockquote>
<p>These internal locks are an implementation mechanism of free-threaded CPython. They should not be interpreted as Python the language defining a unified Memory Model.</p>
<p>So the change is from:</p>
<pre><code class="language-text">GIL-enabled

one global GIL
      │
      ▼
protect CPython Runtime
</code></pre>
<p>to:</p>
<pre><code class="language-text">Free-threaded

finer-grained internal synchronization
      │
      ▼
protect CPython Runtime
</code></pre>
<p>For application code, the same three questions still exist.</p>
<p>Different paths, same destination. Removing the GIL does not mean CPython no longer needs synchronization; it moves from one global lock toward finer-grained locks, atomic operations, and Runtime coordination. In this respect, its implementation approach increasingly resembles Java and Go.</p>
<hr />
<h4>2.3.3 So What Should Python Programs Rely On?</h4>
<p>In summary, Java, Go, and CPython differ at this layer in an important way:</p>
<pre><code class="language-text">Java
  │
  ▼
Java Memory Model
  │
  └── happens-before

Go
  │
  ▼
Go Memory Model
  │
  └── happens-before

Python
  │
  ▼
Concurrency semantics of different Interpreters
  │
  └── CPython
        ├── GIL-enabled
        └── Free-threaded
</code></pre>
<p>The summary is simple: Python does not have one unified Memory Model, at the same level as the JMM or Go Memory Model, that applies to every interpreter implementation. Therefore, when discussing concurrency safety in Python, first identify which Interpreter is being used, and rely on explicit synchronization tools and the concurrency guarantees they provide.</p>
<hr />
<h2>3. Next: Mutexes</h2>
<p>Earlier, we discussed the hardware layer; this article explains the rules at the language memory model layer.</p>
<p>The next article begins with the first concrete synchronization tool, Mutex, and follows it from the language memory model layer all the way down to the hardware layer.</p>
<hr />
<blockquote>
<p>This article was first published on <a href="https://thinkerqaq.com/en/articles/concurrency-series-02-language-memory-model/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=article_syndication">ThinkerQAQ's personal blog</a> and syndicated here by the author. The original article may be revised over time; please refer to the personal blog for the latest version.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Concurrency Programming (1.5): The Operating System Layer — Threads, Schedulers, and Runtime Scheduling]]></title><description><![CDATA[The previous article examined how the CPU executes count++ and how atomic instructions, cache coherence, and fences provide the hardware foundations for…]]></description><link>https://thinkerqaq.hashnode.dev/concurrency-programming-1-5-the-operating-system-layer-threads-schedulers-and-runtime-scheduling</link><guid isPermaLink="true">https://thinkerqaq.hashnode.dev/concurrency-programming-1-5-the-operating-system-layer-threads-schedulers-and-runtime-scheduling</guid><category><![CDATA[Python]]></category><category><![CDATA[golang]]></category><category><![CDATA[Java]]></category><category><![CDATA[os]]></category><category><![CDATA[concurrency]]></category><category><![CDATA[scheduler]]></category><dc:creator><![CDATA[ThinkerQAQ]]></dc:creator><pubDate>Thu, 22 Oct 2026 11:11:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/b57a8202-fa48-4193-bfa9-c54caa2581a7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Table of Contents</h2>
<ul>
<li><a href="#0-from-language-level-execution-units-to-the-cpu">0. From Language-Level Execution Units to the CPU</a></li>
<li><a href="#1-how-does-java-schedule-threads">1. How Does Java Schedule Threads?</a><ul>
<li><a href="#11-from-platform-threads-to-virtual-threads">1.1 From Platform Threads to Virtual Threads</a><ul>
<li><a href="#111-the-simplest-model-one-java-thread-per-os-thread">1.1.1 The Simplest Model: One Java Thread per OS Thread</a></li>
<li><a href="#112-the-problem-resource-and-scheduling-costs-of-many-os-threads">1.1.2 The Problem: Resource and Scheduling Costs of Many OS Threads</a></li>
<li><a href="#113-the-solution-let-the-jvm-schedule-lightweight-threads">1.1.3 The Solution: Let the JVM Schedule Lightweight Threads</a></li>
<li><a href="#114-the-result-platform-threads-and-virtual-threads">1.1.4 The Result: Platform Threads and Virtual Threads</a></li>
</ul>
</li>
<li><a href="#12-execution-layers">1.2 Execution Layers</a></li>
<li><a href="#13-from-java-thread-lifecycles-to-their-implementation">1.3 From Java Thread Lifecycles to Their Implementation</a><ul>
<li><a href="#131-creation-and-start-created--runnable">1.3.1 Creation and Start: Created → Runnable</a></li>
<li><a href="#132-waiting-to-be-scheduled-runnable">1.3.2 Waiting to Be Scheduled: Runnable</a></li>
<li><a href="#133-running-runnable--running">1.3.3 Running: Runnable → Running</a></li>
<li><a href="#134-parking-and-waiting-running--waiting">1.3.4 Parking and Waiting: Running → Waiting</a></li>
<li><a href="#135-wakeup-and-resumption-waiting--runnable--running">1.3.5 Wakeup and Resumption: Waiting → Runnable → Running</a></li>
<li><a href="#136-completion-and-cleanup-running--terminated">1.3.6 Completion and Cleanup: Running → Terminated</a></li>
</ul>
</li>
<li><a href="#14-end-to-end-sequence-diagrams">1.4 End-to-End Sequence Diagrams</a></li>
<li><a href="#15-platform-threads-vs-virtual-threads">1.5 Platform Threads vs. Virtual Threads</a></li>
<li><a href="#16-returning-to-the-opening-question">1.6 Returning to the Opening Question</a></li>
</ul>
</li>
<li><a href="#2-how-does-go-schedule-goroutines">2. How Does Go Schedule Goroutines?</a><ul>
<li><a href="#21-from-g-m-to-g-m-p">2.1 From G-M to G-M-P</a><ul>
<li><a href="#211-the-simplest-model-a-goroutine-runs-on-an-os-thread">2.1.1 The Simplest Model: A Goroutine Runs on an OS Thread</a></li>
<li><a href="#212-the-problem-many-goroutines-fewer-os-threads">2.1.2 The Problem: Many Goroutines, Fewer OS Threads</a></li>
<li><a href="#213-the-solution-evolving-from-g-m-to-g-m-p">2.1.3 The Solution: Evolving from G-M to G-M-P</a></li>
<li><a href="#214-the-final-model-g-m-and-p">2.1.4 The Final Model: G, M, and P</a></li>
</ul>
</li>
<li><a href="#22-execution-layers">2.2 Execution Layers</a></li>
<li><a href="#23-from-a-goroutines-lifecycle-to-its-runtime-implementation">2.3 From a Goroutine's Lifecycle to Its Runtime Implementation</a><ul>
<li><a href="#231-creation-created--runnable">2.3.1 Creation: Created → Runnable</a></li>
<li><a href="#232-waiting-for-scheduling-runnable">2.3.2 Waiting for Scheduling: Runnable</a></li>
<li><a href="#233-execution-runnable--running">2.3.3 Execution: Runnable → Running</a></li>
<li><a href="#234-parking-and-waiting-running--waiting">2.3.4 Parking and Waiting: Running → Waiting</a></li>
<li><a href="#235-wakeup-and-resumption-waiting--runnable--running">2.3.5 Wakeup and Resumption: Waiting → Runnable → Running</a></li>
<li><a href="#236-system-calls-and-preemption">2.3.6 System Calls and Preemption</a></li>
<li><a href="#237-completion-and-cleanup-running--dead">2.3.7 Completion and Cleanup: Running → Dead</a></li>
</ul>
</li>
<li><a href="#24-end-to-end-sequence-diagram">2.4 End-to-End Sequence Diagram</a></li>
<li><a href="#25-returning-to-the-opening-question">2.5 Returning to the Opening Question</a></li>
</ul>
</li>
<li><a href="#3-how-does-cpython-schedule-threads">3. How Does CPython Schedule Threads?</a><ul>
<li><a href="#31-from-native-os-threads-to-the-gil-and-free-threaded-builds">3.1 From Native OS Threads to the GIL and Free-Threaded Builds</a><ul>
<li><a href="#311-the-simplest-model-one-python-thread-per-os-thread">3.1.1 The Simplest Model: One Python Thread per OS Thread</a></li>
<li><a href="#312-the-problem-sharing-interpreter-state-across-os-threads">3.1.2 The Problem: Sharing Interpreter State Across OS Threads</a></li>
<li><a href="#313-the-default-solution-the-gil-restricts-interpreter-execution">3.1.3 The Default Solution: The GIL Restricts Interpreter Execution</a></li>
<li><a href="#314-the-next-step-free-threaded-cpython">3.1.4 The Next Step: Free-Threaded CPython</a></li>
<li><a href="#315-the-final-model-thread-pythreadstate-and-gil">3.1.5 The Final Model: Thread, PyThreadState, and GIL</a></li>
</ul>
</li>
<li><a href="#32-execution-layers">3.2 Execution Layers</a></li>
<li><a href="#33-from-cpython-thread-lifecycles-to-their-implementation">3.3 From CPython Thread Lifecycles to Their Implementation</a><ul>
<li><a href="#331-creation-and-start-created--runnable">3.3.1 Creation and Start: Created → Runnable</a></li>
<li><a href="#332-waiting-for-scheduling-runnable">3.3.2 Waiting for Scheduling: Runnable</a></li>
<li><a href="#333-execution-runnable--running">3.3.3 Execution: Runnable → Running</a></li>
<li><a href="#334-parking-and-waiting-running--waiting">3.3.4 Parking and Waiting: Running → Waiting</a></li>
<li><a href="#335-wakeup-and-resumption-waiting--runnable--running">3.3.5 Wakeup and Resumption: Waiting → Runnable → Running</a></li>
<li><a href="#336-completion-and-cleanup-running--terminated">3.3.6 Completion and Cleanup: Running → Terminated</a></li>
</ul>
</li>
<li><a href="#34-end-to-end-sequence-diagram">3.4 End-to-End Sequence Diagram</a></li>
<li><a href="#35-returning-to-the-opening-question">3.5 Returning to the Opening Question</a></li>
</ul>
</li>
<li><a href="#4-what-the-three-implementations-have-in-common">4. What the Three Implementations Have in Common</a><ul>
<li><a href="#41-how-does-an-execution-unit-reach-the-cpu--establish-its-carrier">4.1 How Does an Execution Unit Reach the CPU? — Establish Its Carrier</a></li>
<li><a href="#42-who-selects-what-runs-next--separate-the-scheduling-layers">4.2 Who Selects What Runs Next? — Separate the Scheduling Layers</a></li>
<li><a href="#43-what-happens-after-a-wait--regain-an-execution-opportunity">4.3 What Happens After a Wait? — Regain an Execution Opportunity</a></li>
</ul>
</li>
<li><a href="#5-next-language-memory-models">5. Next: Language Memory Models</a></li>
</ul>
<hr />
<h2>0. From Language-Level Execution Units to the CPU</h2>
<p>The previous article examined how the CPU executes <code>count++</code> and how atomic instructions, cache coherence, and fences provide the hardware foundations for atomicity, visibility, and ordering.</p>
<p>This article moves one layer up, to the operating system. It asks one central question:</p>
<blockquote>
<p><strong>How do Java threads, Go goroutines, and CPython threads pass through their runtimes and the operating system to execute on a CPU?</strong></p>
</blockquote>
<p>Answering it requires understanding three things:</p>
<ol>
<li><strong>Execution mapping:</strong> How does a language-level execution unit map to an OS thread?</li>
<li><strong>Scheduling responsibilities:</strong> What do the runtime scheduler and the operating system scheduler each control?</li>
<li><strong>Waiting and resumption:</strong> When an execution unit waits, wakes, or resumes, what is suspended, and which threads can continue running?</li>
</ol>
<p>Next, we will examine Java, Go, and CPython in turn. For each, we will trace the evolution of its threading model, then use execution layers, lifecycles, and sequence diagrams to answer these three questions. Finally, we will draw out the language-independent mechanisms they share and return to the opening question.</p>
<hr />
<h2>1. How Does Java Schedule Threads?</h2>
<h3>1.1 From Platform Threads to Virtual Threads</h3>
<h4>1.1.1 The Simplest Model: One Java Thread per OS Thread</h4>
<p>When a program has only a few concurrent tasks, the most straightforward approach is to map each Java thread to an OS thread. Java creates the thread, and the operating system schedules the corresponding OS thread.</p>
<p>With relatively few threads, this one-to-one mapping is easy to understand and debug.</p>
<h4>1.1.2 The Problem: Resource and Scheduling Costs of Many OS Threads</h4>
<p>Suppose a service handles many requests, each on its own thread:</p>
<pre><code class="language-text">Request 1 → Thread 1 → OS Thread 1
Request 2 → Thread 2 → OS Thread 2
Request 3 → Thread 3 → OS Thread 3
...
Request N → Thread N → OS Thread N
</code></pre>
<p>Many request-handling tasks spend most of their time waiting:</p>
<pre><code class="language-text">Read from a database
Wait for a network response
Wait for a message
Wait for other I/O
</code></pre>
<p>If Java threads continue to map one-to-one to OS threads, two costs become significant:</p>
<ol>
<li><strong>Native-thread resources:</strong> Even while a task is waiting, the operating system must retain resources such as its kernel thread and native stack. These costs grow with the thread count.</li>
<li><strong>Scheduling overhead:</strong> When many threads wake frequently and become Runnable, the operating system must schedule among more runnable threads. Context switches and scheduler work consume additional CPU time.</li>
</ol>
<h4>1.1.3 The Solution: Let the JVM Schedule Lightweight Threads</h4>
<p>How can we address these costs?</p>
<p>In this setting, logical tasks spend most of their time waiting for I/O and relatively little time using the CPU. They do not need to occupy a dedicated OS thread for their entire lifetime. The key is to <strong>decouple logical execution units from OS threads</strong>.</p>
<p>Once they are decoupled, the JVM can introduce its own scheduler:</p>
<pre><code class="language-text">Logical Thread A ─┐
Logical Thread B ─┼─→ JVM Scheduler ─→ Carrier Thread 1 ⇔ OS Thread 1
Logical Thread C ─┤                  └→ Carrier Thread 2 ⇔ OS Thread 2
Logical Thread D ─┘
</code></pre>
<p>A Carrier Thread is a Platform Thread in the carrier role. The ⇔ symbol indicates its one-to-one native mapping to an OS thread, not a method call.</p>
<p>The JVM can manage many logical threads while the operating system schedules a smaller number of underlying OS threads.</p>
<p>Java calls this lightweight logical thread a <strong>Virtual Thread</strong>.</p>
<h4>1.1.4 The Result: Platform Threads and Virtual Threads</h4>
<p>A Platform Thread maps one-to-one to an OS Thread; the JVM Scheduler dispatches Virtual Threads onto Platform Threads acting as carriers, and the OS Scheduler schedules the underlying OS Threads.</p>
<hr />
<h3>1.2 Execution Layers</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/db7635f7-e5af-41b1-8afd-9726cd5ac7cb.png" alt="A platform JavaThread maps one-to-one to an OS thread. The JVM Scheduler dispatches a VirtualThread continuation to a carrier platform thread, where it is mounted and executed. The OS scheduler ultimately schedules the underlying OS thread on a CPU." style="display:block;margin:0 auto" />

<h3>1.3 From Java Thread Lifecycles to Their Implementation</h3>
<p>A Java thread's conceptual lifecycle is:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/83feb29d-ff93-41b8-aab3-6635d900ec37.png" alt="A thread is created, becomes Runnable after starting, and runs when scheduled. It may wait, wake, and be scheduled again before eventually terminating." style="display:block;margin:0 auto" />

<p>This is a conceptual lifecycle, not an exact mapping to Java's <code>Thread.State</code>. In particular, <code>RUNNABLE</code> covers both threads awaiting CPU time and threads currently running.</p>
<h4>1.3.1 Creation and Start: Created → Runnable</h4>
<p>A platform thread creates an OS thread at <code>start()</code>; a virtual thread submits its continuation to the JVM scheduler.</p>
<p><strong>(1) Platform Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/ba401667-ca9b-4dbc-bfbc-632e77f87dea.png" alt="HotSpot creates a JavaThread and native OS Thread when a platform thread starts." style="display:block;margin:0 auto" />

<p><strong>(2) Virtual Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/65fa591e-2cf4-4a4d-aac5-653f6818b8dd.png" alt="A virtual thread starts by submitting its Continuation to the JVM scheduler." style="display:block;margin:0 auto" />

<h4>1.3.2 Waiting to Be Scheduled: Runnable</h4>
<p>A platform thread waits for OS scheduling; a virtual thread waits for JVM dispatch to a carrier thread.</p>
<p><strong>(1) Platform Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/3b0204ef-aaff-4ea0-a7b1-3c2e0236f267.png" alt="The platform OS Thread is runnable but not yet scheduled on a CPU." style="display:block;margin:0 auto" />

<p><strong>(2) Virtual Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/53f01a0c-0f1e-4d99-9eee-b2de6d724ec9.png" alt="The virtual thread Continuation is queued, but carrier dispatch has not happened." style="display:block;margin:0 auto" />

<h4>1.3.3 Running: Runnable → Running</h4>
<p>A platform thread executes <code>Thread.run()</code> on its OS thread; a virtual thread mounts and runs its continuation on a carrier.</p>
<p><strong>(1) Platform Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/bc56c504-1025-4d16-a4ce-86cca8e436e4.png" alt="The OS scheduler runs the native thread, which enters Java user code through HotSpot." style="display:block;margin:0 auto" />

<p><strong>(2) Virtual Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/b804bd38-fbcb-4a77-b700-f9b7047af726.png" alt="The JVM scheduler dispatches a Continuation, which mounts and runs on a Carrier." style="display:block;margin:0 auto" />

<h4>1.3.4 Parking and Waiting: Running → Waiting</h4>
<p>A waiting platform thread normally retains its OS thread; <strong>when unmounting is possible</strong>, a virtual thread releases its carrier.</p>
<p><strong>(1) Platform Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/5f9d1ce7-faa2-43c9-a23c-d70eb3900356.png" alt="A waiting platform thread normally blocks its underlying OS Thread." style="display:block;margin:0 auto" />

<p><strong>(2) Virtual Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/ac986d35-186c-4523-b5ab-3bd3aeda1d2b.png" alt="If the wait permits unmounting, the virtual thread parks and frees the carrier." style="display:block;margin:0 auto" />

<h4>1.3.5 Wakeup and Resumption: Waiting → Runnable → Running</h4>
<p>A platform thread resumes on the same OS thread; a virtual thread reenters the JVM scheduler and may resume on a different carrier.</p>
<p><strong>(1) Platform Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/364f2e40-856b-481e-be26-e40684010975.png" alt="The platform OS Thread becomes runnable and resumes after scheduling." style="display:block;margin:0 auto" />

<p><strong>(2) Virtual Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/e1becb4a-3868-434d-9572-810cd6c19553.png" alt="The virtual thread requeues, and may mount on a different Carrier to resume." style="display:block;margin:0 auto" />

<h4>1.3.6 Completion and Cleanup: Running → Terminated</h4>
<p>A platform thread cleans up its JavaThread and OS thread; a virtual thread terminates without terminating its carrier.</p>
<p><strong>(1) Platform Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/2bfe4e52-e823-4381-a0dd-18943a14ee57.png" alt="The platform thread completes its exit path and native OS Thread cleanup." style="display:block;margin:0 auto" />

<p><strong>(2) Virtual Thread</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/8d505e78-5264-4620-982e-235de6ed3734.png" alt="The completed virtual thread terminates without terminating its Carrier." style="display:block;margin:0 auto" />

<h3>1.4 End-to-End Sequence Diagrams</h3>
<p>The following diagrams trace the native startup path for a platform thread and the essential mount, unmount, and resume operations for a virtual thread. The virtual-thread sequence assumes its carrier already has CPU time, so it does not repeat OS scheduling.</p>
<h4>1.4.1 Platform Thread</h4>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/f1b22249-9946-4d8f-8f69-7420db93cc01.png" alt="Thread.start creates an OS thread through HotSpot. Once scheduled, it executes Thread.run. A blocking wait suspends that OS thread. When scheduled again, the thread continues where it stopped instead of repeating the startup routine." style="display:block;margin:0 auto" />

<h4>1.4.2 Virtual Thread</h4>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/e50a4421-c66b-4bde-beb9-5653428ef89f.png" alt="A virtual thread is submitted to the JVM scheduler. A carrier with CPU time runs and mounts its continuation. A wait that permits unmounting frees the carrier to do other work. The virtual thread may later resume on a different carrier." style="display:block;margin:0 auto" />

<h3>1.5 Platform Threads vs. Virtual Threads</h3>
<p>Their lifecycle differences can be summarized as follows:</p>
<table>
<thead>
<tr>
<th>Lifecycle phase</th>
<th>Platform Thread</th>
<th>Virtual Thread</th>
</tr>
</thead>
<tbody><tr>
<td>Created → Runnable</td>
<td>The JVM creates an OS thread</td>
<td>The JVM submits a continuation to its scheduler</td>
</tr>
<tr>
<td>Runnable</td>
<td>Waits for the OS scheduler</td>
<td>The JVM scheduler dispatches it to a carrier; the OS scheduler schedules the carrier's OS thread</td>
</tr>
<tr>
<td>Running</td>
<td>The OS thread executes Java code directly</td>
<td>The virtual thread mounts on a Carrier Thread backed by an OS Thread</td>
</tr>
<tr>
<td>Waiting</td>
<td>The OS thread generally waits as well</td>
<td>The virtual thread can wait independently and release its carrier if it can unmount</td>
</tr>
<tr>
<td>Resume</td>
<td>The OS thread becomes runnable again</td>
<td>The virtual thread is resubmitted to the JVM scheduler</td>
</tr>
<tr>
<td>Terminated</td>
<td>JavaThread / OS thread exit</td>
<td>Its continuation completes and <code>afterDone()</code> moves it to TERMINATED</td>
</tr>
</tbody></table>
<p>The central distinction is the execution relationship: platform threads remain associated with their OS threads, while the JVM dispatches virtual threads to carriers. When a virtual thread can unmount, its carrier becomes available for other tasks.</p>
<p>For implementation details, see OpenJDK's <a href="https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/Thread.java"><code>Thread.java</code></a>, <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/prims/jvm.cpp"><code>jvm.cpp</code></a>, <a href="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/javaThread.cpp"><code>javaThread.cpp</code></a>, and <a href="https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/VirtualThread.java"><code>VirtualThread.java</code></a>. For the virtual-thread design, see <a href="https://openjdk.org/jeps/444">JEP 444</a>.</p>
<h3>1.6 Returning to the Opening Question</h3>
<p>As <a href="#12-execution-layers">Section 1.2, Execution Layers</a> shows, a Platform Thread maps one-to-one to an OS Thread, while a Virtual Thread reaches an OS Thread through a Carrier Thread. <strong>Therefore, both thread models ultimately execute on OS Threads.</strong></p>
<p><a href="#132-waiting-to-be-scheduled-runnable">Section 1.3.2, Waiting to Be Scheduled</a> and <a href="#133-running-runnable--running">Section 1.3.3, Running</a> show that the JVM Scheduler dispatches Virtual Threads to Carrier Threads, while the OS Scheduler schedules the underlying OS Threads. <strong>Therefore, the scheduling paths differ, but the OS Scheduler ultimately grants CPU execution time to both.</strong></p>
<p><a href="#134-parking-and-waiting-running--waiting">Section 1.3.4, Parking and Waiting</a> and <a href="#135-wakeup-and-resumption-waiting--runnable--running">Section 1.3.5, Wakeup and Resumption</a> show that a Virtual Thread can release its Carrier Thread when waiting permits unmounting, then reenter JVM scheduling when ready. <strong>Therefore, many waiting tasks need not each occupy an OS Thread for their entire lifetime.</strong></p>
<p><strong>Answering the opening question:</strong> Java retains the native-thread model of Platform Threads while allowing Virtual Threads to reuse Carrier Threads, so many concurrent tasks can ultimately execute on the CPU using a relatively small number of OS Threads.</p>
<hr />
<h2>2. How Does Go Schedule Goroutines?</h2>
<h3>2.1 From G-M to G-M-P</h3>
<h4>2.1.1 The Simplest Model: A Goroutine Runs on an OS Thread</h4>
<p>For a small number of concurrent tasks, the simplest approach is:</p>
<pre><code class="language-text">Goroutine
    ↓
OS Thread
    ↓
Operating System Scheduler
    ↓
CPU Core
</code></pre>
<p>One Goroutine maps to one OS Thread; the Go runtime manages the Goroutine, while the operating system schedules its OS Thread.</p>
<p>At modest thread counts, this one-to-one model is easy to understand and debug.</p>
<h4>2.1.2 The Problem: Many Goroutines, Fewer OS Threads</h4>
<p>Suppose a service handles many requests, each on its own Goroutine:</p>
<pre><code class="language-text">Request 1 → Goroutine 1 → OS Thread 1
Request 2 → Goroutine 2 → OS Thread 2
Request 3 → Goroutine 3 → OS Thread 3
...
Request N → Goroutine N → OS Thread N
</code></pre>
<p>Many service tasks spend most of their time waiting for databases, network responses, or other I/O.</p>
<p>If Goroutines remained mapped one-to-one to OS Threads, they would face the same two costs as Java Platform Threads:</p>
<ol>
<li><strong>Native-thread resources:</strong> a waiting task would still retain a kernel thread, native stack, and other resources.</li>
<li><strong>Scheduling overhead:</strong> many OS Threads repeatedly becoming runnable would increase context switching and OS scheduling work.</li>
</ol>
<h4>2.1.3 The Solution: Evolving from G-M to G-M-P</h4>
<p>How can the runtime address this?</p>
<p>These logical tasks spend much of their time waiting for I/O rather than using the CPU. They therefore do not need to retain a dedicated OS Thread for their entire lifetime.</p>
<p>The solution is to decouple the <code>logical execution unit</code> from the <code>OS Thread</code>: the Go runtime schedules Goroutines so a limited number of OS Threads can be reused.</p>
<h5>2.1.3.1 Step One: G-M with a Global Run Queue</h5>
<p>Start with G, M, and a global run queue:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/3dc739de-c695-42b4-820f-42ae220a400a.png" alt="G1 through G4 enter one global run queue. Both M0 and M1 draw runnable goroutines from it, so they may contend for the same queue." style="display:block;margin:0 auto" />

<p>Here, G represents a goroutine and M represents an OS thread.</p>
<p>Runnable Gs primarily enter a shared global run queue, from which multiple Ms fetch work.</p>
<p>As concurrency grows, two problems emerge:</p>
<pre><code class="language-text">Multiple M instances
  ↓
Access the same Global Run Queue
  ↓
More synchronization contention
</code></pre>
<p>And:</p>
<pre><code class="language-text">Scheduling state for G is centralized
        ↓
Poorer locality
</code></pre>
<p>To reduce contention on the global queue, some scheduling state and runnable work can be distributed across execution contexts:</p>
<blockquote>
<p><strong>What if each execution context had its own scheduling resources and local run queue?</strong></p>
</blockquote>
<h5>2.1.3.2 Step Two: Introduce P and Local Run Queues</h5>
<p>To reduce contention for the global queue, the Go runtime introduces <strong>P</strong>, with each P maintaining its own local run queue:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/7f079f90-1563-4cf5-9e3d-977683a993d1.png" alt="In G-M-P, P0 and P1 each own a local run queue. This snapshot shows G1 and G2 in P0's queue, executed by M0 holding P0, and G3 and G4 in P1's queue, executed by M1 holding P1. The queues are illustrative: a goroutine is not permanently tied to one P." style="display:block;margin:0 auto" />

<p>P holds the runtime resources needed to execute Go user code and maintains its own local run queue. The actual G-M-P scheduler also retains a global queue; goroutines are not permanently assigned to a particular P. We will examine other work sources next.</p>
<p>This gives us the defining constraint of G-M-P:</p>
<blockquote>
<p><strong>An M needs a P to execute Go user code.</strong></p>
</blockquote>
<p>Scheduling state is no longer concentrated in one queue contested by all M instances; each P maintains its own local run queue.</p>
<h5>2.1.3.3 Third Question: What If a P's Local Queue Is Empty?</h5>
<p>Local queues reduce contention but can leave work unevenly distributed: one P is busy while another has nothing queued. An M holding an idle P can use work stealing to obtain runnable goroutines from another P's queue.</p>
<p>The scheduler can also find work from other sources:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/02f0328f-68e8-4aeb-81f6-5e2476bbe543.png" alt="findRunnable searches for runnable goroutines using the local queue, global queue, work stolen from another P, network polling, and timers." style="display:block;margin:0 auto" />

<p>In the actual implementation, <code>findRunnable()</code> considers fairness, timers, the network poller, and other conditions when searching for runnable Gs. It does not rigidly follow the same sequence every time.</p>
<h5>2.1.3.4 Fourth Question: What If M Blocks in a System Call?</h5>
<p>Another constraint comes from blocking system calls:</p>
<blockquote>
<p><strong>If an M holding a P blocks in the OS, must the P become idle too?</strong></p>
</blockquote>
<p>If P remains permanently attached to that M, other goroutines cannot run using its scheduling resources while the M is blocked.</p>
<p>P and M therefore need to be separable:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/2d858e2e-4c87-45bf-b4b6-83350b4bfed6.png" alt="Initially M0 holds P0 and runs G0. When M0 blocks in a system call, it still carries G0, but the runtime can reclaim P0 and hand it to M1 to run another goroutine. P need not remain idle until M0's system call returns." style="display:block;margin:0 auto" />

<p>G0 remains blocked in the system call on M0's OS thread. Meanwhile the runtime can reclaim P0 so M1 can run another runnable G. When M0 returns from the system call, it must try to acquire a P again before resuming Go user code. P and M must therefore be free to separate and recombine.</p>
<h5>2.1.3.5 Fifth Question: What If One G Runs for Too Long?</h5>
<p>Even without I/O, a goroutine that runs continuously for a long time can reduce the execution opportunities available to other runnable goroutines.</p>
<p>The runtime therefore also needs <strong>preemption</strong>:</p>
<pre><code class="language-text">G Running
    ↓
Preempt
    ↓
G Runnable
    ↓
Scheduler
    ↓
Another G gets CPU time
</code></pre>
<p>Modern Go supports preemption, including asynchronous preemption, to reduce the chance that a long-running goroutine blocks progress elsewhere. Preemption does not, by itself, guarantee strict fairness.</p>
<h4>2.1.4 The Final Model: G, M, and P</h4>
<h5>2.1.4.1 G: Goroutine</h5>
<p><strong>G</strong> represents a goroutine: a logical execution unit managed by the Go runtime.</p>
<p>It holds:</p>
<ul>
<li>Stack;</li>
<li>PC / scheduler context;</li>
<li>Scheduling state such as Runnable, Running, and Waiting.</li>
</ul>
<p><code>go f()</code> creates a G, not a new OS thread.</p>
<h5>2.1.4.2 M: Machine</h5>
<p><strong>M</strong> represents a real OS thread.</p>
<pre><code class="language-text">M
↓
OS Thread
↓
OS Scheduler
↓
CPU
</code></pre>
<p>The OS scheduler schedules the OS thread represented by M onto a CPU.</p>
<p>Each M also has a special <strong>g0</strong> goroutine. It uses M's system stack, on which the runtime performs scheduling, stack management, and other low-level work.</p>
<h5>2.1.4.3 P: Processor</h5>
<p><strong>P</strong> represents the runtime resources and permission an M needs to execute Go user code.</p>
<p>It contains scheduler and allocator state, a local run queue, and <code>runnext</code> for preferential scheduling.</p>
<p>The number of P instances is determined by <code>GOMAXPROCS</code>:</p>
<pre><code class="language-text">GOMAXPROCS
    ↓
Number of P instances
    ↓
Maximum parallelism for Go user code
</code></pre>
<p>Their roles can be summarized in one sentence:</p>
<blockquote>
<p><strong>G is the task to run; M is the OS thread; P provides the runtime resources an M needs to execute Go code.</strong></p>
</blockquote>
<h3>2.2 Execution Layers</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/31f80153-a1a8-4a68-b875-e6592313066e.png" alt="go f() is called at the language layer. The Go runtime represents the goroutine as G and uses P, run queues, and its scheduler to assign it to M. M represents an OS thread, which is ultimately scheduled onto a CPU by the OS scheduler." style="display:block;margin:0 auto" />

<p>The Go runtime scheduler and OS scheduler operate at different layers. Switching goroutines on the same M does not require an OS thread context switch.</p>
<h3>2.3 From a Goroutine's Lifecycle to Its Runtime Implementation</h3>
<p>A goroutine's conceptual lifecycle is:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/aee6d56c-9255-4169-b2e9-0fb827815afc.png" alt="Once created, a goroutine becomes Runnable and is later scheduled to Running. It may wait, enter a system call, or be preempted and become runnable again before ultimately exiting as Dead." style="display:block;margin:0 auto" />

<p>The diagram uses conceptual states such as Created and Waiting. The Go runtime uses more specific internal states, including <code>_Grunnable</code> and <code>_Gwaiting</code>.</p>
<h4>2.3.1 Creation: Created → Runnable</h4>
<p><code>go f()</code> creates a G, marks it runnable, and enqueues it for scheduling.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/1d06f3a7-d28a-4ae8-87f6-f982889a65c4.png" alt=" creates a G, marks it runnable, and enqueues it for scheduling." style="display:block;margin:0 auto" />

<h4>2.3.2 Waiting for Scheduling: Runnable</h4>
<p>The G is in a run queue, but an M holding a P has not yet selected it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/53e025f8-09be-4931-b870-e638137865f0.png" alt="The G is in a run queue, but an M holding a P has not yet selected it." style="display:block;margin:0 auto" />

<h4>2.3.3 Execution: Runnable → Running</h4>
<p>An M holding a P selects a runnable G through the runtime scheduler, then runs it through <code>execute(G)</code> and <code>gogo</code>. The OS scheduler independently schedules M's underlying OS thread.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/1c3f80b7-10be-4083-bafb-438d5c5e7ee8.png" alt="An M holding a P selects G through the runtime scheduler and executes it with  and . OS scheduling of M remains independent." style="display:block;margin:0 auto" />

<h4>2.3.4 Parking and Waiting: Running → Waiting</h4>
<p><code>gopark()</code> suspends only G, letting its M continue running other runnable goroutines while holding P.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/91b6e458-278f-49df-aa83-5c2a88a9a574.png" alt=" suspends only G, letting its M continue running other runnable goroutines while holding P." style="display:block;margin:0 auto" />

<h4>2.3.5 Wakeup and Resumption: Waiting → Runnable → Running</h4>
<p>When the wait ends, <code>goready()</code> requeues G so the scheduler can dispatch it to an available M.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/ea819429-3f64-48a2-8720-e8e26ccbba48.png" alt="When the wait ends,  requeues G so the scheduler can dispatch it to an available M." style="display:block;margin:0 auto" />

<h4>2.3.6 System Calls and Preemption</h4>
<p>A system call can block M while P becomes available to another M; preemption makes a long-running G eligible for rescheduling.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/adc319a7-b164-4923-bd2b-089dab5f3197.png" alt="A system call can block M while P becomes available to another M. Preemption makes a long-running G eligible for rescheduling." style="display:block;margin:0 auto" />

<h4>2.3.7 Completion and Cleanup: Running → Dead</h4>
<p>When G's function returns, the runtime cleans up G, marks it dead, and M schedules other work.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/e9668775-a4c5-46db-83cf-424e43f32aff.png" alt="When G's function returns, the runtime cleans up G, marks it dead, and M schedules other work." style="display:block;margin:0 auto" />

<h3>2.4 End-to-End Sequence Diagram</h3>
<p>This sequence follows one G through creation, execution, waiting, and termination. OS scheduling and goroutine switching are separate layers: the diagram shows M being scheduled onto a CPU once, but later goroutine switches on that same M need not trigger another OS scheduling event.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/68b5586a-a631-4f5a-81f5-d2588240bd22.png" alt="go f creates and queues a runnable G. An M already running on a CPU uses the Go runtime scheduler to select it from the work available to its P. gopark suspends the G, goready queues it again, and after exit the M looks for more work." style="display:block;margin:0 auto" />

<p>For implementation details, see Go's <a href="https://go.dev/src/runtime/proc.go"><code>proc.go</code></a> and <a href="https://go.dev/src/runtime/runtime2.go"><code>runtime2.go</code></a>.</p>
<h3>2.5 Returning to the Opening Question</h3>
<p><a href="#214-the-final-model-g-m-and-p">Section 2.1.4, The Final Model</a> and <a href="#22-execution-layers">Section 2.2, Execution Layers</a> establish that G is a logical execution unit, M corresponds to an OS thread, and M needs P to execute Go code. <strong>Therefore, a G executes through an M holding a P, not as a dedicated OS thread.</strong></p>
<p><a href="#2133-third-question-what-if-a-ps-local-queue-is-empty">Section 2.1.3.3, Work Sources</a>, <a href="#232-waiting-for-scheduling-runnable">Section 2.3.2, Waiting for Scheduling</a>, and <a href="#233-execution-runnable--running">Section 2.3.3, Execution</a> show the Go scheduler selecting runnable Gs from queues, work stealing, or netpoll for an M, while the OS scheduler schedules the underlying OS thread. <strong>Therefore, the two schedulers decide which G runs and which OS thread receives CPU time, respectively.</strong></p>
<p><a href="#234-parking-and-waiting-running--waiting">Section 2.3.4, Parking</a> and <a href="#235-wakeup-and-resumption-waiting--runnable--running">Section 2.3.5, Resumption</a> show that M can run other Gs while one G waits. <a href="#236-system-calls-and-preemption">Section 2.3.6, System Calls and Preemption</a> shows that another M may take P if a system call blocks the original M, and a long-running G may be preempted. <strong>Therefore, waiting or blocking need not leave the associated scheduling resources idle indefinitely.</strong></p>
<p><strong>Answering the opening question:</strong> Go uses G-M-P to schedule many goroutines on M instances holding P, while the OS scheduler runs their underlying OS threads on CPUs, allowing concurrent tasks to make progress on a limited number of CPU cores.</p>
<hr />
<h2>3. How Does CPython Schedule Threads?</h2>
<h3>3.1 From Native OS Threads to the GIL and Free-Threaded Builds</h3>
<h4>3.1.1 The Simplest Model: One Python Thread per OS Thread</h4>
<p>For a small number of concurrent tasks, the execution relationship is straightforward:</p>
<pre><code class="language-text">Python Thread
      ↓
OS Thread
      ↓
Operating System Scheduler
      ↓
CPU Core
</code></pre>
<p>The OS scheduler decides which OS thread receives CPU time.</p>
<h4>3.1.2 The Problem: Sharing Interpreter State Across OS Threads</h4>
<p>For example, the OS might schedule two Python threads simultaneously on different CPU cores:</p>
<pre><code class="language-text">Python Thread A → OS Thread A → CPU Core A
Python Thread B → OS Thread B → CPU Core B
</code></pre>
<p>The operating system can schedule both threads in parallel. Inside CPython, however, the threads share interpreter state, including object internals, reference counts, and runtime data structures.</p>
<p>This raises a separate runtime question:</p>
<blockquote>
<p><strong>How can multiple OS threads safely access shared state while executing Python interpreter code?</strong></p>
</blockquote>
<p>The default GIL-enabled CPython build uses the GIL to protect shared interpreter state. Whether a thread can execute protected Python code therefore depends on two independent conditions: OS scheduling and the GIL.</p>
<pre><code class="language-text">OS Scheduler
Decides which OS Thread gets CPU time

GIL
Restricts which Thread can execute protected Python interpreter code
</code></pre>
<h4>3.1.3 The Default Solution: The GIL Restricts Interpreter Execution</h4>
<p>In a default GIL-enabled build, a thread must hold the GIL while executing interpreter code protected by it:</p>
<pre><code class="language-text">OS Thread
    ↓
OS Scheduler
    ↓
CPU Time

Python Thread
    ↓
Acquire GIL
    ↓
Python Interpreter Execution
</code></pre>
<p>Executing Python bytecode therefore requires two things: the OS thread must have CPU time, and the thread must hold the GIL.</p>
<p>If another thread already holds the GIL:</p>
<pre><code class="language-text">Thread A
  ↓
Holds the GIL
  ↓
Executes Python bytecode

Thread B
  ↓
Attempts to acquire the GIL
  ↓
Waits
  ↓
The GIL is released
  ↓
Competes to acquire it again
</code></pre>
<p>Their responsibilities remain distinct:</p>
<ul>
<li>The OS scheduler governs <strong>OS Thread → CPU</strong>.</li>
<li>The GIL gates <strong>Thread → Python Interpreter Execution</strong> in a default CPython build.</li>
</ul>
<h4>3.1.4 The Next Step: Free-Threaded CPython</h4>
<p>To allow multiple Python threads to execute Python code in parallel, the normal execution path must no longer depend on that global lock:</p>
<pre><code class="language-text">Python Thread A → OS Thread A → CPU Core A
Python Thread B → OS Thread B → CPU Core B
</code></pre>
<p>A free-threaded build does not rely on the GIL as a global execution lock on its normal path. Instead, finer-grained synchronization protects CPython's internal state, allowing multiple Python threads to execute code in parallel. What changes is interpreter synchronization; the OS scheduler still schedules the same kind of native threads.</p>
<p>CPython thus offers two principal execution models:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/508307be-ef01-41f0-ba59-a6c33e515429.png" alt="Both Python thread models map to native OS threads scheduled by the OS. The GIL restricts interpreter execution in the default build; finer-grained runtime synchronization permits parallel execution in the free-threaded build. Dashed arrows denote interpreter constraints rather than OS scheduling calls." style="display:block;margin:0 auto" />

<p>Some incompatible extensions may still cause a free-threaded runtime to enable the GIL.</p>
<h4>3.1.5 The Final Model: Thread, PyThreadState, and GIL</h4>
<h5>3.1.5.1 Python Thread</h5>
<p>Python programs create and manage threads through <code>threading.Thread</code>.</p>
<p>In CPython, <code>_thread</code> ultimately creates a real OS thread.</p>
<h5>3.1.5.2 OS Thread</h5>
<p>The OS scheduler determines when an OS thread runs on a CPU.</p>
<p>Unlike Go's G-M-P or Java virtual threads, CPython does not multiplex large numbers of <code>threading.Thread</code> objects onto a smaller pool of OS threads.</p>
<h5>3.1.5.3 PyThreadState</h5>
<p><strong>PyThreadState</strong> stores the interpreter-side runtime state associated with a Python thread.</p>
<p>A thread's execution context in CPython consists of two parts:</p>
<pre><code class="language-text">OS Thread
    +
PyThreadState
    ↓
Execution context for this Thread inside the CPython Runtime
</code></pre>
<h5>3.1.5.4 GIL</h5>
<p>In the default GIL-enabled build, a thread must hold the <strong>GIL</strong> to execute protected Python interpreter code.</p>
<p>Even when its OS thread has CPU time, it must still satisfy the GIL requirement before executing the corresponding Python bytecode.</p>
<h3>3.2 Execution Layers</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/daea09d1-22b8-4fd9-8ecb-45a0639230b6.png" alt="threading.Thread belongs to the language layer. The CPython runtime manages _thread, PyThreadState, and interpreter execution. In default GIL-enabled builds, the GIL restricts interpreter execution only. The native OS thread is still scheduled onto a CPU by the OS scheduler." style="display:block;margin:0 auto" />

<h3>3.3 From CPython Thread Lifecycles to Their Implementation</h3>
<p>CPython relies on OS scheduling for <code>threading.Thread</code>. The default GIL-enabled build also requires GIL ownership to execute protected interpreter code; free-threaded builds use different synchronization mechanisms.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/64025932-5ebf-4ce4-8987-67fe1214880d.png" alt="The OS schedules native threads, which may block and later resume. GIL ownership is a separate interpreter execution constraint, not a second OS scheduling layer." style="display:block;margin:0 auto" />

<h4>3.3.1 Creation and Start: Created → Runnable</h4>
<p><code>Thread.start()</code> creates an OS thread and its <code>PyThreadState</code>; CPU execution still depends on OS scheduling.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/46cd9484-82a2-4ffd-a8b6-ca340113cc8f.png" alt=" creates an OS thread and its . CPU execution still depends on OS scheduling." style="display:block;margin:0 auto" />

<h4>3.3.2 Waiting for Scheduling: Runnable</h4>
<p>The OS thread underlying a Python thread waits for OS scheduling; the default build must also satisfy its GIL requirement.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/525f3911-db16-435c-a69c-206d602ca18a.png" alt="The OS thread underlying a Python thread waits for OS scheduling. The default build must also satisfy its GIL requirement." style="display:block;margin:0 auto" />

<h4>3.3.3 Execution: Runnable → Running</h4>
<p>The OS scheduler grants CPU time to the native thread; in the default GIL-enabled build, the thread acquires the GIL before executing Python code.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/dc235eae-d3a8-4f60-b6c8-6cb366e07713.png" alt="The OS scheduler grants CPU time to the native thread. In the default GIL-enabled build, the thread acquires the GIL before executing Python code." style="display:block;margin:0 auto" />

<h4>3.3.4 Parking and Waiting: Running → Waiting</h4>
<p>Waiting for I/O, locks, or conditions may block the underlying OS thread. In the default build, operations that release the GIL also allow another thread to execute protected interpreter code.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/8ef0cb8f-2ed1-4989-96c3-c8266f24ad86.png" alt="Blocking I/O, locks, and conditions may park the OS thread. Paths that release the GIL also yield interpreter execution in the default build." style="display:block;margin:0 auto" />

<h4>3.3.5 Wakeup and Resumption: Waiting → Runnable → Running</h4>
<p>When ready, the OS thread reenters OS scheduling; the default build must reacquire the GIL before resuming protected Python execution if it was released.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/63a370be-4df9-4024-b513-8b2b9f19b284.png" alt="When ready, the OS thread reenters OS scheduling. The default build must reacquire the GIL before resuming protected Python execution if it was released." style="display:block;margin:0 auto" />

<h4>3.3.6 Completion and Cleanup: Running → Terminated</h4>
<p>After <code>Thread.run()</code> returns, the runtime cleans up <code>PyThreadState</code>, the native thread exits, and its ThreadHandle reaches the completed state.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/e6df273e-28d8-4dea-ad35-6e52ade0a3c4.png" alt="After  returns, the runtime cleans up , the native thread exits, and ThreadHandle completes." style="display:block;margin:0 auto" />

<h3>3.4 End-to-End Sequence Diagram</h3>
<p>Here is the thread lifecycle in default GIL-enabled CPython:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/4960c801-d0c2-441e-a916-91da72f9ed1f.png" alt="The OS schedules the native thread. GIL ownership independently constrains interpreter execution in the default build, including waits and resumption." style="display:block;margin:0 auto" />

<p>For implementation details, see CPython's <a href="https://github.com/python/cpython/blob/main/Modules/_threadmodule.c"><code>_threadmodule.c</code></a>, <a href="https://github.com/python/cpython/blob/main/Python/pystate.c"><code>pystate.c</code></a>, and <a href="https://github.com/python/cpython/blob/main/Python/ceval_gil.c"><code>ceval_gil.c</code></a>; for free-threading, see the <a href="https://docs.python.org/3/howto/free-threading-python.html">official guide</a>.</p>
<h3>3.5 Returning to the Opening Question</h3>
<p><a href="#315-the-final-model-thread-pythreadstate-and-gil">Section 3.1.5, Final Model</a> and <a href="#32-execution-layers">Section 3.2, Execution Layers</a> show that <code>threading.Thread</code> maps to a native OS thread, while <code>PyThreadState</code> records its interpreter state. <strong>Therefore, the OS scheduler directly grants CPU execution opportunities; CPython has no separate user-space thread scheduler like Go's G-M-P.</strong></p>
<p><a href="#313-the-default-solution-the-gil-restricts-interpreter-execution">Section 3.1.3, GIL</a>, <a href="#332-waiting-for-scheduling-runnable">Section 3.3.2, Waiting for Scheduling</a>, and <a href="#333-execution-runnable--running">Section 3.3.3, Execution</a> show that a thread in the default GIL-enabled build must acquire the GIL before executing protected Python interpreter code. <strong>Therefore, CPU time alone does not guarantee Python-code execution; the GIL is an execution constraint, not another thread scheduler.</strong></p>
<p><a href="#334-parking-and-waiting-running--waiting">Section 3.3.4, Parking and Waiting</a>, <a href="#335-wakeup-and-resumption-waiting--runnable--running">Section 3.3.5, Resumption</a>, and <a href="#314-the-next-step-free-threaded-cpython">Section 3.1.4, Free-Threaded</a> establish that a blocking operation can suspend an OS thread and release the GIL, whereas free-threaded builds change Python-code synchronization without changing the <code>threading.Thread</code> to OS-thread mapping. <strong>Therefore, the interpreter's concurrency constraints change, but the execution carrier does not.</strong></p>
<p><strong>Answering the opening question:</strong> CPython relies on the OS scheduler for native threads. The default build uses the GIL to limit parallel execution of Python code within one interpreter, whereas free-threaded builds permit multiple threads to execute Python code in parallel; neither model multiplexes large numbers of <code>threading.Thread</code> instances onto a few OS threads.</p>
<hr />
<h2>4. What the Three Implementations Have in Common</h2>
<p>After examining Java, Go, and CPython, we can set aside their specific APIs and runtime names. Three questions remain: <strong>what carries an execution unit, who decides when it runs, and how does it resume after waiting?</strong></p>
<h3>4.1 How Does an Execution Unit Reach the CPU? — Establish Its Carrier</h3>
<p>First, distinguish the <strong>execution unit</strong> defined by a language or runtime from the <strong>OS thread</strong> that the operating system schedules. They do not necessarily have a one-to-one relationship.</p>
<p>Suppose a program has three execution units, A, B, and C. The most direct approach gives each its own OS thread:</p>
<pre><code class="language-text">Execution Unit A → OS Thread 1
Execution Unit B → OS Thread 2
Execution Unit C → OS Thread 3
</code></pre>
<p>Tasks may, however, spend most of their time waiting for I/O. If every task permanently occupies an OS thread, native-thread resources must remain allocated even for waiting tasks.</p>
<p>Another approach separates <strong>tasks</strong> from <strong>underlying threads</strong>:</p>
<pre><code class="language-text">Runnable Task A ─┐
Runnable Task B ─┼─→ Runtime Dispatch → OS Thread 1
Runnable Task C ─┘                 (reused over time)
</code></pre>
<p>One OS thread can execute different tasks at different times. Models that do not require user-space scheduling can keep the direct one-to-one mapping. Both ultimately reach the same execution path:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/3b1b1567-13ec-4094-8f02-9e752a519f35.png" alt="Execution units reach OS threads through direct mapping or runtime carriers; the OS scheduler schedules native threads onto the CPU." style="display:block;margin:0 auto" />

<p><strong>Therefore, execution units can have different carriers, but the OS scheduler ultimately dispatches OS threads—not language-level tasks—to the CPU.</strong></p>
<h3>4.2 Who Selects What Runs Next? — Separate the Scheduling Layers</h3>
<p>Establishing a carrier does not mean that a task immediately runs. Two different decisions remain:</p>
<ul>
<li><strong>Task selection:</strong> If a runtime manages lightweight runnable tasks, it chooses one to assign to an underlying thread.</li>
<li><strong>Thread scheduling:</strong> The OS scheduler selects a runnable OS thread to receive CPU time.</li>
</ul>
<p>Consider a single CPU core. OS threads T1 and T2 are both runnable, and a runtime has already assigned task A to T1:</p>
<pre><code class="language-text">Runtime: select Task A → assign to T1

OS Scheduler: T1 and T2 are both Runnable
                         ↓
                    select T2 first
                         ↓
                   CPU runs T2
</code></pre>
<p>Although the runtime has selected task A, <strong>it cannot make progress until T1 receives CPU time</strong>. Conversely, an OS thread receiving CPU time does not guarantee that a language-level synchronization condition is satisfied; such a condition is not another thread scheduler.</p>
<p>With direct one-to-one mapping, a separate runtime thread scheduler may be unnecessary, but OS scheduling still applies. Long-running work may also have to yield or be preempted so other tasks can make progress.</p>
<p><strong>Therefore, runtime task dispatch and OS thread scheduling make choices at different levels; Runnable is not the same as Running.</strong></p>
<h3>4.3 What Happens After a Wait? — Regain an Execution Opportunity</h3>
<p>Suppose task A is executing and then starts an I/O operation, while task B is already runnable. The central question is: <strong>can A's underlying OS thread run B while A waits?</strong></p>
<pre><code class="language-text">Task A running → waits for I/O
                     │
                     ├─ Suspend only logical Task A
                     │       └─ OS thread can run Task B
                     │
                     └─ Block the underlying OS thread
                             └─ That thread waits; other OS threads can run
</code></pre>
<p>Either path is possible, depending on the execution model and whether the specific wait releases its carrier. <strong>Suspending a logical task does not necessarily block its OS thread; blocking one OS thread does not stop the entire process.</strong></p>
<p>When the I/O completes, task A is not guaranteed to run immediately. It becomes eligible for scheduling again:</p>
<pre><code class="language-text">Waiting
   ↓ condition ready / wakeup
Runnable
   ↓ optionally queue in runtime and wait for a carrier
   ↓ OS thread receives CPU time
Running
</code></pre>
<p>If only the logical task was suspended, the OS thread could make progress on other tasks. If the OS thread itself was blocked, it must become runnable and receive CPU time again. <strong>Waiting and resumption determine whether execution resources can be reused; wakeup does not mean immediate execution.</strong></p>
<p>Returning to the opening question: <strong>a language-level execution unit must first be associated with an OS thread. When present, a runtime scheduler selects the task; the OS scheduler assigns CPU time to its underlying thread. Waiting and resumption determine whether that thread can serve other tasks in the meantime. Together, these mechanisms explain how concurrent execution units reach the CPU.</strong></p>
<hr />
<h2>5. Next: Language Memory Models</h2>
<p>The previous chapter explained how the CPU executes instructions. This chapter addresses the missing link: how runtimes and operating systems give concurrent execution units CPU time and resume them after waiting.</p>
<p>With the execution path established, the next chapter turns to the <strong>Language Memory Model</strong>—the rules governing concurrent reads and writes, which provide the foundation for later discussions of Mutex, Atomic, Volatile, and other synchronization mechanisms.</p>
<hr />
<blockquote>
<p>This article was first published on <a href="https://thinkerqaq.com/en/articles/concurrency-series-01-5-thread-scheduling/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=article_syndication">ThinkerQAQ's personal blog</a> and syndicated here by the author. The original article may be revised over time; please refer to the personal blog for the latest version.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Concurrency Programming (1): Start with the Hardware — From count++ to Atomicity, Visibility, and Ordering]]></title><description><![CDATA[This article continues to use count, but focuses only on the hardware layer: What concurrency problems do the CPU and memory system introduce, and what…]]></description><link>https://thinkerqaq.hashnode.dev/concurrency-programming-1-start-with-the-hardware-from-count-to-atomicity-visibility-and-ordering</link><guid isPermaLink="true">https://thinkerqaq.hashnode.dev/concurrency-programming-1-start-with-the-hardware-from-count-to-atomicity-visibility-and-ordering</guid><category><![CDATA[golang]]></category><category><![CDATA[Python]]></category><category><![CDATA[Java]]></category><category><![CDATA[concurrency]]></category><dc:creator><![CDATA[ThinkerQAQ]]></dc:creator><pubDate>Mon, 19 Oct 2026 13:03:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/7c1d4fda-c2a4-4ad5-b923-4f39ece66122.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Table of Contents</h2>
<ul>
<li><a href="#1-start-with-the-von-neumann-architecture">1. Start with the von Neumann Architecture</a></li>
<li><a href="#2-what-does-count-become">2. What Does <code>count++</code> Become?</a></li>
<li><a href="#3-why-do-we-need-caches">3. Why Do We Need Caches?</a></li>
<li><a href="#4-concurrency-on-one-core-why-can-count-lose-an-update">4. Concurrency on One Core: Why Can <code>count++</code> Lose an Update?</a></li>
<li><a href="#5-from-one-core-to-multiple-cores-visibility">5. From One Core to Multiple Cores: Visibility</a></li>
<li><a href="#6-what-about-the-order-of-multiple-memory-operations">6. What About the Order of Multiple Memory Operations?</a><ul>
<li><a href="#61-store-buffer">6.1 Store Buffer</a></li>
<li><a href="#62-out-of-order-execution">6.2 Out-of-Order Execution</a></li>
<li><a href="#63-store-buffer-vs-out-of-order-execution">6.3 Store Buffer vs Out-of-Order Execution</a></li>
</ul>
</li>
<li><a href="#7-we-have-actually-encountered-three-problems">7. We Have Actually Encountered Three Problems</a></li>
<li><a href="#8-how-does-hardware-address-them">8. How Does Hardware Address Them?</a><ul>
<li><a href="#81-atomicity-atomic-instruction">8.1 Atomicity: Atomic Instruction</a></li>
<li><a href="#82-visibility-cache-coherence">8.2 Visibility: Cache Coherence</a></li>
<li><a href="#83-memory-ordering-fence">8.3 Memory Ordering: Fence</a></li>
</ul>
</li>
<li><a href="#9-summary">9. Summary</a></li>
</ul>
<hr />
<p>This article continues to use <code>count</code>, but focuses only on the hardware layer:</p>
<blockquote>
<p><strong>What concurrency problems do the CPU and memory system introduce, and what capabilities does the hardware provide to address them?</strong></p>
</blockquote>
<hr />
<h2>1. Start with the von Neumann Architecture</h2>
<p>The von Neumann model treats programs as data: instructions and data are stored in the same general way. At a high level, a computer can be divided into a control unit, arithmetic unit, memory, input devices, and output devices.</p>
<p>The arithmetic and control units form the CPU, which also contains registers. The <strong>Program Counter (PC) stores the address of the next instruction</strong>; the Instruction Register (IR) stores the instruction currently being processed; general-purpose registers such as <code>R1</code> hold data and intermediate results.</p>
<pre><code class="language-text">┌──────────────────┐                    ┌──────────────────┐
│   Input Device   │                    │  Output Device   │
└─────────┬────────┘                    └─────────▲────────┘
          │                                       │
          └───────────────────┬───────────────────┘
                              │
                          System Bus
                              │
             ┌────────────────┴─────────────────┐
             │                                  │
             ▼                                  ▼
┌────────────────────────┐         ┌────────────────────────┐
│          CPU           │         │         Memory         │
│                        │         │                        │
│      Control Unit      │         │  Instructions + Data   │
│     Arithmetic Unit    │         │                        │
│       Registers        │         │                        │
│       PC / IR / R1     │         │                        │
└────────────────────────┘         └────────────────────────┘
</code></pre>
<p>The execution of one instruction can be simplified as:</p>
<pre><code class="language-text">        ┌────────────────────────────────────┐
        │                                    │
PC provides the next instruction address     │
        ↓                                    │
Fetch: load the instruction from memory      │
        ↓                                    │
IR: hold the current instruction             │
        ↓                                    │
Decode: decode the current instruction       │
        ↓                                    │
Execute: execute the current instruction     │
        ↓                                    │
PC advances to the next instruction ─────────┘
</code></pre>
<hr />
<h2>2. What Does <code>count++</code> Become?</h2>
<p>A source-level expression such as <code>count++</code> must first become machine instructions, and each of those instructions then goes through the execution loop from the previous section.</p>
<p>For discussion, we can simplify <code>count++</code> into three machine-level operations:</p>
<pre><code class="language-text">LOAD  R1, [count]    // load count into R1
ADD   R1, 1          // increment R1 inside the CPU
STORE [count], R1    // write the result back to count
</code></pre>
<p>Connecting this to the instruction cycle from the previous section:</p>
<pre><code class="language-text">count++
        ↓ simplified compiled / translated form

LOAD  R1, [count]
ADD   R1, 1
STORE [count], R1

        ↓ each machine instruction goes through the previous cycle

PC → address of LOAD
Fetch LOAD
IR ← LOAD R1, [count]
Decode LOAD
Execute LOAD

        ↓

PC → address of ADD
Fetch ADD
IR ← ADD R1, 1
Decode ADD
Execute ADD

        ↓

PC → address of STORE
Fetch STORE
IR ← STORE [count], R1
Decode STORE
Execute STORE
</code></pre>
<p>Both <code>LOAD</code> and <code>STORE</code> access data. That leads naturally to the next question:</p>
<blockquote>
<p><strong>What would happen if every LOAD and STORE had to wait directly for main memory?</strong></p>
</blockquote>
<hr />
<h2>3. Why Do We Need Caches?</h2>
<p>A CPU executes far faster than main memory can be accessed. If every <code>LOAD</code> had to wait for main memory to return data and every <code>STORE</code> had to wait until memory completed the write, the CPU would spend a large amount of time idle.</p>
<p>Modern processors therefore place smaller, faster cache levels between a CPU core and main memory. A simplified hierarchy looks like this; exact levels and which caches are shared depend on the processor design:</p>
<pre><code class="language-text">CPU
 │
 ▼
L1 Cache
 │
 ▼
L2 Cache
 │
 ▼
L3 Cache
 │
 ▼
Memory
</code></pre>
<p>When the CPU accesses <code>count</code>, it normally loads the cache line containing it into a cache. Simplifying again:</p>
<pre><code class="language-text">CPU
 │
 ▼
Cache: count = 0
 │
 ▼
Memory: count = 0
</code></pre>
<p>Later accesses to <code>count</code> may hit in the cache instead of going to main memory every time.</p>
<p>So:</p>
<blockquote>
<p><strong>Caches address the speed gap between the CPU and memory.</strong></p>
</blockquote>
<p>That is fundamentally a performance problem.</p>
<p>Many concurrency issues are related to caches, but we will leave those details aside for now and move directly to the concurrency problems themselves.</p>
<hr />
<h2>4. Concurrency on One Core: Why Can <code>count++</code> Lose an Update?</h2>
<p>Suppose the machine has only one CPU core:</p>
<pre><code class="language-text">Thread A ──┐
           │
           ├──&gt; Core 0 ──&gt; Cache ──&gt; Memory
           │
Thread B ──┘
</code></pre>
<p>The two threads cannot literally execute at the same instant on that core, but the scheduler can interleave them.</p>
<p>Start with:</p>
<pre><code class="language-text">count = 0
</code></pre>
<p>Then the following schedule is possible:</p>
<pre><code class="language-text">Thread A                              Thread B
   │                                    │
   ├─ LOAD count -&gt; 0                   │
   │                                    │
   ├─────── context switch ────────────&gt;│
   │                                    ├─ LOAD  count -&gt; 0
   │                                    ├─ ADD   1
   │                                    ├─ STORE count -&gt; 1
   │                                    │
   │&lt;────── context switch ─────────────┤
   ├─ ADD   1                           │
   ├─ STORE count -&gt; 1                  │
</code></pre>
<p>During a context switch, the operating system preserves Thread A's execution context, including intermediate state. When A resumes, it can continue computing from the <code>0</code> it read earlier.</p>
<p>The final result is:</p>
<pre><code class="language-text">count = 1
</code></pre>
<p>The reason is that:</p>
<pre><code class="language-text">LOAD
ADD
STORE
</code></pre>
<p>are not one indivisible operation.</p>
<p>If another execution unit can run between those steps, a lost update becomes possible.</p>
<p>This gives us the first problem:</p>
<blockquote>
<p><strong>Problem 1: How can a compound operation execute indivisibly?</strong></p>
</blockquote>
<p>That is the problem of:</p>
<pre><code class="language-text">Atomicity
</code></pre>
<p>We will not answer it yet. First, move from one core to multiple cores.</p>
<hr />
<h2>5. From One Core to Multiple Cores: Visibility</h2>
<p>With multiple CPU cores, two threads may truly execute in parallel:</p>
<pre><code class="language-text">Thread A                       Thread B
   │                              │
   ▼                              ▼
Core A                         Core B
   │                              │
   ▼                              ▼
Cache A                        Cache B
   │                              │
   └──────────────┬───────────────┘
                  ▼
                Memory
</code></pre>
<p>The original <code>count++</code> race still exists, but multiple cores introduce another issue.</p>
<p>Suppose:</p>
<pre><code class="language-text">count = 0
</code></pre>
<p>Both Core A and Core B have read <code>count</code>. The same data may now exist in separate caches:</p>
<pre><code class="language-text">Core A Cache                    Core B Cache

count = 0                       count = 0

               Memory
              count = 0
</code></pre>
<p>Now Core A changes <code>count</code>:</p>
<pre><code class="language-text">Core A Cache                    Core B Cache

count = 1                       count = 0
</code></pre>
<p>The question becomes:</p>
<blockquote>
<p><strong>May Core B keep using its old copy?</strong></p>
</blockquote>
<p>Or more generally:</p>
<blockquote>
<p><strong>Problem 2: After one core writes data, when can other cores observe the new value?</strong></p>
</blockquote>
<p>That is:</p>
<pre><code class="language-text">Visibility
</code></pre>
<p>Again, hold the answer for a moment and move to the third class of problem.</p>
<hr />
<h2>6. What About the Order of Multiple Memory Operations?</h2>
<p>Continue with the same <code>count</code> example:</p>
<pre><code class="language-text">count = 0
ready = false
</code></pre>
<pre><code class="language-text">Thread A                         Thread B
   │                                │
   ├─ count = 1                     ├─ read ready
   └─ ready = true                  └─ if true, read count
</code></pre>
<p>A programmer naturally wants the following guarantee:</p>
<blockquote>
<p><strong>If Thread B has already observed</strong> <code>ready = true</code><strong>, it should also observe the earlier write</strong> <code>count = 1</code><strong>.</strong></p>
</blockquote>
<p>Ideally, B should only see one of these outcomes:</p>
<pre><code class="language-text">Outcome 1:

Thread A                         Thread B

                                 read ready -&gt; false
                                 do not enter if

Outcome 2:

Thread A                         Thread B

count = 1
ready = true                     read ready -&gt; true
                                 enter if
                                 read count -&gt; 1
                                 print 1
</code></pre>
<p>We do not want:</p>
<pre><code class="language-text">Thread A                         Thread B

count = 1
(write not yet observed by B)
ready = true                     read ready -&gt; true
                                 enter if
                                 read count -&gt; 0
                                 print 0
</code></pre>
<p>So the question is:</p>
<blockquote>
<p><strong>Once B has read</strong> <code>ready = true</code><strong>, how do we prevent it from still reading the old</strong> <code>count = 0</code><strong>?</strong></p>
</blockquote>
<p>This is no longer merely a question about multiple cached copies of the same location. We now have two different memory locations:</p>
<pre><code class="language-text">count
ready
</code></pre>
<p>and a relationship between two writes:</p>
<pre><code class="language-text">count = 1
ready = true
</code></pre>
<p>This gives us the third problem:</p>
<blockquote>
<p><strong>Problem 3: In what order may memory operations become observable to other cores?</strong></p>
</blockquote>
<p>That is:</p>
<pre><code class="language-text">Memory Ordering
</code></pre>
<p>Why does hardware make this problem more complicated?</p>
<p>For the same reason as before: modern CPUs introduce various optimizations for performance.</p>
<hr />
<h3>6.1 Store Buffer</h3>
<p>Continue with <code>count++</code>. Suppose <code>count = 0</code> and Core A executes:</p>
<pre><code class="language-text">LOAD  count -&gt; 0
ADD   1     -&gt; 1
STORE [count], 1
</code></pre>
<p>The <code>STORE</code> of <code>count = 1</code> may first enter a Store Buffer, as shown below:</p>
<pre><code class="language-text">CPU (Core)
    │
    ▼
Store Buffer
    │
    ▼
Cache
    │
    ▼
Memory
</code></pre>
<p>Note: Store Buffer and Cache are not the same thing. The Store Buffer is a temporary structure on a Core's write path, while the Cache stores cached data. For simplicity, we will continue with the following abstraction:</p>
<pre><code class="language-text">CPU (Core A)
executes STORE [count], 1
        │
        ▼
Store Buffer
holds count = 1 temporarily
        │
        ▼
Memory
</code></pre>
<p>Core A can continue executing before the buffered write becomes visible to other cores:</p>
<pre><code class="language-text">initial: count = 0

Core A                               Core B
   │                                    │
   ├─ LOAD  count -&gt; 0                  │
   ├─ ADD   1     -&gt; 1                  │
   ├─ STORE count -&gt; 1                  │
   │  enters Store Buffer               │
   ├─ continue with later instructions  │
   │                                    ├─ LOAD count -&gt; 0
   │                                    │  still sees old value
   └─ count = 1 becomes visible         │
</code></pre>
<hr />
<h3>6.2 Out-of-Order Execution</h3>
<p>Modern CPUs also rearrange internal execution, where dependencies allow it, to keep execution units busy.</p>
<p>As long as this does not change the result observed by the current thread, such optimization is valid from that thread's perspective. The difficulty appears when multiple cores interact.</p>
<p>Continue with the earlier <code>count</code> publication example:</p>
<pre><code class="language-text">Thread A / Core A                 Thread B / Core B

count = 1                        if ready {
ready = true                         print(count)
                                 }
</code></pre>
<p>The source code writes <code>count</code> before <code>ready</code>. But without additional ordering constraints, on hardware whose memory model permits it, another core may observe:</p>
<pre><code class="language-text">initial: count = 0, ready = false

Thread A / Core A                 Thread B / Core B
   │                                  │
   ├─ STORE count = 1                 │
   │  not yet observed by Core B      │
   ├─ STORE ready = true              │
   │                                  ├─ LOAD ready -&gt; true
   │                                  └─ LOAD count -&gt; 0
</code></pre>
<p>In other words, A issued the <code>count</code> write first in source order, yet B observed <code>ready = true</code> first and then still read the old <code>count = 0</code>.</p>
<p>The underlying question is:</p>
<blockquote>
<p><strong>In what order is another core allowed to observe these memory operations?</strong></p>
</blockquote>
<hr />
<h3>6.3 Store Buffer vs Out-of-Order Execution</h3>
<p>These two mechanisms are easy to confuse, so compare the questions they answer directly:</p>
<table>
<thead>
<tr>
<th>Concept</th>
<th>What question does it answer?</th>
</tr>
</thead>
<tbody><tr>
<td>Store Buffer</td>
<td>After a <code>STORE</code> has already been executed by the current Core, why might another Core still not see it yet?</td>
</tr>
<tr>
<td>Out-of-Order Execution</td>
<td>Why might an independent instruction that appears later in source code execute earlier inside the Core?</td>
</tr>
</tbody></table>
<p>Their common effect is that both can influence the order in which memory operations are ultimately observed by other Cores.</p>
<hr />
<h2>7. We Have Actually Encountered Three Problems</h2>
<p>The earlier examples can be summarized as three categories:</p>
<table>
<thead>
<tr>
<th>Problem</th>
<th>Symptom</th>
<th>What hardware needs to answer</th>
</tr>
</thead>
<tbody><tr>
<td>Atomicity</td>
<td>A and B both execute <code>count++</code>, both may read <code>0</code>, and both may finally write <code>1</code></td>
<td>Which operations appear indivisible to other execution units?</td>
</tr>
<tr>
<td>Visibility</td>
<td>Core A has changed <code>count</code> to <code>1</code>, while Core B may still read a cached <code>0</code></td>
<td>After one Core writes data, when can other Cores observe it?</td>
</tr>
<tr>
<td>Ordering</td>
<td>Core A completes <code>count++</code> and then writes <code>ready = true</code>; Core B may observe <code>ready = true</code> first and still read <code>count = 0</code></td>
<td>In what order may multiple Memory operations be observed by other Cores?</td>
</tr>
</tbody></table>
<p>Next, let's look at the basic capabilities provided by hardware:</p>
<blockquote>
<p><strong>What mechanisms does modern hardware provide for these three problems?</strong></p>
</blockquote>
<hr />
<h2>8. How Does Hardware Address Them?</h2>
<h3>8.1 Atomicity: Atomic Instruction</h3>
<p>A normal <code>LOAD + ADD + STORE</code> consists of several operations. To make a Read-Modify-Write operation appear indivisible to other execution units, hardware provides atomic operations such as:</p>
<pre><code class="language-text">Compare-And-Swap
Exchange
Fetch-And-Add
</code></pre>
<p>Internally, these operations are not necessarily a single microscopic step. “Atomic” describes what competing execution units are allowed to observe:</p>
<pre><code class="language-text">┌──────────────────┐
│ read old value   │
│      │           │
│      ▼           │
│ modify           │
│      │           │
│      ▼           │
│ write new value  │
└────────┬─────────┘
         │
         └── appears as one indivisible atomic operation to competitors
</code></pre>
<p>Return to <code>count++</code>. If a language implements the increment with a hardware-supported atomic Read-Modify-Write, we can reason about the result as:</p>
<pre><code class="language-text">initial: count = 0

Thread A                         Thread B

atomically change count 0 -&gt; 1
                                 atomically change count 1 -&gt; 2

final: count = 2
</code></pre>
<p>It does not matter whether A or B wins first. Each atomic update must operate on a definite previous value, so both threads cannot independently read <code>0</code> and then both write <code>1</code>.</p>
<hr />
<h3>8.2 Visibility: Cache Coherence</h3>
<p>Multicore processors use cache-coherence protocols to coordinate cached copies of the same memory location across cores.</p>
<p>MESI is one of the classic coherence protocols:</p>
<pre><code class="language-text">M - Modified
E - Exclusive
S - Shared
I - Invalid
</code></pre>
<p>Real processors may use MESI or extensions and variants such as:</p>
<pre><code class="language-text">MESI
MOESI
MESIF
...
</code></pre>
<p>We do not need every state transition here; focus on the problem coherence solves.</p>
<p>Suppose both cores cache the line containing <code>count</code>:</p>
<pre><code class="language-text">Core A Cache              Core B Cache

count = 0                 count = 0
Shared                    Shared
</code></pre>
<p>If Core A wants to modify it to:</p>
<pre><code class="language-text">count = 1
</code></pre>
<p>the hardware must coordinate ownership of that cache line and the state of other copies.</p>
<p>A simplified view is:</p>
<pre><code class="language-text">Core A wants to modify count
        │
        ▼
obtain write permission for the cache line
        │
        ▼
invalidate copies that may no longer be used
        │
        ▼
Core A performs the modification
</code></pre>
<p>After Core A obtains write permission, Core B's old <code>count = 0</code> copy is invalidated. The next time B reads <code>count</code>, it cannot continue using that stale copy.</p>
<hr />
<h3>8.3 Memory Ordering: Fence</h3>
<p>Suppose <code>count = 1</code> has not yet been observed by B. On hardware that permits this outcome, we may see:</p>
<pre><code class="language-text">Thread A / Core A                       Thread B / Core B
|                                       |
+-- STORE count = 1                     |
+-- STORE ready = true                  |
|                                       +-- LOAD ready -&gt; true
|                                       +-- LOAD count -&gt; 0
</code></pre>
<p>To forbid that result, fences can constrain the order of memory operations on each side:</p>
<pre><code class="language-text">Thread A / Core A                       Thread B / Core B
|                                       |
+-- STORE count = 1                     |
+-- FENCE                               |
+-- STORE ready = true                  |
|                                       +-- LOAD ready -&gt; true
|                                       +-- FENCE
|                                       +-- LOAD count -&gt; 1
</code></pre>
<p>The writer-side fence prevents <code>count = 1</code> from being ordered after <code>ready = true</code>. The reader-side fence prevents the read of <code>count</code> from being ordered before the read of <code>ready</code>.</p>
<p>A fence does not guarantee that B will read <code>ready = true</code>. But once B has observed <code>ready = true</code> under the required synchronization pattern, the later read of <code>count</code> cannot still return the stale <code>0</code>.</p>
<hr />
<h2>9. Summary</h2>
<p>Hardware provides different foundations for the three concurrency problems:</p>
<table>
<thead>
<tr>
<th>Problem</th>
<th>Hardware capability</th>
</tr>
</thead>
<tbody><tr>
<td>Atomicity</td>
<td>Atomic Instruction: provides an atomic Read-Modify-Write operation (for example, Compare-And-Swap), making each update appear indivisible to competitors so they cannot observe any intermediate state.</td>
</tr>
<tr>
<td>Visibility</td>
<td>Cache Coherence: coordinates cached copies of the same Cache Line across Cores, so after one Core completes a write, other Cores do not continue using an invalidated stale value.</td>
</tr>
<tr>
<td>Ordering</td>
<td>Fence: constrains the order of related Load / Store operations, preventing other Cores from observing those memory operations in an order the program does not allow.</td>
</tr>
</tbody></table>
<p>Next: <a href="https://thinkerqaq.com/en/articles/concurrency-series-01-5-thread-scheduling/">Thread and runtime scheduling</a> — how threads and goroutines get CPU time.</p>
<hr />
<blockquote>
<p>This article was first published on <a href="https://thinkerqaq.com/en/articles/concurrency-series-01-hardware/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=article_syndication">ThinkerQAQ's personal blog</a> and syndicated here by the author. The original article may be revised over time; please refer to the personal blog for the latest version.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Concurrency Programming (0): The Problem Space and Scope]]></title><description><![CDATA[This series focuses only on: Concurrency within a single process on a single machine. In other words, we are concerned with multiple execution units inside…]]></description><link>https://thinkerqaq.hashnode.dev/concurrency-programming-0-the-problem-space-and-scope</link><guid isPermaLink="true">https://thinkerqaq.hashnode.dev/concurrency-programming-0-the-problem-space-and-scope</guid><category><![CDATA[Java]]></category><category><![CDATA[Python]]></category><category><![CDATA[golang]]></category><category><![CDATA[concurrency]]></category><dc:creator><![CDATA[ThinkerQAQ]]></dc:creator><pubDate>Sat, 17 Oct 2026 12:01:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad35f2b8a03d6436a78e29a/de7195ac-59b5-4437-b001-082648be8f10.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Table of Contents</h2>
<ul>
<li><a href="#1-define-the-scope-first">1. Define the Scope First</a></li>
<li><a href="#2-start-with-a-shared-variable">2. Start with a Shared Variable</a></li>
<li><a href="#3-two-main-coordination-models">3. Two Main Coordination Models</a><ul>
<li><a href="#31-shared-memory">3.1 Shared Memory</a></li>
<li><a href="#32-message-passing">3.2 Message Passing</a></li>
</ul>
</li>
<li><a href="#4-why-does-this-code-work-correctly">4. Why Does This Code Work Correctly?</a></li>
<li><a href="#5-languages-need-to-define-concurrency-semantics">5. Languages Need to Define Concurrency Semantics</a></li>
<li><a href="#6-why-do-we-still-need-to-go-down-to-the-hardware">6. Why Do We Still Need to Go Down to the Hardware?</a></li>
<li><a href="#7-a-single-diagram-for-the-layers-covered-in-this-series">7. A Single Diagram for the Layers Covered in This Series</a></li>
<li><a href="#8-next-start-with-the-hardware">8. Next: Start with the Hardware</a></li>
</ul>
<hr />
<h2>1. Define the Scope First</h2>
<p>This series focuses only on:</p>
<blockquote>
<p><strong>Concurrency within a single process on a single machine.</strong></p>
</blockquote>
<p>In other words, we are concerned with multiple execution units inside the same process.</p>
<p>For example:</p>
<ul>
<li>Java Thread;</li>
<li>Go Goroutine;</li>
<li>Python Thread / asyncio Task.</li>
</ul>
<p>Inter-process communication, distributed systems, and network communication are outside the scope of this series.</p>
<hr />
<h2>2. Start with a Shared Variable</h2>
<p>Suppose a process contains this variable:</p>
<pre><code class="language-text">count = 0
</code></pre>
<p>Now there are two execution units:</p>
<pre><code class="language-text">Thread A                    Thread B

count++                     count++
</code></pre>
<p>If A and B each execute once, is the final result guaranteed to be:</p>
<pre><code class="language-text">count = 2
</code></pre>
<p>No.</p>
<p>Logically, <code>count++</code> can be broken down into three steps:</p>
<pre><code class="language-text">read count
compute count + 1
write count back
</code></pre>
<p>So the following interleaving is possible:</p>
<pre><code class="language-text">initial: count = 0

Thread A                    Thread B

read count -&gt; 0
compute count + 1 -&gt; 1
                            read count -&gt; 0
                            compute count + 1 -&gt; 1
write count -&gt; 1
                            write count -&gt; 1
</code></pre>
<p>The final value becomes:</p>
<pre><code class="language-text">count = 1
</code></pre>
<p>Both threads complete one <code>+1</code>, but because they read the same old value <code>0</code>, both compute <code>1</code>, and one of the updates is overwritten.</p>
<p>The key problem is that two Threads modify <code>count</code> at the same time:</p>
<blockquote>
<p><strong>Multiple execution units access and modify the same mutable data concurrently.</strong></p>
</blockquote>
<p>Within a single process on a single machine, there are two main models for handling this safely under concurrency:</p>
<ul>
<li><strong>Shared Memory</strong>: multiple execution units directly share mutable data and coordinate concurrent access through synchronization mechanisms such as locks and atomic operations;</li>
<li><strong>Message Passing</strong>: execution units avoid directly sharing mutable data where possible and exchange data through messages instead.</li>
</ul>
<p>We will keep using <code>count++</code> to see how these two models handle the same concurrency problem.</p>
<hr />
<h2>3. Two Main Coordination Models</h2>
<h3>3.1 Shared Memory</h3>
<p>Continue with <code>count</code>.</p>
<p>In Java, the shared state can be protected with synchronization, for example with <code>synchronized</code>:</p>
<pre><code class="language-java">class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}
</code></pre>
<p>Both threads still access the same <code>Counter</code>:</p>
<pre><code class="language-text">Thread A                    Thread B

counter.increment()         counter.increment()
</code></pre>
<p>However, because of <code>synchronized</code>, the two threads cannot enter the critical section in <code>increment()</code> at the same time. In this example, that critical section is <code>count++</code>.</p>
<p>Suppose Thread A acquires the lock first. The execution becomes:</p>
<pre><code class="language-text">initial: count = 0

Thread A                         Thread B

request to enter increment()
acquire lock
                                 request to enter increment()
                                 cannot acquire the same lock; wait
read count       -&gt; 0
compute count+1  -&gt; 1
write count      -&gt; 1
leave critical section and release lock
                                 acquire lock
                                 read count       -&gt; 1
                                 compute count+1  -&gt; 2
                                 write count      -&gt; 2
                                 leave critical section and release lock

final: count = 2
</code></pre>
<p>If Thread B acquires the lock first, the order of A and B is reversed, but the result is still <code>2</code>.</p>
<p>What matters is not which thread runs first. What matters is that before one thread leaves the critical section, another thread cannot enter the same critical section. The interleaving in which both threads read <code>0</code> therefore cannot occur.</p>
<p>Go and Python provide similar mechanisms:</p>
<pre><code class="language-text">Go      -&gt; Mutex
Python  -&gt; Lock
</code></pre>
<p>They share the same idea:</p>
<blockquote>
<p><strong>Mutable data is shared directly, but access to it is coordinated through synchronization mechanisms such as locks and atomic operations.</strong></p>
</blockquote>
<hr />
<h3>3.2 Message Passing</h3>
<p>Continue with <code>count</code>.</p>
<p>For example, in Go:</p>
<pre><code class="language-go">increments := make(chan int)

go func() { // Counter Owner Goroutine
    count := 0

    for delta := range increments {
        count += delta
    }
}()
</code></pre>
<p>The two producer Goroutines do not modify <code>count</code> directly. Instead, they send increments to the same Channel:</p>
<pre><code class="language-go">go func() { // Goroutine A
    increments &lt;- 1
}()

go func() { // Goroutine B
    increments &lt;- 1
}()
</code></pre>
<p>We can abstract this as:</p>
<pre><code class="language-text">Goroutine A ── +1 ──┐
                     ├──&gt; Channel ──&gt; Counter Owner Goroutine ──&gt; count++
Goroutine B ── +1 ──┘
</code></pre>
<p>Both Goroutines only send messages. The actual <code>count</code> is modified by a dedicated <code>Counter Owner Goroutine</code>.</p>
<p>Java and Python provide similar message-passing tools:</p>
<ul>
<li>Java <code>BlockingQueue</code></li>
<li>Python <code>queue.Queue</code> / <code>asyncio.Queue</code></li>
</ul>
<p>They share the same idea:</p>
<blockquote>
<p><strong>Multiple execution units coordinate through messages instead of directly modifying the same state at the same time.</strong></p>
</blockquote>
<hr />
<h2>4. Why Does This Code Work Correctly?</h2>
<p>So far, we have handled the original <code>count++</code> problem in two ways.</p>
<p>With Shared Memory, we used synchronization mechanisms:</p>
<pre><code class="language-text">synchronized / Lock / Mutex
</code></pre>
<p>With Message Passing, we used:</p>
<pre><code class="language-text">Channel / Queue
</code></pre>
<p>But there is a more fundamental question:</p>
<blockquote>
<p><strong>Why does this code work correctly?</strong></p>
</blockquote>
<p>To answer it, we need to look at three fundamental properties of concurrent programs:</p>
<ul>
<li><strong>Atomicity</strong>: when an execution unit performs one or more operations, whether those operations appear to other execution units as one indivisible whole; other execution units cannot observe intermediate states, only the state before or after the operation.</li>
<li><strong>Visibility</strong>: after one execution unit writes data, whether other execution units can observe that write.</li>
<li><strong>Ordering</strong>: when one execution unit performs multiple operations in sequence, whether other execution units observe those operations taking effect in an order consistent with that sequence.</li>
</ul>
<p>Programmers need to know what guarantees they can rely on across these three dimensions.</p>
<p>That leads us to:</p>
<blockquote>
<p><strong>The concurrency semantics defined by the programming language.</strong></p>
</blockquote>
<hr />
<h2>5. Languages Need to Define Concurrency Semantics</h2>
<p>A programming language needs to define:</p>
<blockquote>
<p><strong>When multiple execution units access and modify the same mutable data concurrently, which behaviors are allowed and which rules programmers may rely on.</strong></p>
</blockquote>
<p>Java has the:</p>
<pre><code class="language-text">Java Memory Model
</code></pre>
<p>Go has the:</p>
<pre><code class="language-text">Go Memory Model
</code></pre>
<p>Python is somewhat different. In this series, we mainly discuss:</p>
<pre><code class="language-text">Python / CPython Concurrency Semantics
</code></pre>
<p>These rules need to answer three questions:</p>
<table>
<thead>
<tr>
<th>Question</th>
<th>What it means for the <code>count</code> example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Atomicity</strong></td>
<td><code>count++</code> contains <code>read → increment → write</code>. Without locking, A's and B's steps may interleave. With the same lock, the critical section cannot be interleaved by another execution unit using that same lock.</td>
</tr>
<tr>
<td><strong>Visibility</strong></td>
<td>After A writes <code>count = 1</code> and releases the lock, B, after acquiring the same lock, must be able to observe that write instead of continuing to use the old value <code>0</code>.</td>
</tr>
<tr>
<td><strong>Ordering</strong></td>
<td>A writes <code>count = 1</code> and then releases the lock; B subsequently acquires the same lock and reads <code>count</code>. The synchronization rules must guarantee that B observes these operations in a way consistent with that order.</td>
</tr>
</tbody></table>
<hr />
<h2>6. Why Do We Still Need to Go Down to the Operating System and Hardware?</h2>
<p>Languages define concurrency rules, and those rules are ultimately realized by the operating system and the underlying hardware.</p>
<p>The Operating System manages the execution units on which programs actually run, including thread scheduling, blocking, and wake-up, and provides synchronization primitives such as semaphores, futexes, and condition variables.</p>
<p>Hardware provides lower-level primitives and mechanisms, including atomic instructions, memory-ordering guarantees, CPU caches, and store buffers.</p>
<hr />
<h2>7. A Single Diagram for the Layers Covered in This Series</h2>
<p>First, consider the conventional software-to-hardware stack:</p>
<pre><code class="language-text">Source Code
├──→ Java: Java Application / JDK Source
├──→ Go: Go Application / Standard Library Source
└──→ Python: Python Application / Standard Library Source
        ↓
Compiler / Translator
├──→ Java: javac
├──→ Go: Go Compiler
└──→ Python: CPython Compiler
        ↓
Runtime / Execution Environment
├──→ Java: HotSpot JVM (Interpreter / JIT / Runtime)
├──→ Go: Go Runtime
└──→ Python: CPython Interpreter / Runtime
        ↓
Operating System
└──→ Linux / Windows / macOS
        ↓
Hardware
        │
        ├── ISA ──→ x86-64 / ARM64
        │      ↓
        ├── Microarchitecture ──→ Cache / Store Buffer / Reorder Buffer / Out-of-Order Execution
        │      ↓
        └── Physical Hardware ──→ CPU / DRAM / Interconnect
</code></pre>
<p>The Language Memory Model / Concurrency Semantics forms a <strong>cross-layer constraint</strong>: the Compiler, Runtime, Operating System, and Hardware work together to realize the concurrency guarantees defined by the language.</p>
<p>This series focuses on concurrency. To give the later Mutex, Atomic, Volatile, and related articles a consistent structure, we combine the <strong>concurrency rules</strong> with the <strong>conventional software / hardware stack</strong> into one discussion model:</p>
<pre><code class="language-text">Concurrency Tools
├──→ Java: synchronized / Lock / Atomic / BlockingQueue
├──→ Go: Mutex / Atomic / Channel
└──→ Python: Lock / Queue / asyncio
        ↓
Language Memory Model / Concurrency Semantics
├──→ Java: Java Memory Model
├──→ Go: Go Memory Model
└──→ Python: Python / CPython Concurrency Semantics
        ↓
Runtime / Language Implementation
├──→ Java: HotSpot JVM (Interpreter / JIT / Runtime)
├──→ Go: Go Runtime
└──→ Python: CPython Interpreter / Runtime
        ↓
Operating System
└──→ Threads / Scheduler / Semaphores / Futexes / Condition Variables
        ↓
Hardware
├──→ ISA: x86-64 / ARM64
├──→ Microarchitecture: Cache / Store Buffer / Reorder Buffer / Out-of-Order Execution
└──→ Physical Hardware: CPU / DRAM / Interconnect
</code></pre>
<h2>8. Next: Start with the Hardware</h2>
<p>The next article moves down to the Hardware layer.</p>
<p>It focuses on:</p>
<ul>
<li>why CPUs need caches;</li>
<li>how multiple CPU Cores coordinate cached data;</li>
<li>why Store Buffers and Out-of-Order Execution affect Memory-access ordering;</li>
<li>what Atomic Instructions provide.</li>
</ul>
<hr />
<blockquote>
<p>This article was first published on <a href="https://thinkerqaq.com/en/articles/concurrency-series-00/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=article_syndication">ThinkerQAQ's personal blog</a> and syndicated here by the author. The original article may be revised over time; please refer to the personal blog for the latest version.</p>
</blockquote>
]]></content:encoded></item></channel></rss>