Почему мьютекс не работает без защиты блокировки?

У меня есть следующий код:

      #include <chrono>
#include <iostream>
#include <mutex>
#include <thread>

int shared_var {0};
std::mutex shared_mutex;

void task_1()
{
    while (true)
    {
        shared_mutex.lock();
        const auto temp = shared_var;
        std::this_thread::sleep_for(std::chrono::seconds(1));
        
        if(temp == shared_var)
        {
            //do something
        }
        else
        {
            const auto timenow = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
            std::cout << ctime(&timenow) << ": Data race at task_1: shared resource corrupt \n";
            std::cout << "Actual value: " << shared_var << "Expected value: " << temp << "\n"; 
        }
        shared_mutex.unlock();
    }
}


void task_2()
{
    while (true)
    {
        std::this_thread::sleep_for(std::chrono::seconds(2));
        ++shared_var;
    
    }
}

int main()
{
    auto task_1_thread = std::thread(task_1);
    auto task_2_thread = std::thread(task_2);
 
    task_1_thread.join();
    task_2_thread.join();
    return 0;
}

shared_varзащищено, но не защищено в task_2Что ожидается: я ожидал, что ветка не будет введена, так как общий ресурс заблокирован. Что на самом деле происходит: запуск этого кода приведет к вводу elseфилиал в task_1.

Ожидаемый результат достигается при замене shared_mutex.lock();с std::lock_guard<std::mutex> lock(shared_mutex);а также shared_mutex.unlock();с std::lock_guard<std::mutex> unlock(shared_mutex);

Вопросы:

  1. В чем проблема моего текущего подхода?
  2. Почему это работает с lock_guard?

Я запускаю код на:https://www.onlinegdb.com/online_c++_compiler

3 ответа

Предположим, у вас есть комната с двумя входами. В одном входе есть дверь, в другом нет. Комната называется shared_var. Есть два парня, которые хотят войти в комнату, их зовут и .

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

taks_2может свободно войти в комнату через прихожую без двери. task_1использует дверь под названием shared_mutex.

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

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

Если вы поэкспериментируете, вы можете заметить, что без замка вы найдете обоих парней в комнате, а после добавления замка вы не найдете обоих парней в комнате. Хотя это чистая удача (на самом деле неудача, потому что это заставляет вас поверить, что замок помог). На самом деле замок почти не изменился. Парень позвонил task_2может войти в комнату, пока другой парень находится внутри.

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

Ой, извините, я запутался, рассказывая историю.

TL;DR: В вашем коде не имеет значения, используете ли вы блокировку или нет. На самом деле мьютекс в вашем коде бесполезен, потому что только один поток разблокирует/блокирует его. Чтобы правильно использовать мьютекс, оба потока должны заблокировать его перед чтением/записью общей памяти.

С UB (как гонка данных) вывод не определен, вы можете увидеть «ожидаемый» вывод или странные вещи, сбой, ...

  1. В чем проблема моего текущего подхода?

В первом примере у вас есть гонка данных при записи (не атомарная) shared_varв одном потоке без синхронизации и читать в другом потоке.

  1. Почему это работает с lock_guard?

В модифицированном образце вы дважды блокируете один и тот же (нерекурсивный) мьютекс, который также является UB.

Изstd::mutex::lock:

Если блокировка вызывается потоком, который уже владеет мьютексом, поведение не определено.

У вас просто есть 2 разных поведения для 2 разных UB (когда что угодно может случиться для обоих случаев).

Блокировка мьютекса не блокирует переменную, она просто блокирует мьютекс, так что другой код не может одновременно заблокировать тот же мьютекс.

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

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

Причина, по которой это работает, когда вы заключаете мьютекс в std::lock_guard, заключается в том, что защита блокировки удерживает блокировку мьютекса до конца области видимости, которая в данном случае является концом функции.

Ваша функция сначала блокирует мьютекс с помощью блокировки lock_guard, чтобы позже в той же области попытаться заблокировать тот же мьютекс с помощью блокировки lock_guard. Поскольку мьютекс уже заблокирован блокировкой lock_guard, выполнение останавливается и вывод не производится, поскольку программа больше не выполняется.

Если вы выведете «ok» в своем коде в точке комментария «//do something», вы увидите, что вы получаете вывод один раз, а затем программа останавливает весь вывод.

Примечание; поскольку такое поведение гарантировано, см. ответ @Jarod42 для получения более подробной информации об этом. Как и в большинстве случаев неожиданного поведения в C++, здесь, вероятно, задействован UB.

Другие вопросы по тегам