Do I need a lock when only a single thread writes to a shared variable?
Asked Answered
H

7

11

I have 2 threads and a shared float global. One thread only writes to the variable while the other only reads from it, do I need to lock access to this variable? In other words:

volatile float x;

void reader_thread() {
    while (1) {
        // Grab mutex here?
        float local_x = x;
        // Release mutex?
        do_stuff_with_value(local_x);
    }
}

void writer_thread() {
    while (1) {
        float local_x = get_new_value_from_somewhere();
        // Grab mutex here?
        x = local_x;
        // Release mutex?
    }
}

My main concern is that a load or store of a float not being atomic, such that local_x in reader_thread ends up having a bogus, partially updated value.

  1. Is this a valid concern?
  2. Is there another way to guarantee atomicity without a mutex?
  3. Would using sig_atomic_t as the shared variable work, assuming it has enough bits for my purposes?

The language in question is C using pthreads.

Hogle answered 13/5, 2009 at 18:44 Comment(3)
why do you want to avoid using a mutex?Stage
The reader thread will run every frame of a game as a plugin. While it's probably premature optimization, I wanted to avoid any overhead that I can. That said, I'm convinced by the answers that I should use a mutex, and I doubt that amount of overhead will be a problem.Hogle
Thanks for all the answers. Sounds like I shouldn't count on the read/write being atomic, especially for portability, so locking seems to be the way to go.Hogle
F
14

Different architectures have different rules, but in general, memory loads and stores of aligned, int-sized objects are atomic. Smaller and larger may be problematic. So if sizeof(float) == sizeof(int) you might be safe, but I still wouldn't depend on it in a portable program.

Also, the behavior of volatile isn't particularly well-defined... The specification uses it as a way to prevent optimizing away accesses to memory-mapped device I/O, but says nothing about its behavior on any other memory accesses.

In short, even if loads and stores are atomic on float x, I would use explicit memory barriers (though how varies by platform and compiler) in instead of depending on volatile. Without the guarantee of loads and stores being atomic, you would have to use locks, which do imply memory barriers.

Furst answered 13/5, 2009 at 19:2 Comment(3)
"memory loads and stores of aligned, int-sized objects are atomic" -> yes, but the memory propogation isn't. So setting x = 1 is indeterminatly visible to other cores. You CANNOT use this method with multiple CPUs. Even a lock doesn't help you here, unless it contains an implicit memory barrier, because the lock could come and go while the write you wanted still hasn't propogated.Quinque
Memory propagation between CPUs is usually on cachelines, but you're write that ordering isn't guaranteed. However, I have not yet seen a locking library which does not contain implicit memory barriers.Furst
Locks would not be of much use unless they provided a memory barrier. Herb Sutter has written a great article on this (www.drdobbs.com/cpp/201804238?pgno=2)Fuzzy
A
6

According to section 24.4.7.2 of the GNU C library documentation:

In practice, you can assume that int and other integer types no longer than int are atomic. You can also assume that pointer types are atomic; that is very convenient. Both of these assumptions are true on all of the machines that the GNU C library supports and on all POSIX systems we know of.

float technically doesn't count under these rules, although if a float is the same size as an int on your architecture, what you could do is make your global variable an int, and then convert it to a float with a union every time you read or write it.

The safest course of action is to use some form of mutex to protect accesses to the shared variable. Since the critical sections are extremely small (reading/writing a single variable), you're almost certainly going to get better performance out of a light-weight mutex such as a spin lock, as opposed to a heavy-weight mutex that makes system calls to do its job.

Aggravate answered 13/5, 2009 at 19:9 Comment(1)
+1 Good point; spin locks are better-performing than more complex locking mechanisms when there's no contention.Furst
S
3

I would lock it down. I'm not sure how large float is in your environment, but it might not be read/written in a single instruction so your reader could potentially read a half-written value. Remember that volatile doesn't say anything about atomicity of operations, it simply states that the read will come from memory instead of being cached in a register or something like that.

Stephanystephen answered 13/5, 2009 at 18:54 Comment(0)
S
3

The assignment is not atomic, at least for some compilers, and in the sense that it takes a single instruction to perform. The following code was generated by Visual C++ 6.0 - f1 and f2 are of type float.

4:        f2 =  f1;
00401036   mov         eax,dword ptr [ebp-4]
00401039   mov         dword ptr [ebp-8],eax
Stage answered 13/5, 2009 at 19:16 Comment(1)
Yeah, usually there are only "move memory to register" and "move register to memory" instructions, no "move memory to memory". I don't think this has any special bearing on OP's question, though; if this were in reader_thread, "copy x to register" is atomic, and "copy register to local_x on my stack" is safe; the situation is symmetric in writer_thread. reader_thread may see a slightly out-of-date value, but that's a lot better than seeing an inconsistent value.Furst
J
2

In the memory model introduced by C11 and later, the clear answer is yes: you do need a lock or other means of synchronization, or else to declare the variable x as atomic_float using <stdatomic.h>.

If a non-atomic variable is written by one thread, and either read or written by another, without appropriate synchronization to ensure that one access happens before the other in the precise sense defined in the standard, then a data race exists and the behavior of the program becomes undefined. (In particular, the bad effects need not be limited to just getting a bogus value when you read the variable; the program is allowed to crash, corrupt unrelated data, etc.)

Note that the presence of volatile is irrelevant. Declaring a variable volatile does not save you from UB when a data race otherwise exists, and if a data race is avoided by use of atomic_float or otherwise, then volatile is not needed.

Jarrad answered 9/10, 2022 at 19:24 Comment(0)
P
0

Since it's a single word in memory you're changing you should be fine with just the volatile declaration.

I don't think you guarantee you'll have the latest value when you read it though, unless you use a lock.

Proudfoot answered 13/5, 2009 at 18:48 Comment(3)
Explain the down mod, please. I'm saying the same as the accepted answer albeit shorter.Proudfoot
I'm going to upvote to cancel the -1 since I don't think your answer deserves a negative. My guess is whoever downvoted did so because even if I did put the locks in, that will still not guarantee that I'd have the latest value (the reader thread could run after the writer thread receives a new value, but before it assigns it to the shared variable).Hogle
Note that this answer is obsolete for C11 and later.Jarrad
A
0

In all probability, no. Since you have no chance for write collision the only concern is whether you could read it while it's half-written. It's hugely unlikely that your code is going to be run on a platform where writing a float doesn't happen in a single operation if you're writing something with threads.

However it's possible because the definition of a float in C does not mandate that the underlying hardware storage be limited to the processor's word size. You could be compiling to machine code where, say, sign and mantissa are written in two different operations.

The real question, I think, is two questions: "what's the downside to having a mutex here?" and "What's the repercussions if I get a garbage read?"

Perhaps rather than a mutex you should write an assert that determines whether the storage size of a float is smaller or equal to the word size of the underlying CPU.

Achromatous answered 13/5, 2009 at 19:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.