1

Experts on threading/concurrency/memory model in .NET, could you verify that the following code is correct under all circumstances (that is, regardless of OS, .NET runtime, CPU architecture, etc.)?

class SomeClassWhoseInstancesAreAccessedConcurrently
{
    private Strategy _strategy;
    
    public SomeClassWhoseInstancesAreAccessedConcurrently()
    {
        _strategy = new SomeStrategy();
    }
    
    public void DoSomething()
    {
        Volatile.Read(ref _strategy).DoSomething();
    }

    public void ChangeStrategy()
    {
        Interlocked.Exchange(ref _strategy, new AnotherStrategy());
    }
}

This pattern comes up pretty frequently. We have an object which is used concurrently by multiple threads and at some point the value of one of its fields needs to be changed. We want to guarantee that from that point on every access to that field coming from any thread observe the new value.

Considering the example above, we want to make sure that after the point in time when ChangeStrategy is executed, it can't happen that SomeStrategy.DoSomething is called instead of AnotherStrategy.DoSomething because some of the threads don't observe the change and use the old value cached in a register/CPU cache/whatever.

To my knowledge of the topic, we need at least volatile read to prevent such caching. The main question is that is it enough or we need Interlocked.CompareExchange(ref _strategy, null, null) instead to achieve the correct behavior?

If volatile read is enough, a further question arises: do we need Interlocked.Exchange at all or even volatile write would be ok in this case?

As I understand, volatile reads/writes use half-fences which allows a write followed by a read reordered, whose implications I still can't fully understand, to be honest. However, as per ECMA 335 specification, section I.12.6.5, "The class library provides a variety of atomic operations in the System.Threading.Interlocked class. These operations (e.g., Increment, Decrement, Exchange, and CompareExchange) perform implicit acquire/release operations." So, if I understand this correctly, Interlocked.Exchange should create a full-fence, which looks enough.

But, to complicate things further, it seems that not all Interlocked operations were implemented according to the specification on every platform.

I'd be very grateful if someone could clear this up.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Adam Simon
  • 2,762
  • 16
  • 22
  • Please clarify this scenario: Thread 1 executes `Volatile.Read(ref _strategy)`, and then gets suspended by the OS. Five milliseconds later Thread 2 executes `ChangeStrategy()`. Another five milliseconds later Thread 1 resumes, and executes the strategy that had obtained before the suspension (the old strategy). is this a scenario that you would like to prevent? – Theodor Zoulias Oct 22 '22 at 12:35
  • 1
    No, it isn't. (I guess this scenario could only be prevented by locking.) What I want to achieve is that *after* Thread 1 has finished executing `ChangeStrategy`, all subsequent calls to `DoSomething` in any threads execute `AnotherStrategy.DoSomething`. – Adam Simon Oct 22 '22 at 13:33

1 Answers1

2

Yes, your code is safe. It is functionally equivalent with using a lock like this:

public void DoSomething()
{
    Strategy strategy;
    lock (_locker) strategy = _strategy;
    strategy.DoSomething();
}

public void ChangeStrategy()
{
    Strategy strategy = new AnotherStrategy();
    lock (_locker) _strategy = strategy;
}

Your code is more performant though, because the lock imposes a full fence, while the Volatile.Read imposes a potentially cheaper half fence.

You could improve the performance even more by replacing the Interlocked.Exchange (full fence) with a Volatile.Write (half fence). The only reason to prefer the Interlocked.Exchange over the Volatile.Write is when you want to retrieve the previous strategy as an atomic operation. Apparently this is not needed in your case.

For simplicity you could even get rid of the Volatile.Write/Volatile.Read calls, and just declare the _strategy field as volatile.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Thanks for the quick answer. I want to achieve exactly the behavior you described using `lock`s. But to what degree are you sure on a scale of, say, 1 to 10 that it's equivalent to my code considering all sorts of CPU architectures (not just x86/64)? Especially, in the case of using half-fences only (volatile field), which allows a write followed by a read to be reordered. Is this irrelevant in this case because only a single memory location is accessed whose value doesn't depend on other memory locations read/written concurrently? – Adam Simon Oct 22 '22 at 23:45
  • @Adam Simon my level of certainty is 9,9 out of 10. I am not expecting your code to have any problem, regarding the visibility of the updated `_strategy` from other threads, on any CPU architecture supported by the .NET. – Theodor Zoulias Oct 22 '22 at 23:56
  • @AdamSimon to put it in another way, if your code was problematic, lots of things in .NET would be problematic too. For example the [`CancellationToken`](https://learn.microsoft.com/en-us/dotnet/api/system.operationcanceledexception.cancellationtoken) would stop propagating cancellation to some threads, because its [internal state](https://github.com/dotnet/runtime/blob/4e85471628214a3e6257132cadd8f7fd33299eb0/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs#L38) is a `volatile` field. – Theodor Zoulias Oct 23 '22 at 00:08
  • Well, 9.9 out of 10 is pretty convincing! So is the usage of volatile in the sources of .NET. But statements like "Volatile is evil", "It doesn’t mean what you think.", etc. in articles like http://joeduffyblog.com/2010/12/04/sayonara-volatile/ makes one think twice whether it's a good idea to use it without fully understanding the underlying concepts, which seem to be getting a deeper and deeper rabbit hole as one's researching them... – Adam Simon Oct 23 '22 at 00:43
  • 1
    @AdamSimon you are right to be cautious with the `volatile`. My understanding is that the Microsoft people have committed some sins with this keyword in the past, regarding its effect and its documentation, and the guilt surfaced as dramatic articles and blog posts. When I was learning multithreading I was also confused with this keyword, but after reading lots of .NET source code I am now confident with it. You could take a look at [this related issue](https://github.com/dotnet/runtime/issues/67330 "Confusion regarding Volatile.Read guarantees"), that I opened on GitHub a few months ago. – Theodor Zoulias Oct 23 '22 at 00:58