Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is volatile required here

I'm implementing a 'sequence lock' class to allow locked write and lock-free reads of a data structure.

The struct that will contain the data contains the sequence value, which will be incremented twice while the write takes place. Once before the writing starts, and once after the writing is completed. The writer is on other threads than the reader(s).

This is what the struct that holds a copy of the data, and the sequence value looks like:

template<typename T>
struct seq_data_t
{
    seq_data_t() : seq(0) {};
    int seq;                     <- should this be 'volatile int seq;'?
    T data;
};

The whole sequence lock class holds N copies of this structure in a circular buffer. Writer threads always write over the oldest copy of the data in the circular buffer, then mark it as the current copy. The writing is mutex locked.

The read function does not lock. It attempts to read the 'current' copy of the data. It stores the 'seq' value before reading. Then it reads data. Then it reads the seq value again, and compares it to the value it read the first time. If the seq value has not changed, the read is deemed to be good.

Since the writer thread could change the value of 'seq' while a read is occurring, I'm thinking that the seq variable should be marked volatile, so that the read function will explicitly read the value after it reads the data.

The read function looks like this: It will be on threads other than the writer, and perhaps several threads.

    void read(std::function<void(T*)>read_function)
    {
        for (;;)
        {
            seq_data_type<T>* d = _data.current; // get current copy
            int seq1 = d->seq;      // store starting seq no
            if (seq1 % 2)           // if odd, being modified...
                continue;           //     loop back

            read_function(&d->data);  // call the passed in read function
                                      // passing it our data.


//??????? could this read be optimized out if seq is not volatile?
            int seq2 = d->seq;      // <-- does this require that seq be volatile?
//???????

            if (seq1 == seq2)       // if still the same, good.
                return;             // if not the same, we will stay in this
                                    // loop until this condition is met.
        }
    }

Questions:

1) must seq be volatile in this context?

2) in the context of a struct with multiple members, are only the volatile qualified variable volatile, and not the other members? i.e. is only 'seq' volatile if I only mark it volatile within the struct?

like image 856
ttemple Avatar asked Jan 29 '23 16:01

ttemple


2 Answers

Don't use volatile, use std::atomic<>. volatile is designed and meant to be used for interacting with memory mapped hardware, std::atomic<> is designed and meant to be used for thread synchronization. Use the right tool for the job.

Features of good std::atomic<> implementations:

  • They are lockless for standard integer types (everything up to long long, usually).

  • They work with any data type, but will use a transparent mutex for complex data types.

  • If std::atomic<> is lockless, it inserts the correct memory barriers/fences to achieve correct semantics.

  • Manipulations of std::atomic<> cannot be optimized away, they are designed for inter-thread communication after all.

like image 63
cmaster - reinstate monica Avatar answered Feb 02 '23 00:02

cmaster - reinstate monica


As said Is volatile required here - You sholud not use volatile for inter-thread synchronization. Here is why (from C++ standard):

[..] volatile is a hint to the implementation to avoid aggressive optimization involving the object because the value of the object might be changed by means undetectable by an implementation.[...]

What volatile doesn't do is ensure that the sequence of the operations (and especially memory reads and writes) in one thread is visible in the same order in other threads (due to superscalar architecture of modern CPUs) . For this you need memory barriers or memory fences (different names for same thing). Here is some more reading that you may find useful:

  • What is a memory fence?
  • memory orderes for memory barriers - http://en.cppreference.com/w/cpp/atomic/memory_order
like image 21
bartop Avatar answered Feb 02 '23 00:02

bartop