В чем причина двойной NULL-проверки указателя для блокировки мьютекса? - PullRequest
15 голосов
/ 04 июня 2019

Я недавно прочитал книгу о системном программном обеспечении.В нем есть пример, который я не понимаю.

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

Почему автор проверяет (pInst == NULL) дважды?

Ответы [ 2 ]

22 голосов
/ 04 июня 2019

Когда два потока пытаются вызвать GetInstance() в первый раз одновременно, оба увидят pInst == NULL при первой проверке. Один поток сначала получит блокировку, что позволит ему изменить pInst.

Второй поток будет ждать, пока блокировка не станет доступной. Когда первый поток снимает блокировку, второй получит его, и теперь значение pInst уже было изменено первым потоком, поэтому второму не нужно создавать новый экземпляр.

Безопасна только вторая проверка между lock() и unlock(). Это будет работать без первой проверки, но будет медленнее, потому что каждый вызов GetInstance() будет вызывать lock() и unlock(). Первая проверка позволяет избежать ненужных вызовов lock().

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;
}

См. Также https://en.wikipedia.org/wiki/Double-checked_locking (скопировано из комментария interjay ).

Примечание: Эта реализация требует, чтобы и чтение, и доступ к записи volatile T* pInst были атомарными. В противном случае второй поток может прочитать частично записанное значение, просто записываемое первым потоком. Для современных процессоров доступ к значению указателя (а не к указанным данным) является атомарной операцией, хотя не гарантируется для всех архитектур.

Если доступ к pInst не был атомарным, второй поток может прочитать частично записанное не NULL значение при проверке pInst перед получением блокировки, а затем может выполнить return pInst до того, как первый поток завершит свою работу, что приведет к возвращению неверного значения указателя.

2 голосов
/ 04 июня 2019

Я полагаю, lock() является дорогостоящей операцией.Я также предполагаю, что чтение на T* указателях выполняется на этой платформе атомарно, поэтому вам не нужно блокировать простые сравнения pInst == NULL, так как операция загрузки значения pInst будет ex.отдельная инструкция по сборке на этой платформе.

Предполагая, что: если lock() является дорогостоящей операцией, лучше ее не выполнять, если нам не нужно.Итак, сначала мы проверим, если pInst == NULL.Это будет отдельная инструкция по сборке, поэтому нам не нужно lock().Если pInst == NULL, нам нужно изменить его значение, выделить новое pInst = new ....

Но - представьте себе ситуацию, когда 2 (или более) потока находятся прямо в точке между первым pInst == NULL и непосредственно передlock().Обе темы будут pInst = new.Они уже проверили первый pInst == NULL, и для них обоих это было правдой.

Первый (любой) поток начинает выполнение и выполняет lock(); pInst = new T; unlock().Затем второй поток, ожидающий lock(), начинает выполнение.Когда это начинается, pInst != NULL, потому что другой поток выделил это.Поэтому нам нужно проверить его pInst == NULL внутри lock() еще раз, чтобы память не просочилась и pInst не перезаписалась ..

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...