Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the reason for double NULL check of pointer for mutex lock

I recently read a book about system software. There is an example in it that I don't understand.

volatile T* pInst = 0;
T* GetInstance()
{
  if (pInst == NULL)
  {
   lock();
   if (pInst == NULL)
     pInst = new T;
   unlock();
  }
  return pInst;
}

Why does the author check (pInst == NULL) twice?

like image 267
BigDongle Avatar asked Jun 04 '19 09:06

BigDongle


People also ask

Can a mutex be locked twice?

To solve your issue, you can use std::recursive_mutex , which can be locked/unlocked multiple times from the same thread. From cppreference: A calling thread owns a recursive_mutex for a period of time that starts when it successfully calls either lock or try_lock .

What happens if you try to lock a locked mutex?

The mutex is either in a locked or unlocked state for a thread. If a thread attempts to relock a mutex that it has already locked, it will return with an error. If a thread attempts to unlock a mutex that is unlocked, it will return with an error.

What happens mutex lock?

Mutexes are used to protect shared resources. If the mutex is already locked by another thread, the thread waits for the mutex to become available. The thread that has locked a mutex becomes its current owner and remains the owner until the same thread has unlocked it.

Why is mutex lock needed?

Why Do You Still Need a Lock if the Variable is Atomic? Even if you have an atomic variable, you still need the mutex. Without it, there is still the potential to miss the notification under a case of a wake-up interleaved with an update to the atomic variable.


2 Answers

When two threads try call GetInstance() for the first time at the same time, both will see pInst == NULL at the first check. One thread will get the lock first, which allows it to modify pInst.

The second thread will wait for the lock to get available. When the first thread releases the lock, the second will get it, and now the value of pInst has already been modified by the first thread, so the second one doesn't need to create a new instance.

Only the second check between lock() and unlock() is safe. It would work without the first check, but it would be slower because every call to GetInstance() would call lock() and unlock(). The first check avoids unnecessary lock() calls.

volatile T* pInst = 0;
T* GetInstance()
{
  if (pInst == NULL) // unsafe check to avoid unnecessary and maybe slow lock()
  {
   lock(); // after this, only one thread can access pInst
   if (pInst == NULL) // check again because other thread may have modified it between first check and returning from lock()
     pInst = new T;
   unlock();
  }
  return pInst;
}

See also https://en.wikipedia.org/wiki/Double-checked_locking (copied from interjay's comment).

Note: This implementation requires that both read and write accesses to volatile T* pInst are atomic. Otherwise the second thread may read a partially written value just being written by the first thread. For modern processors, accessing a pointer value (not the data being pointed to) is an atomic operation, although not guaranteed for all architectures.

If access to pInst was not atomic, the second thread may read a partially written non-NULL value when checking pInst before getting the lock and then may execute return pInst before the first thread has finished its operation, which would result in returning a wrong pointer value.

like image 77
Bodo Avatar answered Nov 08 '22 09:11

Bodo


I assume lock() is costly operation. I also assume that read on T* pointers is done atomically on this platform, so you don't need to lock simple comparisons pInst == NULL, as the load operation of pInst value will be ex. a single assembly instruction on this platform.

Assuming that: If lock() is a costly operation, it's best not to execute it, if we don't have to. So first we check if pInst == NULL. This will be a single assembly instruction, so we don't need to lock() it. If pInst == NULL, we need to modify it's value, allocate new pInst = new ....

But - imagine a situation, where 2 (or more) threads are right in the point between first pInst == NULL and right before lock(). Both threads will to pInst = new. They already checked the first pInst == NULL and for both of them it was true.

The first (any) thread starts it's execution and does lock(); pInst = new T; unlock(). Then the second thread waiting on lock() starts it's execution. When it starts, pInst != NULL, because another thread allocated that. So we need to check it pInst == NULL inside lock() again, so that memory is not leaked and pInst overwritten..

like image 2
KamilCuk Avatar answered Nov 08 '22 07:11

KamilCuk