1
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        

        private void Form1_Load(object sender, EventArgs e)
        {
            Task.Run(() => { SomeWork(); });
        }
        
        private void SomeWork()
        {
            while (true) {
                // Check config
                if (Config.SomeBool) {
                    // Do something
                } else {
                    // Do something else
                }

                Thread.Sleep(100);
            }
        }

        private void checkBox1_CheckedChanged(object sender, EventArgs e)
        {
            Config.SomeBool = checkBox1.Checked;
        }
    }

    public static class Config
    {
        public static bool SomeBool { get; set; }
    }
}

Let's say in my winforms application, I have a thread running in the background doing some work in a loop. It needs to to different things based on some Config that can be modified by user on the UI thread.

C# Specification says

Reads and writes of the following data types shall be atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types.

I don't quiet understand how "atomic" works in C#. Let's get back to my program above, if I manually changes some value of Config class via the checkbox, is the background thread always gonna receive the latest Config.SomeBool value, or should I lock it like this:

        private readonly static object _someBoolLock = new object();
        private static bool _someBool;
        public static bool SomeBool
        {
            get
            {
                lock (_someBoolLock) {
                    return _someBool;
                }
            }
            set
            {
                lock (_someBoolLock) {
                    _someBool = value;
                }
            }
        }

I'm asking because the program kinda runs "fine" without the lock but I can't say I tested it enough to be sure that it's okay to do so. The static Config class has many more properties in my real program, I wonder if I should lock them all, given they may be read by othe threads?


EDIT: I forgot to mention that I don't care if Config.SomeBool become false while // Do something , all I care is everytime the background thread checks the value, it will always be the same as shown on the UI. If user changes it during // Do something, the background thread should finish it nontheless.

While trying to search for this problem I got many conflicted answers most likely from unexperienced coders like myself. Basically it comes down to:

  • Will the value Config.SomeBool be cached somewhere in the background thread? If so, how long would it be held in cache? (If this loop gets the old value, would next loop get the new value or still the old one?)
  • What's the correct approach to ensure the value to be "latest" every time it's read by any thread?

EDIT: I've post what I read during the past few days as an answer below. If you found this post from a search result, please do read it. The short answer to the question in the title is "no".

RadarNyan
  • 306
  • 1
  • 2
  • 12
  • There is no need to lock the variable as you've done in your second example. The race condition that matters in code is if the value of `Config.SomeBool` matters after you enter your if statement. – Loocid Feb 27 '23 at 01:43
  • @Loocid I forgot to mention that the value doesn't matter after entering the if statement. I've edited the question to add more information on that :) – RadarNyan Feb 27 '23 at 02:11
  • If Class `Config` had more than one attribute, and you need to change multiple attributes at the SAME TIME, as an ATOMIC OPERATION, then you'd need the `lock` approach so that all of the values could be changed together. You'd need to write a method that receives all of the values together somehow, then lock, write all the values, and release. Additionally, the `SomeWork()` method would need to acquire the lock as well so that none of the values change while it accesses information. – Idle_Mind Feb 27 '23 at 02:37
  • In a nutshell at a CPU level, when you write a single machine word (eg 64bit value on a 64bit processor) [to an aligned memory address], other threads will either read the old value or the new value. Never a mixture of bits from both values. Other atomic operations or locking, is mainly to ensure that any reading thread will only see a consistent set of data. – Jeremy Lakeman Feb 27 '23 at 04:19
  • 1
    Note that the significant part why you are finding only answers from "unexperienced coders" is because the experienced once will not try to write lock-free code and hence will have no guidance to provide on the subject. As result you really have to find experts in .Net memory model (which are quite rare) to get any reliable information. To help with your searches consider adding "volatile" , "read/write reordering", "memory model" to your search queries. – Alexei Levenkov Feb 27 '23 at 05:38
  • That's liberal usage of the term "atomic". To most programmers that means that memory is accessed in a thread-safe way. That's not what happens in .NET, there is no guarantee that memory actually gets accessed. [Example](https://stackoverflow.com/questions/41632387/this-code-hangs-in-release-mode-but-works-fine-in-debug-mode). What they meant is that no ["tearing"](https://learn.microsoft.com/en-us/archive/msdn-magazine/2008/october/concurrency-hazards-solving-problems-in-your-multithreaded-code#read-and-write-tearing) can occur. You must use the Interlocked class to make this code safe. – Hans Passant Feb 27 '23 at 11:33

3 Answers3

0

Each single access is atomic.

If that’s the only need you have you’re fine. If you need it again in the same thread you need to copy it and use the copy.

If you need other state that could be modified you need to lock around all the accesses.

In response to your additional questions...

Will the value Config.SomeBool be cached somewhere in the background thread?

No, unless you copy it to another variable. A bool assignment is a copy. All bets are off with the original variable, but the copy is now safe to use.

If so, how long would it be held in cache? (If this loop gets the old value, would next loop get the new value or still the old one?)

That's indeterminate.

What's the correct approach to ensure the value to be "latest" every time it's read by any thread?

Again, if all you are looking to synchronize access for is a single type defined as atomic by the spec, then when you access you are getting the latest value at that time. It could change immediately after your access. There is nothing you can do about that.

But, again, if you need to safely secure access to more than one piece of state, you need a lock. Keep in mind that locks are synchronizing imperative code. They are not synchronizing data. But if your lock is short, and just around "access" code, it's effectively a lock around data. Effectively.

Kit
  • 20,354
  • 4
  • 60
  • 103
  • 1
    You probably should not oversimplify "when you access you are getting the latest value at that time" - there is no guarantees that code will read the latest value. Without proper synchronization (or at least memory barriers if you understand that part) there is no guarantees that latest value will show up fast enough for different threads/cores. – Alexei Levenkov Feb 27 '23 at 05:29
  • @AlexeiLevenkov: That kind of phrasing implies that "the latest value" is a meaningful thing to talk about for pure stores, not just atomic RMWs which must serialize with other atomic RMWs on the same variable. (For atomic RMWs, "latest" means "latest in the modification order for that variable".) For pure loads / pure stores, the important question is whether each access in a loop will force the compiler to do a load in the JIT asm / machine code, like C++ `std::atomic` (even with `memory_order_relaxed`), or if it can optimize it away like C++ and C# plain variables. – Peter Cordes Feb 28 '23 at 07:26
  • @AlexeiLevenkov: see [Is a memory barrier required to read a value that is atomically modified?](https://stackoverflow.com/a/71722126) re: debunking the "latest value" panic. But yes, you do need something, to avoid letting the compiler do stuff like hoist loads out of loops. The C / C++ equivalent of that is discussed in [MCU programming - C++ O2 optimization breaks while loop](https://electronics.stackexchange.com/a/387478) - atomicity isn't sufficient, you do need guaranteed visibility with C++ `std::atomic` also gives even with `memory_order_relaxed`. – Peter Cordes Feb 28 '23 at 07:27
0

I don't quiet understand how "atomic" works in C#.

Atomic in C# means that it's not possible for a thread to observe a torn value. A thread will read the whole value that was previously written by another thread, and not a Frankenstein value that never existed, made of bits and pieces written by more than one threads.

If I manually changes some value of Config class via the checkbox, is the background thread always gonna receive the latest Config.SomeBool value, or should I lock it like this?

It is theoretically possible that the C# compiler or the .NET jitter will try to "optimize" the while loop, by reading the Config.SomeBool once and never reading it again, resulting in an infinite loop. This might happen in case the compiler is able to analyze the // Do something and // Do something else code blocks, and conclude that there is no code in there that changes the backing field of the Config.SomeBool property. This is not very likely to happen, and if it happens it's likely that you'll observe it during the debugging. But it is also possible that it'll never happen on the machine/OS configurations that you test your application, and instead it will happen on your client's machine. So it's not advisable to leave this possibility open, and using the lock statement as you did is a simple and safe way to prevent it from happening.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • I've done some more search and reading during the day using some keywords given by the commenters on the question, during which I came across [Threading in C#, by Joe Albahari](https://www.albahari.com/threading/part4.aspx#_The_volatile_keyword) which did make my brain hurt a bit. In the article which I've linked it mentions that write-read can still be swapped while using the volatile keyword which I tried to understand how that works but not sure if I really did which makes me a little afraid of using it. Could you provide some insight on this? – RadarNyan Feb 27 '23 at 12:37
  • @RadarNyan Joe Albahari is discussing the implications of modifying two fields, and how the order that the fields are modified might seen to be different from the perspective of another thread. In your case you have a single shared field, the `_someBool` field, whose value is independent from the other fields of the `Config` class. At least that's the scenario that is presented in the question. In case there are multiple fields, and their meanings are interconnected, then it's a whole different story. To which neither the `volatile`, nor `lock`ing each field individually, is a solution. – Theodor Zoulias Feb 27 '23 at 13:09
  • After reading more on `atomic`, `volatile` and `memory fence`, how they affect or be affect by software and hardware optimization, I've come to the conclusion to stick with `lock`. And I've decided to use a public locking object instead of locking individually in getter / setter. – RadarNyan Feb 28 '23 at 02:37
  • Re: optimizing `while(keep_running){}` into `if(keep_running) while(1){}` - [MCU programming - C++ O2 optimization breaks while loop](https://electronics.stackexchange.com/a/387478) and [Multithreading program stuck in optimized mode but runs normally in -O0](https://stackoverflow.com/q/58516052) are real examples of code that did break that way, in C++. Their loop bodies were vastly simpler, which is why compilers were able to prove (for code which didn't violate the language rules) the safety of hoisting the loads. – Peter Cordes Feb 28 '23 at 07:34
  • (That code is of course buggy, C and C++ *don't* guarantee anything for concurrent write+read of non-atomic vars, it's UB. C# doesn't have undefined behaviour, instead an atomicity guarantee without a visibility guarantee for unsynchronized plain write+read.) – Peter Cordes Feb 28 '23 at 07:34
  • 1
    @RadarNyan I can understand your current mindset perfectly. I was myself in the same mindset when I first learned about volatility, memory models (strong and weak ones), differences in CPU architectures, differences in the language vs CLR specifications etc. It's terrifying. When all you want is to ensure that your bloody loop will not hang, going with the safer option (`lock`) that requires the less reading and is endorsed by leading C# figures is the reasonable thing to do. A few years in the future you might review your code, and all these `lock`s will look funny though. :-) – Theodor Zoulias Feb 28 '23 at 08:19
-2

After reading more on this topic, I think I should make this question more clear for later readers. The answer to the question in the title is: NO.

Atomic operations (the list is given in the question) just means that if a variable is being written by one thread, other threads would either see the old value or the updated value, not some garbage in-between value. However, there's no guarantee on when the updated value would be seen by any thread.

lock on the other hand is the much safer option since if one thread tries to read while another thread is updating, it must wait for the lock to be released, thus getting the updated value is guaranteed. Of course, this would lead to performance loss, but in my use case it's better to wait and get the updated value, rather than keep using the stalled value.


UPDATE: It's been pointed out to me that the following content deviates too much from what is asked in the question.

  • If you come here for the answer in the title, you should already know it's "no".
  • If you want to know the correct way to ensure synchronization, use lock or read Overview of synchronization primitives on microsoft.com.
  • If you're searching this topic and come across some suggestions of using volatile as a solution, it is incorrect. It was designed to prevent unwanted optimization, not for synchronization.
  • If you want to know what I get on the topic during the last few days reading, you can read on. If not, you can safely leave this page now :)

(end of update)


From MSDN on the volatile keyword

On a multiprocessor system, a volatile read operation does not guarantee to obtain the latest value written to that memory location by any processor. Similarly, a volatile write operation does not guarantee that the value written would be immediately visible to other processors.

Also, from MSDN on the Volatile class

Sequence Thread 1 Thread 2
1 x = 1; ...
2 Volatile.Write(ref y, 1); ...
3 ... int y2 = Volatile.Read(ref y);
4 ... int x2 = x;

Even though the volatile write to y on thread 1 occurred before the volatile read of y on thread 2, thread 2 may still see y2 == 0. The volatile write to y does not guarantee that a following volatile read of y on a different processor will see the updated value.

Yet, just one line below, on the same page (on Volatile class) it states:

 Volatile reads and writes ensure that a value is read or written to memory and not cached (for example, in a processor register). Thus, you can use these operations to synchronize access to a field that can be updated by another thread or by hardware.

...can I really? I'm not so sure after reading what's written above. What is the term "processor" in this context even referring to? Are modern multicore CPUs count as having multiple of them?

Trying to search how "volatile" means outside the scope of C# opens a whole new can of worms:

  Note that volatile variables are not suitable for communication between threads; they do not offer atomicity, synchronization, or memory ordering. A read from a volatile variable that is modified by another thread without synchronization or concurrent modification from two unsynchronized threads is undefined behavior due to a data race.
(Quote from: https://en.cppreference.com/w/c/language/volatile)

At this point, I just gave up.


EDIT: I suppose I should be more specific about what I "gave up" about, what I mean was I've come to the conclusion that "volatile" isn't the answer to "ensure the value read is the latest". The only proper way of doing cross thread synchronization is to use a lock, or basically inventing my own locking mechanism, which I doubt I can do better than the experts who designed the language.

"volatile" in C# isn't the same as "volatile" in C, it provides more guarantee but not enough to make it useable for cross thread synchronization.

According to C# Specification, volatile fields only ensure that:

all threads will observe volatile writes performed by any other thread in the order in which they were performed.

And there's an example given in the specification:

class Test
{
    public static int result;
    public static volatile bool finished;

    static void Thread2()
    {
        result = 143;
        finished = true;
    }

    static void Main()
    {
        finished = false;

        // Run Thread2() in a new thread
        new Thread(new ThreadStart(Thread2)).Start();    

        // Wait for Thread2() to signal that it has a result
        // by setting finished to true.
        for (;;)
        {
            if (finished)
            {
                Console.WriteLine($"result = {result}");
                return;
            }
        }
    }
}

In this example, the method Main starts a new thread that runs the method Thread2. This method stores a value into a non-volatile field called result, then stores true in the volatile field finished. The main thread waits for the field finished to be set to true, then reads the field result. Since finished has been declared volatile, the main thread shall read the value 143 from the field result. If the field finished had not been declared volatile, then it would be permissible for the store to result to be visible to the main thread after the store to finished, and hence for the main thread to read the value 0 from the field result. Declaring finished as a volatile field prevents any such inconsistency.

Basically the volatile keyword here ensures that:

  • When Thread 2 writes to finished, it must have already finished writting to result
  • Thus if the main thread sees finished = true, it must read result = 143 since volatile prevents the value of result field being "pre-read"

However, there is no guarantee that when Thread 2 sets finished = true, the next time main thread checks the finished field it will see true. The main thread could still read a stalled value false from the field, and eventually it will see the updated value true then proceeds to read result = 143, this is what "prevents inconsistency" means.

How long will the main thread see the stalled value? Since it's not written in the specification, it's perfectly legal for main thread to see the stalled false value forever as long as it doesn't read the field result. In reality this doesn't happen, the main thread will see the updated true value eventually, and that's why codes (IMO, mistakenly) using volatile for synchronization do "work". But how long does it take to reach this point of "eventually" is unspecified. (Please do correct me if you can find any specification on this matter.)

By the way, volatile is not listed on the MSDN page Overview of synchronization primitives

More on this:

Atomicity, volatility and immutability are different, part three by Eric Lippert

RadarNyan
  • 306
  • 1
  • 2
  • 12
  • 1
    The `_state` of the `CancellationTokenSource` class is a `volatile int` field ([source code](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenSource.cs#L38)). Whenever you read the `IsCancellationRequested`, you read this field. Are you truly worried that a thread that has read the `IsCancellationRequested` as `false` at one moment, will continue seeing it as `false` forever? Numerous other properties are based on `volatile` fields, like the `Task.IsCompleted`, the `Lazy.Value`, the `SemaphoreSlim.CurrentCount` etc. – Theodor Zoulias Mar 04 '23 at 18:38
  • 1
    @TheodorZoulias, yup, looks like yet another case of panic and confusion over "latest value" guarantees. [Is a memory barrier required to read a value that is atomically modified?](https://stackoverflow.com/a/71722126) is related but about C++. Nothing can make a store visible *immediately*, if you start counting from when the store speculatively executes locally; at that point it hasn't committed to L1d cache so isn't globally visible. – Peter Cordes Mar 04 '23 at 21:28
  • 1
    If there isn't any previous synchronization between threads, there's no way to say how far ahead one is relative to the other; if you use a lock, the reader might get the lock before the writer and not see the value. That bolded text from the MSDN example is weird; how do you define "following" between threads? If a load didn't see the value, it happened later (in the modification order of `y`). Other than that, it's just stating the obvious; if the load sees the store, it creates synchronization between threads. If not, it doesn't. – Peter Cordes Mar 04 '23 at 21:30
  • 1
    `Volatile.Write` makes sure your code JITs to asm that does an actual store; inter-thread latency is then up to hardware, like maybe 30 to 70 nanoseconds. You can't make this faster, all you can do is make the reader run slower, or make the writer wait for the store to become visible to other threads before doing any later memory ops. [Difference between Interlocked.Exchange and Volatile.Write?](https://stackoverflow.com/a/71002288) – Peter Cordes Mar 04 '23 at 21:32
  • 1
    *"ensure the value read is the latest"* Well yeah, that's a nonsensical goal. If the value is ever-changing, you just need a recent value that might make sense, and some way of detecting if there's any problem. Often a CAS, since you can retry if it fails. (Atomic RMWs do always see the latest value, that's how atomic increments can avoid repeating or missing numbers for example.) Stopping all other threads from being able to store temporarily is indeed the only real way to make sure there aren't stores from other threads in flight, but if you need that you don't have a lock-free algorithm. – Peter Cordes Mar 05 '23 at 08:44
  • 1
    *Since it's not written in the specification, it's perfectly legal for main thread to see the stalled false value forever* - Is it? Does the C# standard not have any language like the C++ standard such as "finite time" or "promptly"? Usable implementations *will* make volatile stores visible to volatile loads promptly, by compiling them to store and load instructions. Hardware cache-coherency means you'll typically see tens of nanoseconds of latency before a load of the new value can complete in another core, with worst case being maybe a microsecond with high contention. – Peter Cordes Mar 05 '23 at 08:50
  • C# `volatile` (and Volatile.Write / Volatile.Read) are essentially equivalent to C++ `std::atomic` with `memory_order_release` and `memory_order_acquire`. – Peter Cordes Mar 05 '23 at 08:51
  • 1
    Of course the language standard itself can't specify any hard performance numbers, that depends on the implementation, which includes the hardware. The best the standard can do is say "promptly", and for quality implementations not to compile-time-reorder `volatile` (or C++ `atomic`) loads/stores to the other side of a very long-running loop, especially not an infinite one. And to just JIT compile volatile read/write to load/store in the machine code. – Peter Cordes Mar 05 '23 at 08:56
  • See also [Does hardware memory barrier make visibility of atomic operations faster in addition to providing necessary guarantees?](https://stackoverflow.com/q/61591287) and [MESI Protocol & std::atomic - Does it ensure all writes are immediately visible to other threads?](https://stackoverflow.com/q/60292095) . [What are the latency and throughput costs of producer-consumer sharing of a memory location between hyper-siblings versus non-hyper siblings?](https://stackoverflow.com/q/45602699) has some benchmarks of real x86 hardware. – Peter Cordes Mar 05 '23 at 08:58
  • 1
    I think that your answer deviates too much from what is asked in the question. The question asks about the relation between atomicity and synchronization. Your answer correctly states that atomicity doesn't ensure synchronization. But then you start bashing the `volatile` keyword, which is not even mentioned in the question, because it doesn't ensure that other non-volatile fields will be read in the correct order. Implying that `lock` would ensure it, which it doesn't either. The example in the question shows a single shared field, so arguing about multiple fields is off-topic IMHO. – Theodor Zoulias Mar 05 '23 at 09:04
  • @TheodorZoulias If it sounds like "bashing", that's not my intent. During my searching, the "magic fix" of making the field `volatile` pops up a lot, which kinda "works" but is the result of side effects, rather than the intended use case. This is what I'm trying to emphasize. `volatile` has its own purpose, it guarantees the **order** of the operations, thus providing **consistency** to multiple related fields.  It has nothing to do with synchronization, even for a single `atomic` field. – RadarNyan Mar 05 '23 at 16:55
  • RadarNyan and why do you think that your research about the `volatile` keyword is relevant with the current question? Here we are supposed to answer the question. StackOverflow is a Q/A site. You could search for a question that asks specifically about this keyword, like [this one](https://stackoverflow.com/questions/72275/when-should-the-volatile-keyword-be-used-in-c), and post your research there. To make your answer more valuable, make sure to include specific and practical examples where using the `volatile` keyword is appropriate and inappropriate. – Theodor Zoulias Mar 05 '23 at 17:36
  • Why so personal? I don't see how my answer about "`lock` should be used and why `volatile` isn't a suitable alternative" is unrelated. For the record, I'm not the one who brought `volatile` to this question, and I don't think it's unrelated since it comes up **a lot** when searching on this topic. From what I've read, neither writing to a `volatile` field nor using the `Volatile.Write` method would generate any memory barrier _after_ the write operation, which means there's no guarantee the updated value is immediately visible to other threads, which makes it unsuitable for synchronization. – RadarNyan Mar 06 '23 at 11:57
  • 1
    My perception is that your answer aims at refuting [my answer](https://stackoverflow.com/a/75579288/11178549), and not at answering the question. This is not how this site works. It's a Q/A site, not a forum. It doesn't matter who is the author of the other answer. I would say the same regardless. It's not personal. Anyway, I removed from my answer all references to the `volatile` keyword, for not being directly relevant with what is asked in the question. I admit that it was a digression from my part, and I reckon that my answer has been improved by removing the off-topic digression. – Theodor Zoulias Mar 06 '23 at 15:17