CLR & Concurrency · Lesson 16

Volatile publication is not a compound transaction

Observe ordered publication while leaving stronger synchronization claims explicit.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

The runtime contract

Volatile supports ordering and visibility for particular reads and writes; it does not make an arbitrary multi-step state change atomic or guarantee fairness and eventual scheduling. A common publication protocol writes data before a volatile ready flag, and a reader observes that flag before consuming the published data. The portable program performs the real calls in one sequential flow, so it does not claim to demonstrate hardware contention. The external project uses an actual producer and consumer with a bounded wait. The two forms make the scope of behavioral evidence explicit instead of disguising a sequential example as a concurrency benchmark.

Trace the portable program

The writer stores thirty-seven, then publishes ready as one. The subsequent volatile read sees that flag in the portable sequence, and the program prints value:37 and ready:1. The external consumer waits until it observes the publication flag, reads the data, and reports the same stable values after both owned threads have finished. Passing that run does not prove that removing synchronization would visibly fail on every machine. A common wrong answer treats volatile as a transaction around both fields or assumes a passing stress test can replace reasoning about the publication protocol.

Execute the actual desktop lab

The desktop producer writes data before Volatile.Write publishes ready, and the consumer performs Volatile.Read until it observes the flag before reading data. Its wait is bounded and its workers are owned and joined. This is actual publication behavior on the stated runtime, not a pretend cache-flush animation or an assertion that the portable single-flow sequence created contention. The sample uses a one-time publication and never resets the flag or mutates data afterward. Reusing the same flag for several generations of mutable data would need a stronger state protocol and could not simply inherit every conclusion from this one-shot arrangement.

Pitfalls and boundary limits

Volatile should not be explained as forcing every reader to see a globally latest value immediately or creating one total order for all program activity. The API supplies particular ordering constraints; compound updates and coordination across multiple writers need additional design. A busy wait also consumes execution resources and is not a sensible default for long I/O waits. This experiment has a tiny controlled producer and a finite timeout. If work may take much longer, an appropriate signal, task, channel, or other synchronization contract can communicate readiness without turning a local polling loop into an unbounded production waiting policy.

Practice one: isolate behavior

First change the published data to fifty and predict the corresponding value with ready still one. Then arrange the data before publication but delay the consumer's start until afterward. Hint: the state flag represents readiness, so the consumer should still observe the published value without requiring a remembered notification event. Do not remove synchronization and require a failure as the exercise's expected result; an incorrect program may appear to work on a given run. The purpose is to preserve and explain the protocol, not manufacture a hardware failure screenshot.

Practice two: design and verify

For another exercise, support multiple successive snapshots by publishing an immutable state object through an appropriate atomic reference protocol. Define whether consumers require the latest available version, every version, or only eventual observation. Hint: those are different contracts; one boolean flag cannot represent an arbitrary history without more state. Compare the design with a bounded channel when every accepted item matters. Test the chosen semantics with controlled producers and consumers, and keep timeouts as explicit test-failure bounds rather than treating a finite successful run as proof of fairness or universal progress.

Review the evidence

Review publication code by naming the written data, publication location, reader observation, later mutations, and progress assumptions. The portable example is a real API sequence with deliberately narrow evidence. The external project adds actual cross-thread behavior, but no result here measures a cache flush, a speedup, or a global transaction. Choose synchronization according to the state invariant and delivery requirement, and explain the memory-order reasoning alongside tests. A passing run can confirm the bounded fixture, while the protocol's guarantees come from the documented operations and the discipline with which every participant uses them.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

Volatile publication is not a compound transaction — desktop runtime lab

Set up the SDK project

  1. Save both files at their exact paths in a fresh owned directory.
  2. Use a .NET 10 SDK, then run dotnet run --project RuntimeLab.csproj. This project intentionally uses C# 13 syntax.
  3. Workers and waits are bounded; a timeout or a violated invariant fails the process. No benchmark or NativeAOT publication is implied by an ordinary run.

Download complete SDK project ↗

Project files

Program.csDownload file ↗
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

int data = 0, ready = 0, observed = -1;
OwnedThreads.Run(
    () => { data = 37; Volatile.Write(ref ready, 1); },
    () => { if (!SpinWait.SpinUntil(() => Volatile.Read(ref ready) == 1, TimeSpan.FromSeconds(5))) throw new TimeoutException(); observed = data; });
Console.WriteLine($"value:{observed}");
Console.WriteLine($"ready:{Volatile.Read(ref ready)}");

public static class OwnedThreads
{
    public static void Run(params Action[] actions)
    {
        var errors = new ConcurrentQueue<Exception>();
        var threads = actions.Select(action => new Thread(() => { try { action(); } catch (Exception error) { errors.Enqueue(error); } }) { IsBackground = true }).ToArray();
        foreach (var thread in threads) thread.Start();
        foreach (var thread in threads) if (!thread.Join(TimeSpan.FromSeconds(8))) throw new TimeoutException("Owned worker did not finish");
        if (!errors.IsEmpty) throw new AggregateException(errors);
    }
}
RuntimeLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>13.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>false</PublishAot>
  </PropertyGroup>
</Project>

Expected result

Exact behavioral stdout on the stated desktop lab: value:37 ready:1 Variable diagnostics, where emitted, go to stderr and must not be treated as portable timing or allocation guarantees.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

int data = 0, ready = 0;
data = 37;
Volatile.Write(ref ready, 1);
if (Volatile.Read(ref ready) != 1) throw new InvalidOperationException();
Console.WriteLine($"value:{data}");
Console.WriteLine($"ready:{Volatile.Read(ref ready)}");
Open C# practice

C# practice output

value:37
ready:1
Try this
This single-flow companion does not demonstrate hardware contention. The desktop producer/consumer uses a volatile publication flag with bounded owned threads.

Further reading

Interview questions for this lesson