Search code examples
cmultithreadingcompiler-optimizationvolatile

Why does thread not recognize change of a flag?


I have a strange situation under C/Visual Studio on a Windows 7 platform. There is a problem from time to time and I spent a lot of time to find it. The problem is within a third party library, for which I have the complete code. There a thread is created (the printLog statements are from myself):

    ...
    plafParams->eventThreadFlag = 2;
    printLog("before CreateThread");
    if (plafParams->hReadThread_p = CreateThread(NULL, 0, ( LPTHREAD_START_ROUTINE ) plafPortReadThread, ( void * ) dlmsInstance, 0,
                                                 &plafParams->portReadThreadID) )
    {
        printLog("after CreateThread: OK");
        plafParams->eventThreadFlag = 3;
    }
    else
    {
        unsigned int lasterr = GetLastError();
        printLog("error CreateThread, last error:%x", lasterr);
        /* Could not create the read thread. */
        ...
        ...
        return FAILURE;
    }

    printLog("SUCCESS");
    ...
    ...

The thread function is:

    void *plafPortReadThread(DLMS_GLOBALS *dlmsInstance)
    {

        PLAF_PARAMS *plafParams;


        plafParams = (PLAF_PARAMS *)(dlmsInstance->plafParams);
        printLog("start - plafPortReadThread, plafParams->eventThreadFlag=%x", plafParams->eventThreadFlag);

        while ((plafParams->eventThreadFlag != 1) && (plafParams->eventThreadFlag != 3))
        {
            if (plafParams->eventThreadFlag == 0)
            {
                printLog("exit 1 - plafPortReadThread, plafParams->eventThreadFlag=%x", plafParams->eventThreadFlag);
                CloseHandle(plafParams->hReadThread_p);
                plafFree((void **)&plafParams);
                ExitThread(0);
               break;
            }
        }
        printLog("start - plafPortReadThread, proceed=%d", proceed);
        ...

Now, when the flag is set before the while loop is started within the thread, everything works OK:

SUCCESS
start - plafPortReadThread, plafParams->eventThreadFlag=3

But sometimes the thread is quick enough so the while loop is started before the flag is actually set within the outer part.

The output is then:

start - plafPortReadThread, plafParams->eventThreadFlag=2
SUCCESS

Most surprisingly the while loop doesn't exit, even after the flag has been set to 3.

It seems, that the compiler "optimizes" the flag and assumes, that it cannot be changed from outside.

What could be the problem? I'm really surprised. Or is there something else I have overseen completely? I know, that the code is not very elegant and that such things should better be done with semaphores or signals. But it is not my code and I want to change as little as possible.

After removing the whole while condition it works as expected. Should I change the struct or its fields to volatile ? Everybody says, that volatile is useless in our days and not needed anymore, except in the case, where a memory location is changed by peripherals...


Solution

  • Prior to C11 this is totally platform-dependent, because the effect you are observing is due to the memory model used by your platform. This is different from a compiler optimization as synchronization points between threads require the compiler to insert barrier instructions, instead of, e.g., making something a constant. For C11 for section 7.17.3 specifies the different models. So your value is not optimized out statically, thread A just never reads the value thread B wrote, but still has its local value.

    In practice many projects don't use C11 yet, and thus you will likely have to check the documentation of your platform. Note that in many cases you don't have to modify the type of the variable for the flag (in case you can't). Most memory models specify synchronization points that also forbid reordering of certain instructions, i.e. in:

    int x = 3;
    _Atomic int a = 1;
    x = 5;
    a = 2;
    

    the compiler will often have to ensure that x has the value 3 when a has the value 1, and that when a is assigned the value 2, x will have the value 5. volatile does not participate in this relationship (in the C/C++ 11 models - often confused because it does participate in Java's happened-before), and is mostly useless, unless your writes should never be optimized out because they have side-effects such as a LED blinking which the compiler can't understand:

    volatile int x = 1; // some special location - blink then clear 
    x = 1; // blink then clear
    x = 1; // blink then clear