skip to Main Content
  1. Using (writing) same variable in multiple threads simultaneously causes undefined behavior and crashes.
    Why using mutex, despite on fact that they are also variables, not causes undefined behavior?

  2. If mutex somehow can be used simultaneously, why not make all variables work simultaneously without locking?

All my research is pressing Show definition on mutex::lock in Visual Studio, where I get at the end _Mtx_lock function without realization, and then I found it’s realization (Windows), though it has some functions also without realization:

 int _Mtx_lock(_Mtx_t mtx)
 {    /* lock mutex */
 return (mtx_do_lock(mtx, 0));
 }

 static int mtx_do_lock(_Mtx_t mtx, const xtime *target)
 {    /* lock mutex */
 if ((mtx->type & ~_Mtx_recursive) == _Mtx_plain)
     {    /* set the lock */
     if (mtx->thread_id != static_cast<long>(GetCurrentThreadId()))
         {    /* not current thread, do lock */
         mtx->_get_cs()->lock();
         mtx->thread_id = static_cast<long>(GetCurrentThreadId());
         }
     ++mtx->count;

     return (_Thrd_success);
     }
 else
     {    /* handle timed or recursive mutex */
     int res = WAIT_TIMEOUT;
     if (target == 0)
         {    /* no target --> plain wait (i.e. infinite timeout) */
         if (mtx->thread_id != static_cast<long>(GetCurrentThreadId()))
             mtx->_get_cs()->lock();
         res = WAIT_OBJECT_0;

         }
     else if (target->sec < 0 || target->sec == 0 && target->nsec <= 0)
         {    /* target time <= 0 --> plain trylock or timed wait for */
             /* time that has passed; try to lock with 0 timeout */
             if (mtx->thread_id != static_cast<long>(GetCurrentThreadId()))
                 {    /* not this thread, lock it */
                 if (mtx->_get_cs()->try_lock())
                     res = WAIT_OBJECT_0;
                 else
                     res = WAIT_TIMEOUT;
                 }
             else
                 res = WAIT_OBJECT_0;

         }
     else
         {    /* check timeout */
         xtime now;
         xtime_get(&now, TIME_UTC);
         while (now.sec < target->sec
             || now.sec == target->sec && now.nsec < target->nsec)
             {    /* time has not expired */
             if (mtx->thread_id == static_cast<long>(GetCurrentThreadId())
                 || mtx->_get_cs()->try_lock_for(
                     _Xtime_diff_to_millis2(target, &now)))
                 {    /* stop waiting */
                 res = WAIT_OBJECT_0;
                 break;
                 }
             else
                 res = WAIT_TIMEOUT;

             xtime_get(&now, TIME_UTC);
             }
         }
     if (res != WAIT_OBJECT_0 && res != WAIT_ABANDONED)
         ;

     else if (1 < ++mtx->count)
         {    /* check count */
         if ((mtx->type & _Mtx_recursive) != _Mtx_recursive)
             {    /* not recursive, fixup count */
             --mtx->count;
             res = WAIT_TIMEOUT;
             }
         }
     else
         mtx->thread_id = static_cast<long>(GetCurrentThreadId());

     switch (res)
         {
     case WAIT_OBJECT_0:
     case WAIT_ABANDONED:
         return (_Thrd_success);

     case WAIT_TIMEOUT:
         if (target == 0 || (target->sec == 0 && target->nsec == 0))
             return (_Thrd_busy);
         else
             return (_Thrd_timedout);

     default:
         return (_Thrd_error);
         }
     }
 }

So, according to this code, and the atomic_ keywords I think mutex can be written the next way:

atomic_bool state = false;

void lock()
{
if(!state)
    state = true;
else
    while(state){}
}

void unlock()
{
state = false;
}

bool try_lock()
{
if(!state)
   state = true;
else
   return false;

return true;
}

4

Answers


  1. A mutex is specifically made to synchronize code on different threads that work on the same resource. It is designed not have issues when used on multi-threaded. Be aware that when using different mutexes together, you can still deadlocks when taking them in different order on different threads. C++’s std::unique_lock solves this.

    A variable is meant to use on either single threads or on synchronized threads because that’s how they can be accessed in the fastest way. This has to do with computer architecture (registers, cache, operations in several steps).

    To work with variables on non-synchronized threads, you can work with std::atomic variables. That can be faster than synchronizing, but the access is slower and more cumbersome than for normal variables. Some more complex situations with several variables can only be handled in a synchronous way.

    Login or Signup to reply.
  2. Why mutex can be used in different threads?

    How could it possibly be useful if it couldn’t be used in different threads? Synchronizing multiple threads with a shared mutex is the only reason for it to exist at all.

    Using (writing) same variable in multiple threads simultaneously

    It’s a bad idea to only worry about things happening "simultaneously". The problem is generally things happening with undetermined ordering, ie, unpredictably.

    There are lots of multi-threading bugs that seem impossible if you believe things have to be simultaneous to go wrong.

    causes undefined behavior and crashes.

    Undefined Behaviour is not required to cause a crash. If it had to crash, crashing would be behaviour which was … defined. There are endless questions on here from people who don’t understand this, asking why their "undefined behaviour test" didn’t crash.

    Why using mutex, despite on fact that they are also variables, not causes undefined behavior?

    Because of the way they’re used. Mutexes are not simply assigned to or read from, like simple variables, but are manipulated with specialized code designed specifically to do this correctly.

    You can almost write your own mutex – a spinlock, anyway – just by using std::atomic<int> and a lot of care.

    The difference is that a mutex also interacts with your operating system scheduler, and that interface is not portably exposed as part of the language standard. The std::mutex class bundles the OS-specific part up in a class with correct semantics, so you can write portable C++ instead of being limited to, say, POSIX-compatible C++ or Windows-compatible C++.


    In your exploration of the VS std::mutex implementation, you ignored the mtx->_get_cs()->lock() part: this is using the Windows Critical Section to interact with the scheduler.

    Your implementation is an attempt at a spinlock, which is fine so long as you know the lock is never held for long (or if you don’t mind sacrificing a core for each blocked thread). By contrast, the mutex allows a waiting thread to be de-scheduled until the lock is released. This is the part handled by the Critical Section.

    You also ignored all the timeout and recursive locking code – which is fine, but your implementation isn’t really attempting to do the same thing as the original.

    Login or Signup to reply.
  3. Using (writing) same variable in multiple threads simultaneously causes undefined behavior and crashes. Why using mutex, despite on fact that they are also variables, not causes undefined behavior?

    It is only undefined behaviour for regular variables, and only if there is no synchronisation. std::mutex is defined to be thread safe. It’s entire point is to provide synchronisation to other objects.

    From [intro.races]:

    The library defines a number of atomic operations ([atomics]) and operations on mutexes ([thread]) that are specially identified as synchronization operations. These operations play a special role in making assignments in one thread visible to another.

    Note: For example, a call that acquires a mutex will perform an acquire operation on the locations comprising the mutex. Correspondingly, a call that releases the same mutex will perform a release operation on those same locations.

    Certain library calls synchronize with other library calls performed by another thread.

    The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

    (Emphasis added)

    Login or Signup to reply.
  4. As you have found, std::mutex is thread-safe because it uses atomic operations. It can be reproduced with std::atomic_bool. Using atomic variables from multiple thread is not undefined behavior, because that is the purpose of those variables.

    From C++ standard (emphasis mine):

    The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior.

    Atomic variables are implemented using atomic operations of the CPU. This is not implemented for non-atomic variables, because those operations take longer time to execute and would be useless if the variables are only used in one thread.

    Your example is not thread-safe:

    void lock()
    {
    if(!state)
        state = true;
    else
        while(state){}
    }
    

    If two threads are checking if(!state) simultaneously, it is possible that both enter the if section, and both threads believe they have the ownership:

    Thread 1        Thread 2
    if (!state)     
                    if (!state)
                    state=true;
    state=true;     
    

    You must use an atomic exchange function to ensure that the another thread cannot come in between checking the value and changing it.

    void lock()
    {
        bool expected;
        do {
            expected = false;
        } while (!state.compare_exchange_weak(expected, true));
    }
    

    You can also add a counter and give time for other threads to execute if the wait takes a long time:

    void lock()
    {
        bool expected;
        size_t counter = 0;
        do {
            expected = false;
            if (counter > 100) {
                Sleep(10);
            }
            else if (counter > 20) {
                Sleep(5);
            }
            else if (counter > 3) {
                Sleep(1);
            }
            counter++;
        } while (!state.compare_exchange_weak(expected, true));
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search