Когда два потока пытаются вызвать 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
до того, как первый поток завершит свою работу, что приведет к возвращению неверного значения указателя.