views:

136

answers:

4

I have a question concerning volatile keyword I can't seem to find an answer for.

In my app I have data class that is shared as a state buffer between threads, and I need it to be updated regularly from multiple threads.

Class looks like this:

class CBuffer
{
    //Constructor, destructor, Critical section initialization/destruction
    //...

    volatile wstring m_strParam;
    //...
    void InterlockedParamSetter(const wstring &strIn);
    wstring InterlockedParamGetter();

    ParamSetter(const wstring &strIn);
    wstring ParamGetter();

    Lock();
    Unlock();
}

void CBuffer::InterlockedParamSetter(const wstring &strIn)
{
    Lock();
    const_cast<wstring>(m_strParam) = strIn;
    Unlock();
}

//... other f-ns

But the compiler complains about const_cast conversion.

It almost looks like I'm abusing volatile keyword, but at the same time, I can't let the params to be cached between the calls, because if two or three threads will assign them, something can go wrong.

How do you write thread/cache safe classes in C++?

P.S.: So far locking is not the bottleneck, and the locks are pretty much single-liners, so for now serialization and locking is not an issue from performance standpoint. Of course, if there is a better algorithm, I will gladly listen.

EDIT: I'm still unclear...

Consider this example (inlining + link time codegen);

void Thread1Func()
{
    //Unrolled, inlined InterlockedParamSetter()
    EnterCriticalSection(&cs);
    WriteTo(CBuffer::m_strParam);//write to buffer, wstring not volatile, cache it
    LeavCriticalSection(&cs);
    //Unroll end

    //DoSomethingElse

    //!!!!Thread 2 does InterlockedParamSetter
    //which causes wstring::reserve and invalidates old pointers!!!!

    //Unrolled, inlined InterlockedParamSetter()
    EnterCriticalSection(&cs);
    WriteTo(CBuffer::m_strParam);//oh, good, we have a pointer to old buffer
    //cached in one of the registers, write to it -> access violation
    LeavCriticalSection(&cs);
    //Unroll end
}
+2  A: 

std::string (and std::wstring) wasn't designed to be volatile and I would advise you against using it that way.

The usual way to ensure thread-safety is to use mutexes, semaphores, where needed and to avoid the use of global variables.

And as Marcus Lindblom mention, there are usually read/write fence in the lock mechanisms that take care of dealing with potential caching issues. So you should be safe.

ereOn
The lock and unlock is performed via critical section, but does that guarantee that the contents of the wstring won't be cached if it's not volatile. Say thread 1 gets the value, caches the internal pointer (?because of link time code gen + inlining?), 2nd thread writes to wstring, first thread overwrites the value via direct cached pointer access.
Madman
@MadMan: There are usually read/write fences in the locks that take care of this. (I.e. state sync across threads on lock/unlock).
Marcus Lindblom
@Marcus Lindblom: Thanks, I will update my answer to make it more accurate.
ereOn
+2  A: 

You should const_cast strIn not m_strParam:

m_strParam = const_cast<wstring> (strIn);

There are no generally accepted idioms about thread/cache safe classes in C++, as the current prevalent C++ standard is silent about concurrent programming. volatile does not make any guarantees of atomicity and is useless in writing portable, thread-safe programs. See these links:

Vijay Mathew
I don't think the compiler is at liberty to ignore `volatile`. It may ignore `register` or `inline`, but not `volatile`.
DevSolar
Quite so. If you want a systems-programming language that is designed around concurrent programming, look at Ada. Either that, or you get to wait until some future C++ standard where they manage to bolt it on somehow.
T.E.D.
@DevSolar A compiler may not ignore `volatile` but it is of no help in writing multithreaded programs. I have updated my answer.
Vijay Mathew
A: 

No need to use volatile. A compiler memory barrier should be sufficient. The Lock/Unlock is still needed, though. That is,

class CBuffer
{
    //Constructor, destructor, Critical section initialization/destruction ...
    wstring m_strParam;
};

void CBuffer::InterlockedParamSetter(const wstring &strIn)    
{
    Lock();
    //compiler-specific memory barrier;
    m_strParam = strIn;
    //compiler-specific memory barrier;
    Unlock();
}

Although on some compilers, the volatile keyword has the same meaning as memory barrier when applied to non-primitive types like wstring. Case in point: VC++

Marking memory with a memory barrier is similar to marking memory with the volatile (C++) keyword. However, a memory barrier is more efficient because ...

rwong
+2  A: 

In portable code, volatile has absolutely nothing to do with multithreading.

In MSVC, as an extension, volatile-qualified simple native types such as int can be used with simple read and store operations for atomic accesses, but this does not extend to read-modify-write accesses such as i++ or to objects of class type such as std::string.

To ensure safe access to your m_strParam variable from multiple threads, each thread must lock the associated mutex before accessing m_strParam, and unlock it afterwards. The lock/unlock will ensure that the correct value of m_strParam is seen by each thread --- there will not be any caching of the internal pointer if another thread has modified the variable by the next lock.

If you use locks correctly, you do not need the volatile qualifier, nor do you need to use const_cast.

Anthony Williams
"there will not be any caching of the internal pointer if another thread has modified the variable by the next lock." Is there any documentation on that, I can't find any. See edit in the OP.
Madman
Unlocking and then-locking the **same** mutex will guarantee that any changes made by the unlocking thread before the `unlock()` call are visible to the locking thread after the `lock()` call. If the `cs` in yout `Thread1Func` is the same `CRITICAL_SECTION` referenced by `InterlockedParamSetter` then this will be fine. If it is a different critical section then it doesn't gain you anything --- locks on independent mutexes do not impose ordering or visibility constraints on each other.
Anthony Williams
Then I don't need the memory barrier or any other specific marker? Plain wstring guarded by same CS in all threads is enough?
Madman
Yes, that's right.
Anthony Williams