Гарантируют ли блокировки в C ++ 11 свежесть получаемых данных? - PullRequest
0 голосов
/ 11 октября 2018

Обычно, когда используются типы std :: atomic, к которым одновременно обращаются несколько потоков, нет никакой гарантии, что поток получит значение «актуальное» при обращении к ним, и поток может получить устаревшее значение из кэша или любого более старого значения,Единственный способ получить актуальное значение - это такие функции, как compare_exchange_XXX.(См. Вопросы здесь и здесь )

#include <atomic>

std::atomic<int> cancel_work = 0;
std::mutex mutex;

//Thread 1 executes this function
void thread1_func() 
{
    cancel_work.store(1, <some memory order>);
}


// Thread 2 executes this function
void thread2_func() 
{
    //No guarantee tmp will be 1, even when thread1_func is executed first
   int tmp = cancel_work.load(<some memory order>); 
}

Однако мой вопрос заключается в том, что происходит при использовании мьютекса и блокировки вместо этого?Есть ли у нас какие-либо гарантии свежести общих данных?

Например, если предположить, что поток 1 и поток 2 выполняются одновременно, а поток 1 получает блокировку первым (выполняется первым).Гарантирует ли это, что поток 2 увидит измененное значение, а не старое значение?Имеет ли значение, является ли в этом случае общие данные «cancel_work» атомарными или нет?

#include <atomic>

int cancel_work = 0;  //any difference if replaced with std::atomic<int> in this case?
std::mutex mutex;

// Thread 1 executes this function
void thread1_func() 
{
    //Assuming Thread 1 enters lock FIRST
    std::lock_guard<std::mutex> lock(mutex);

    cancel_work = 1;
}


// Thread 2 executes this function
void thread2_func() 
{
    std::lock_guard<std::mutex> lock(mutex);

    int tmp = cancel_work; //Will tmp be 1 or 0?
}

int main()
{
   std::thread t1(thread1_func);
   std::thread t2(thread2_func);

   t1.join(); t2.join();

   return 0;
}

Ответы [ 2 ]

0 голосов
/ 11 октября 2018

Стандарт C ++ только ограничивает наблюдаемое поведение абстрактной машины в правильно сформированных программах без неопределенного поведения в любом месте во время выполнения абстрактной машины.

Он не дает никаких гарантий относительно отображения между физическими аппаратными действиями, которые выполняет программа, иповедение.

В ваших случаях на абстрактной машине нет порядка между выполнением thread1 и thread2.Даже если физическое оборудование, на которое нужно запланировать и запустить thread1 до thread2, это накладывает нулевые ограничения (в вашем простом примере) на выходные данные, которые генерирует программа.Вывод программ ограничен только тем, какие легальные выходные данные может генерировать абстрактная машина.

Компилятор C ++ может легально:

  1. Полное удаление вашей программы как эквивалент возврата 0;

  2. Докажите, что чтение cancel_work в thread2 не секвенировано относительно всех модификаций cancel_work вдали от 0, и измените его на постоянное чтение 0.

  3. На самом деле сначала запустите thread1, затем запустите thread2, но докажите, что он может обрабатывать операции в thread2 так, как если бы они произошли до запуска thread1, поэтомуне пытайтесь принудительно обновить строку кэша в thread2 и считывать устаревшие данные из cancel_work.

То, что на самом деле происходит на оборудовании, не влияет на то, чтопрограмма может легально делать.И то, что программа может делать по закону, - в ситуациях с потоками ограничивается наблюдаемым поведением абстрактной машины, а также поведением примитивов синхронизации и их использованием в разных потоках.

Для того, чтобы фактические события произошли до возникновения отношений,вам нужно что-то вроде:

std::thread(thread1_func).join();
std::thread(thread2_func).join();

и теперь мы знаем, что все в thread1_func происходит до thread2_func.

Мы все еще можем переписать вашу программу как return 0; и аналогичные изменения,Но теперь у нас есть гарантия того, что thread1_func произойдет раньше, чем код thread2_func.

Обратите внимание, что мы можем устранить (1) выше с помощью:

std::lock_guard<std::mutex> lock(mutex);

int tmp = cancel_work; //Will tmp be 1 or 0?
std::cout << tmp;

и заставить tmpна самом деле будет напечатано.

Программа может быть преобразована в ту, которая печатает 1 или 0 и не имеет никакой многопоточности.Это могло бы сохранить поток, но измените thread2_func, чтобы вывести константу 0.И т.д.


Итак, мы переписываем вашу программу так:

std::condition_variable cv;
bool writ = false;
int cancel_work = 0;  //any difference if replaced with std::atomic<int> in this case?
std::mutex mutex;

// Thread 1 executes this function
void thread1_func() 
{
    {
      std::lock_guard<std::mutex> lock(mutex);

      cancel_work = 1;
    }
    {
      std::lock_guard<std::mutex> lock(mutex);
      writ = true;
      cv.notify_all();
    }
}


// Thread 2 executes this function
void thread2_func() 
{
    std::unique_lock<std::mutex> lock(mutex);

    cv.wait(lock, []{ return writ; } );

    int tmp = cancel_work;
    std::cout << tmp; // will print 1
}

int main()
{
   std::thread t1(thread1_func);
   std::thread t2(thread2_func);

   t1.join(); t2.join();

   return 0;
}

и теперь thread2_func происходит после thread1_func и все хорошо.Чтение гарантировано будет 1.

0 голосов
/ 11 октября 2018

Да, использование мьютекса / блокировки гарантирует, что thread2_func () получит измененное значение.

Однако согласно спецификации std::atomic:

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

Таким образом, ваш код будет работать правильно, используя логику получения / освобождения.

#include <atomic>

std::atomic<int> cancel_work = 0;

void thread1_func() 
{
    cancel_work.store(1, std::memory_order_release);
}

void thread2_func() 
{
    // tmp will be 1, when thread1_func is executed first
    int tmp = cancel_work.load(std::memory_order_acquire); 
}
...