-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Memory model document. #75790
Memory model document. #75790
Changes from 8 commits
b1dc5ee
1fa2ecc
c4b4765
b872aba
aae251b
f13605d
a8a1003
b15a921
d554b05
38f0585
fe0a65e
e8861ec
5a25cab
be72e2e
75adccb
eec3b96
aaeadfc
4cdac19
05694e3
804942f
f1e13ed
d7c179c
615c434
20b6b52
5579343
31431ed
8b23a88
e02a5df
280014d
608f45d
ed89b04
136e637
34a074d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,248 @@ | ||||||||||||||
|
||||||||||||||
# .NET memory model | ||||||||||||||
|
||||||||||||||
## ECMA 335 vs. .NET memory models. | ||||||||||||||
ECMA 335 standard defines a very weak memory model. After two decades the desire to have a flexible model did not result in considerable benefits due to hardware being more strict. On the other hand programming against ECMA model requires extra complexity to handle scenarios that are hard to comprehend and not possible to test. | ||||||||||||||
|
||||||||||||||
In the course of multiple releases .NET runtime implementations settled around a memory model that is a practical compromise between what can be implemented efficiently on the current hardware, while staying reasonably approachable by the developers. This document rationalizes the invariants provided and expected by the CLR runtime in its current implementation with expectation of that being carried to future releases. | ||||||||||||||
|
||||||||||||||
## Alignment | ||||||||||||||
When managed by the .NET runtime, variables of built-in primitive types are *properly aligned* according to the data type size. This applies to both heap and stack allocated memory. | ||||||||||||||
|
||||||||||||||
1-byte, 2-byte, 4-byte variables are stored at 1-byte, 2-byte, 4-byte boundary, respectively. | ||||||||||||||
8-byte variables are 8-byte aligned on 64 bit platforms. | ||||||||||||||
Native-sized integer types and pointers have alignment that matches their size on the given platform. | ||||||||||||||
|
||||||||||||||
## Atomic memory accesses. | ||||||||||||||
Memory accesses to *properly aligned* data of primitive types are always atomic. The value that is observed is always a result of complete read and write operations. | ||||||||||||||
|
||||||||||||||
## Unmanaged memory access. | ||||||||||||||
As unmanaged pointers can point to any addressable memory, operations with such pointers may violate guarantees provided by the runtime and expose undefined or platform-specific behavior. | ||||||||||||||
**Example:** memory accesses through pointers which are *not properly aligned* may be not atomic or cause faults depending on the platform and hardware configuration. | ||||||||||||||
|
||||||||||||||
Although rare, unaligned access is a realistic scenario and thus there is some limited support for unaligned memory accesses, such as: | ||||||||||||||
* `.unaligned` IL prefix | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||||||||||||||
* `Unsafe.ReadUnaligned`, `Unsafe.WriteUnaligned` and ` Unsafe.CopyBlockUnaligned` helpers. | ||||||||||||||
|
||||||||||||||
These facilities ensure fault-free access to potentially unaligned locations, but do not ensure atomicity. | ||||||||||||||
|
||||||||||||||
As of this writing there is no specific support for operating with incoherent memory, device memory or similar. Passing non-ordinary memory to the runtime by the means of pointer operations or native interop results in Undefined Behavior. | ||||||||||||||
|
||||||||||||||
## Sideeffects and optimizations of memory accesses. | ||||||||||||||
.NET runtime assumes that the sideeffects of memory reads and writes include only changing and observing values at specified memory locations. This applies to all reads and writes - volatile or not. **This is different from ECMA model.** | ||||||||||||||
|
||||||||||||||
As a consequence: | ||||||||||||||
* Speculative writes are not allowed. | ||||||||||||||
* Reads cannot be introduced. | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are "Speculative writes are not allowed" and "Reads cannot be introduced" really consequences of the above assumption, or additional guarantees made by the runtime? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a design choice, but it follows from assuming that ordinary writes/reads can have observable sideeffects. If you assume that a write may be visible from other threads (unless the location is known to be local to the current thread), then you can't replace location = actualValue; with location = someSpeculativeValue;
if (speculative value was wrong)
{
location = actualValue;
} Memory models that require only singlethreaded correctness allow this (ex: ordinary writes in C++), which is often unexpected and may result in subtle bugs in multithreaded scenarios. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we are basically saying that the jit cannot introduce a race, but is allowed to remove one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For ordinary memory accesses - yes. A program cannot condition its correctness on a nondeterministic race, that is not guaranteed to ever happen, so we can remove races. Adding a race would be observable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that there are rare scenarios when a program explicitly wants to observe races - like making multiple or repeated reads of the same location and checking if the value has changed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Does this mean the following RyuJit behavior is a bug? [MethodImpl(MethodImplOptions.NoInlining)]
private static int Problem(int* p)
{
var b = *p == 1;
var c = b;
if (b)
{
JitUse(c);
return 2;
}
return 1;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void JitUse<T>(T arg) { } IN000a: 000000 sub rsp, 40
G_M51150_IG02: ; offs=000004H, size=000DH, bbWeight=1 PerfScore 8.25, gcrefRegs=00000000 {}, byrefRegs=00000000 {}, BB01 [0000], byref, isz
IN0001: 000004 xor eax, eax
IN0002: 000006 cmp dword ptr [rcx], 1 ; The original load
IN0003: 000009 sete al
IN0004: 00000C cmp dword ptr [rcx], 1 ; The duplicated load
IN0005: 00000F jne SHORT G_M51150_IG05
G_M51150_IG03: ; offs=000011H, size=000DH, bbWeight=0.50 PerfScore 1.75, gcrefRegs=00000000 {}, byrefRegs=00000000 {}, BB02 [0001], byref
IN0006: 000011 mov ecx, eax
IN0007: 000013 call [RyuJitReproduction.Program:JitUse(bool)]
IN0008: 000019 mov eax, 2
G_M51150_IG04: ; offs=00001EH, size=0005H, bbWeight=0.50 PerfScore 0.62, epilog, nogc, extend
IN000b: 00001E add rsp, 40
IN000c: 000022 ret
G_M51150_IG05: ; offs=000023H, size=0005H, bbWeight=0.50 PerfScore 0.12, gcVars=0000000000000000 {}, gcrefRegs=00000000 {}, byrefRegs=00000000 {}, BB03 [0002], gcvars, byref
IN0009: 000023 mov eax, 1
G_M51150_IG06: ; offs=000028H, size=0005H, bbWeight=0.50 PerfScore 0.62, epilog, nogc, extend
IN000d: 000028 add rsp, 40
IN000e: 00002C ret (This particular example needs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Opened #75916 to track this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we should avoid duplicating reads like this. |
||||||||||||||
* Unused reads can be elided. | ||||||||||||||
* Adjacent nonvolatile reads from the same location can be coalesced. | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are more aggressive than this today. If a program does ordinary reads of a field twice without any possibility of an intervening synchronization construct or aliasing write, the jit may do just one read. The reads to not have to be adjacent. For example
may be transformed to
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, but in a case of "without any possibility of an intervening synchronization construct or aliasing write" you can mentally move the reads around so that they are adjacent and then coalesce. JIT reasons in different terms which are more convenient in the implementation, but I think in the end there is equivalency, and JIT conforms to the rules in this document. (modulo unintentional bugs, if there are any) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just noting #47261 here as that would seem to be one such bug. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #47261 is actually ByDesign. |
||||||||||||||
* Adjacent nonvolatile writes to the same location can be coalesced. | ||||||||||||||
|
||||||||||||||
## Thread-local memory accesses. | ||||||||||||||
It may be possible for an optimizing compiler to prove that some data is accessible only by a single thread. In such case it is permitted to perform further optimizations such as duplicating or removal of memory accesses. | ||||||||||||||
|
||||||||||||||
## Cross-thread access to local variables. | ||||||||||||||
- There is no type-safe mechanism for accessing locations on one thread’s stack from another thread. | ||||||||||||||
- Accessing managed references located on the stack of a different thread by the means of unsafe code will result in Undefined Behavior. | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am curious -- what is an example of UB one can observe today when passing pointers to locals around threads? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is an obvious hazard form the other stack getting shorter and then longer and recycling the location for something completely different. As an example that does not need a lot of stress to fail. The following would typically crash on ARM64 in under 1 second: internal unsafe class Program
{
private static void Main(string[] args)
{
Task.Run(M1);
while (ptrStatic == null) { Thread.Sleep(1);}
ref object otherThreadsLocal = ref Unsafe.AsRef<object>(ptrStatic);
for(; ; )
{
System.Console.WriteLine(otherThreadsLocal); // can see corrupted incomplete objects
}
}
static void* ptrStatic;
static void M1()
{
object local1 = 0;
ptrStatic = Unsafe.AsPointer(ref local1);
for (long i = 0; i < long.MaxValue; i++)
{
local1 = i;
}
}
} |
||||||||||||||
|
||||||||||||||
## Order of memory operations. | ||||||||||||||
* **Ordinary memory accesses** | ||||||||||||||
The effects of ordinary reads and writes can be reordered as long as that preserves single-thread consistency. Such reordering can happen both due to code generation strategy of the compiler or due to weak memory ordering in the hardware. | ||||||||||||||
|
||||||||||||||
* **Volatile reads** have "acquire semantics" - no read or write that is later in the program order may be speculatively executed ahead of a volatile read. | ||||||||||||||
Operations with acquire semantics: | ||||||||||||||
- IL load instructions with `.volatile` prefix when instruction supports such prefix | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mention c# volatile keyword in here? Since most readers will commonly use that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically C# volatile belongs to C# spec, but it is common, so a few notes on that would be useful. Adding "volatile" on a variable in C# is just a decoration that makes no difference to CLR, but it is a hint to C# compiler itself (and few other compilers) to emit reads and writes of that variable as volatile reads/writes - this is the part that CLR will honor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that would be great as an "aside" here, although it's not a CLR concept it is useful context |
||||||||||||||
- `System.Threading.Volatile.Read` | ||||||||||||||
- `System.Thread.VolatileRead` | ||||||||||||||
- Acquiring a lock (`System.Threading.Monitor.Enter` or entering a synchronized method) | ||||||||||||||
|
||||||||||||||
* **Volatile writes** have "release semantics" - the effects of a volatile write will not be observable before effects of all previous, in program order, reads and writes become observable. | ||||||||||||||
Operations with release semantics: | ||||||||||||||
- IL store instructions with `.volatile` prefix when such prefix is supported | ||||||||||||||
- `System.Threading.Volatile.Write` | ||||||||||||||
- `System.Thread.VolatileWrite` | ||||||||||||||
- Releasing a lock (`System.Threading.Monitor.Exit` or leaving a synchronized method) | ||||||||||||||
|
||||||||||||||
* **.volatile initblk** has "release semantics" - the effects of `.volatile initblk` will not be observable earlier than the effects of preceeding reads and writes. | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does any of this map to C# that can be described? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not think C# emits these. |
||||||||||||||
|
||||||||||||||
* **.volatile cpblk** combines ordering semantics of a volatile read and write with respect to the read and written memory locations. | ||||||||||||||
- The writes performed by `.volatile cpblk` will not be observable earlier than the effects of preceeding reads and writes. | ||||||||||||||
- No read or write that is later in the program order may be speculatively executed before the reads performed by `.volatile cpblk` | ||||||||||||||
- `cpblk` may be implemented as a sequence of reads and writes. The granularity and mutual order of such reads and writes is unspecified. | ||||||||||||||
|
||||||||||||||
Note that volatile semantics does not by itself imply that operation is atomic or has any effect on how soon the operation is committed to the coherent memory. It only specifies the order of effects when they eventually become observable. | ||||||||||||||
|
||||||||||||||
`.volatile` and `.unaligned` IL prefixes can be combined where both are permitted. | ||||||||||||||
|
||||||||||||||
It may be possible for an optimizing compiler to prove that some data is accessible only by a single thread. In such case it is permitted to omit volatile semantics when accessing such data. | ||||||||||||||
|
||||||||||||||
* **Full-fence operations** | ||||||||||||||
Full-fence operations have "full-fence semantics" - effects of reads and writes must be observable no later or no earlier than a full-fence operation according to their relative program order. | ||||||||||||||
Operations with full-fence semantics: | ||||||||||||||
- `System.Thread.MemoryBarrier` | ||||||||||||||
- `System.Threading.Interlocked` methods | ||||||||||||||
|
||||||||||||||
## Process-wide barrier | ||||||||||||||
Process-wide barrier has full-fence semantics with an additional guarantee that each thread in the program effectively performs a full fence at arbitrary point synchronized with the process-wide barrier in such a way that effects of writes that precede both barriers are observable by memory operations that follow the barriers. | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe off-topic, but doesn't this also need compiler memory barriers to be effective? e.g. runtime/src/libraries/System.Threading/tests/InterlockedTests.cs Lines 677 to 682 in 118a162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Process-wide barrier is typically paired with a compiler barrier. A not-inlineable getter or a volatile read can work as such, but maybe it is time to have an official compiler barrier helper. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re: #75874 |
||||||||||||||
|
||||||||||||||
The actual implementation may vary depending on the platform. For example interrupting the execution of every core in the current process' affinity mask could be a suitable implementation. | ||||||||||||||
|
||||||||||||||
## Synchronized methods | ||||||||||||||
Synchronized methods have the same memory access semantics as if a lock is acquired at an entrance to the method and released upon leaving the method. | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good to reference the actual attribute |
||||||||||||||
|
||||||||||||||
## Object assignment | ||||||||||||||
Object assignment to a location potentially accessible by other threads is a release with respect to write operations to the instance’s fields and metadata. | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @danmoseley - here is somehting that could differ on Mono. Mono has a switch to turn this off. It is not the default behavior though, so I do not think it needs to be mentioned. |
||||||||||||||
The motivation is to ensure that storing an object reference to shared memory acts as a "committing point" to all modifications that are reachable through the instance reference. It also guarantees that a freshly allocated instance is valid (i.e. method table and necessary flags are set) when other threads, including background GC threads are able to access the instance. | ||||||||||||||
The reading thread does not need to perform an acquiring read before accessing the content of an instance since all supported platforms honor ordering of data-dependent reads. | ||||||||||||||
|
||||||||||||||
However, the ordering sideeffects of reference assignment should not be used for general ordering purposes because: | ||||||||||||||
- ordinary reference assignments are still treated as ordinary assignments and could be reordered by the compiler. | ||||||||||||||
- an optimizing compiler can omit the release semantics if it can prove that the instance is not shared with other threads. | ||||||||||||||
|
||||||||||||||
## Instance constructors | ||||||||||||||
CLR does not specify any ordering effects to the instance constructors. | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment wherever CLR is used, is Mono relevant? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think Mono is the same here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will be changing |
||||||||||||||
|
||||||||||||||
## Static constructors | ||||||||||||||
All side effects of static constructor execution must happen before accessing any member of the type. | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Must or will? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More precisely "will become observable not later than effects of accessing any member of the type..." or something like that. Easy way to see it as if there is a lock that is taken when static constructor runs and releasing the lock is a release. The lock also guarantees that static constructor runs only once. |
||||||||||||||
|
||||||||||||||
## Hardware considerations | ||||||||||||||
Currently supported implementations of CLR and system libraries make a few expectations about the hardware memory model. These conditions are present on all supported platforms and transparently passed to the user of the runtime. The future supported platforms will likely support these too as the large body of preexisting software will make it burdensome to break common assumptions. | ||||||||||||||
|
||||||||||||||
* Naturally aligned reads and writes with sizes up to the platform pointer size are atomic. | ||||||||||||||
That applies even for locations targeted by overlapping aligned reads and writes of different sizes. | ||||||||||||||
**Example:** a read of a 4-byte aligned int32 variable will yield a value that existed prior some write or after some write. It will never be a mix of before/after bytes. | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit - "prior to" or "before" |
||||||||||||||
|
||||||||||||||
* The memory is cache-coherent and writes to a single location will be seen by all cores in the same order (multicopy atomic). | ||||||||||||||
**Example:** when the same location is updated with values in ascending order (like 1,2,3,4,...), no observer will see a descending sequence. | ||||||||||||||
|
||||||||||||||
* It may be possible for a thread to see its own writes before they appear to other cores (store buffer forwarding), as long as the single-thread consistency is not violated. | ||||||||||||||
|
||||||||||||||
* The memory managed by the runtime is ordinary memory (not device register file or the like) and the only sideeffects of memory operations are storing and reading of values. | ||||||||||||||
|
||||||||||||||
* It is possible to implement release consistency memory model. | ||||||||||||||
Either the platform defaults to release consistency or stronger (i.e. x64 is TSO, which is stronger), or provides means to implement release consistency via fencing operations. | ||||||||||||||
|
||||||||||||||
* Memory ordering honors data dependency | ||||||||||||||
**Example:** reading a field, will not use a cached value fetched from the location of the field prior obtaining a reference to the instance. | ||||||||||||||
(Some versions of Alpha processors did not support this, most current architectures do) | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this relevant today? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, but people keep bringing this up as a concern. - "I heard that some CPUs do not honor data dependency" - Yes, some flavors of Alphas had that issue, possibly a mistake in the design, no, noone does that again. |
||||||||||||||
|
||||||||||||||
## Examples and common patterns: | ||||||||||||||
The following examples work correctly on all supported CLR implementations regardless of the target OS or architecture. | ||||||||||||||
|
||||||||||||||
* Constructing an instance and sharing with another thread is safe and does not require explicit fences. | ||||||||||||||
|
||||||||||||||
```cs | ||||||||||||||
|
||||||||||||||
static MyClass obj; | ||||||||||||||
|
||||||||||||||
// thread #1 | ||||||||||||||
void ThreadFunc1() | ||||||||||||||
{ | ||||||||||||||
while (true) | ||||||||||||||
{ | ||||||||||||||
obj = new MyClass(); | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
// thread #2 | ||||||||||||||
void ThreadFunc1() | ||||||||||||||
{ | ||||||||||||||
while (true) | ||||||||||||||
{ | ||||||||||||||
obj = null; | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
// thread #3 | ||||||||||||||
void ThreadFunc2() | ||||||||||||||
{ | ||||||||||||||
MyClass localObj = obj; | ||||||||||||||
if (localObj != null) | ||||||||||||||
{ | ||||||||||||||
// accessing members of the local object is safe because | ||||||||||||||
// - reads cannot be introduced, thus localObj cannot be re-read and become null | ||||||||||||||
// - publishing assignment to obj will not become visible earlier than write operations in the MyClass constructor | ||||||||||||||
// - indirect accesses via an instance are dependent reads, thus we will see results of constructor's writes | ||||||||||||||
System.Console.WriteLine(localObj.ToString()); | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The example is expected to work correctly even if
The rule about static constructors above tells that the cctor runs before any member is accessed and it would be useless to guarantee that static constructor "runs before" accessing without implying that the access will see the complete results. The mechanism will vary and often the results of the static constructors will be computed well in advance before the actual access (at JIT time or Load time). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't mean static constructor. Consider sealed class C
{
//There may only be one instance, so why not make every field static?
static string name;
private C()
{
name = "My Name";
}
public override string ToString()
{
return name;//should always return "My Name"
}
static readonly object lockObj = new object();
static C c;
//Thread 1 and 2
public static void PrintSingleton()
{
if (c is null)
{
lock (lockObj)
{
c ??= new C();
}
}
Console.WriteLine(c.ToString());
}
} There is no dependency between reading There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is just a write to some shared memory from the constructor. It could be a static variable in another class or unmanaged memory. Constructor can do many things. It can also launch a thread that will write to statics or it can do The sample here is not about that, but about guarantees provided by the runtime. |
||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
``` | ||||||||||||||
|
||||||||||||||
* Singleton (using a lock) | ||||||||||||||
|
||||||||||||||
```cs | ||||||||||||||
|
||||||||||||||
private object _lock = new object(); | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||
private MyClass _inst; | ||||||||||||||
|
||||||||||||||
public MyClass GetSingleton() | ||||||||||||||
{ | ||||||||||||||
if (_inst == null) | ||||||||||||||
{ | ||||||||||||||
lock (_lock) | ||||||||||||||
{ | ||||||||||||||
// taking a lock is an acquire, the read of _inst will happen after taking the lock | ||||||||||||||
// releasing a lock is a release, if another thread assigned _inst, the write will be observed no later than the release of the lock | ||||||||||||||
// thus if another thread initialized the singleton, the current thread is guaranteed to see that here. | ||||||||||||||
|
||||||||||||||
if (_inst == null) | ||||||||||||||
{ | ||||||||||||||
_inst = new MyClass(); | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
return _inst; | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may be better to use volatile read here to ensure side effects of the constructor, e.g. static field writes, are visible. runtime/src/libraries/System.Private.CoreLib/src/System/Threading/LazyInitializer.cs Lines 247 to 248 in c59b517
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need for volatile reads. Any data written directly or indirectly prior to the publishing of the instance will be visible via the instance. (see comments about the statics in the previous comment) |
||||||||||||||
} | ||||||||||||||
|
||||||||||||||
``` | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
* Singleton (using an interlocked operation) | ||||||||||||||
|
||||||||||||||
```cs | ||||||||||||||
private MyClass _inst; | ||||||||||||||
|
||||||||||||||
public MyClass GetSingleton() | ||||||||||||||
{ | ||||||||||||||
MyClass localInst = _inst; | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may be better to use volatile read here. See above for reasons. |
||||||||||||||
|
||||||||||||||
if (localInst == null) | ||||||||||||||
{ | ||||||||||||||
// unlike the example with the lock, we may construct multiple instances | ||||||||||||||
// only one will "win" and become a unique singleton object | ||||||||||||||
Interlocked.CompareExchange(ref _inst, new MyClass(), null); | ||||||||||||||
|
||||||||||||||
// since Interlocked.CompareExchange is a full fence, | ||||||||||||||
// we cannot possibly read null or some other spurious instance that is not the singleton | ||||||||||||||
localInst = _inst; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
return localInst; | ||||||||||||||
} | ||||||||||||||
``` | ||||||||||||||
|
||||||||||||||
* Communicating with another thread by checking a flag. | ||||||||||||||
|
||||||||||||||
```cs | ||||||||||||||
internal class Program | ||||||||||||||
{ | ||||||||||||||
static bool flag; | ||||||||||||||
|
||||||||||||||
static void Main(string[] args) | ||||||||||||||
{ | ||||||||||||||
Task.Run(() => flag = true); | ||||||||||||||
|
||||||||||||||
// the repeated read will eventually see that the value of 'flag' has changed, | ||||||||||||||
// but the read must be Volatile to ensure all reads are not coalesced | ||||||||||||||
// into one read prior entering the while loop. | ||||||||||||||
while (!Volatile.Read(ref flag)) | ||||||||||||||
{ | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
System.Console.WriteLine("done"); | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we also want to mention that reading and storing object references and unmanaged pointers is atomic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Object references are always aligned. We can mention that (if it is already not mentioned).
But unmanaged pointers are just numbers with dereference operation defined. They may not be aligned, then reading/storing is not necessarily atomic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you mean the pointers themselves. When managed by the runtime the pointers and references are "properly aligned" and thus read/writes are atomic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant the value of unmanaged pointer. Unmanaged pointer is not a primitive type, so the current wording does not cover it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it may be worth mentioning this. It follows from the "properly aligned" but pointers/references is a very common scenario.