Объясните состояние гонки в двойной проверке блокировки - PullRequest
3 голосов
/ 20 декабря 2011
void undefined_behaviour_with_double_checked_locking()
{
    if(!resource_ptr)                                    #1
    {
        std::lock_guard<std::mutex> lk(resource_mutex);  #2
        if(!resource_ptr)                                #3
        {
           resource_ptr.reset(new some_resource);        #4
        }
    }
    resource_ptr->do_something();                        #5
}

, если поток видит указатель, написанный другим потоком, он может не увидеть вновь созданный экземпляр some_resource, в результате чего вызов do_something () работает с неверными значениями.Это пример типа состояния гонки, определенного как стандарт данных C ++ как гонки данных и, следовательно, заданного как неопределенное поведение.

Вопрос > Я видел приведенное выше объяснениепочему код имеет проблему двойной проверки блокировки, которая вызывает состояние гонки.Однако мне все еще трудно понять, в чем проблема.Возможно, конкретный двухпоточный пошаговый рабочий процесс поможет мне действительно понять проблему гонки для приведенного выше кода.

Одно из решений, упомянутых в книге, заключается в следующем:

std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;

void init_resource()
{
    resource_ptr.reset(new some_resource);
}
void foo()
{
    std::call_once(resource_flag,init_resource); #1
    resource_ptr->do_something();
}
#1 This initialization is called exactly once

Любой комментарий приветствуется -Спасибо

Ответы [ 2 ]

8 голосов
/ 20 декабря 2011

В этом случае (в зависимости от реализации .reset и !) может возникнуть проблема, когда поток 1 проходит неполный путь при инициализации resource_ptr, а затем останавливается / переключается. Затем появляется поток 2, выполняет первую проверку, видит, что указатель не равен нулю, и пропускает проверку блокировки / полностью инициализированной проверки. Затем он использует частично инициализированный объект (вероятно, в результате чего происходят плохие вещи). Затем поток 1 возвращается и завершает инициализацию, но уже слишком поздно.

Причина, по которой возможна частично инициализированная resource_ptr, заключается в том, что ЦПУ разрешено переупорядочивать инструкции (при условии, что он не изменяет однопоточное поведение). Таким образом, хотя код выглядит так, как будто он должен полностью инициализировать объект и затем присвоить его resource_ptr, оптимизированный код сборки может делать что-то совсем другое, и ЦПУ также не гарантированно будет выполнять инструкции по сборке в порядке, в котором они указаны в двоичном виде!

Вывод состоит в том, что когда задействованы несколько потоков, ограждения памяти (блокировки) являются единственным способом гарантировать, что все происходит в правильном порядке.

4 голосов
/ 20 декабря 2011

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

Например, если вы думаете, что операция new some_resource состоит из двух шагов:

  • выделить память для some_resource
  • инициализировать some_resource (для этого обсуждения я собираюсь сделать упрощающее предположение, что эта инициализация не может вызвать исключение)

Тогда вы можете видеть, что компилятор может реализовать защищенную мьютексом часть кода следующим образом:

1. allocate memory for `some_resource`
2. store the pointer to the allocated memory in `resource_ptr`
3. initialize `some_resource`

Теперь становится ясно, что если другой поток выполняет функцию между шагами 2 и3, тогда можно вызвать resource_ptr->do_something(), в то время как some_resource не был инициализирован.

Обратите внимание, что на некоторых архитектурах процессоров также возможно, чтобы этот тип переупорядочения происходил в оборудовании, если не установлены надлежащие барьеры памяти(и такие барьеры будут реализованы мьютексом).

...