Почему кажется, что мьютекс, полученный с помощью std :: lock_guard, все еще действует через некоторое время после своего действия - PullRequest
0 голосов
/ 08 июня 2019

Как мы знаем, правильное использование std :: lock_guard выглядит следующим образом: RAII style:

void increase_decrease() {
    std::lock_guard<std::mutex> guard(global_mutex);

    static const int times = 50;
    for (int i = 0; i < times; i++) {
        global_data ++;
    }
    for (int i = 0; i < times; i++) {
        global_data --;
    }
}

Здесь моя точка зрения не о том, какиспользовать std::lock_guard или мьютекс.

В приведенном ниже коде мы сознательно используем std::lock_guard неправильно 1015 *.(То есть поместить его в блок перед критической секцией.)

16 создаются потоки, чтобы добавить 1 к 1 и вычесть 1 из глобальной переменной int, которая инициализируется как 0, в 50 раз.

std::lock_guard вызывается в блоке, а блок находится перед критической секцией ( НЕПРАВИЛЬНО! Никогда не делайте что-то подобное!) .Mutex будет выпущен после блока ( неправильное использование, снова ), следуя механизму RAII .Итак, когда он входит в критическую секцию, там нет блокировки.

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

int global_data = 0;
std::mutex global_mutex;
void increase_decrease() {
    // XXX: INCORRECT USAGE! INCORRECT USAGE! INCORRECT USAGE!
    {
        std::lock_guard<std::mutex> guard(global_mutex);
    }
    // // XXX: uncomment to sleep for a litter while
    // std::this_thread::sleep_for(std::chrono::milliseconds(10));
    static const int times = 50;
    for (int i = 0; i < times; i++) {
        global_data ++;
    }
    for (int i = 0; i < times; i++) {
        global_data --;
    }
}

void try_mutex() {
    const int num_workers = 16;
    std::vector<std::thread> workers;

    auto start = std::chrono::system_clock::now();
    for (int i = 0; i < num_workers; i++) {
        std::thread t(increase_decrease);
        workers.push_back(std::move(t));
    }
    for (auto &t: workers) {
        t.join();
    }
    auto end = std::chrono::system_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "global_data: " << global_data
        << ", elapsed second: " << elapsed_seconds.count();
}

int main() {
    try_mutex();
}

Я обнаружил, что если сон в течение 10 мс вызывает разные результаты.

Без сна стандартный вывод 20 вызовов main:

global_data: 0, elapsed second: 0.000363
global_data: 0, elapsed second: 0.000359
global_data: 0, elapsed second: 0.000349
global_data: 0, elapsed second: 0.000345
global_data: 0, elapsed second: 0.000352
global_data: 0, elapsed second: 0.000323
global_data: 0, elapsed second: 0.000619
global_data: 0, elapsed second: 0.000431
global_data: 34, elapsed second: 0.000405
global_data: -14, elapsed second: 0.000415
global_data: 0, elapsed second: 0.000497
global_data: 0, elapsed second: 0.000366
global_data: 0, elapsed second: 0.000413
global_data: 0, elapsed second: 0.000406
global_data: 0, elapsed second: 0.000353
global_data: 0, elapsed second: 0.000363
global_data: 0, elapsed second: 0.000361
global_data: 0, elapsed second: 0.000358
global_data: 0, elapsed second: 0.000348
global_data: 0, elapsed second: 0.000367

Однако , если мы раскомментируем сон , стандартный вывод 20 вызовов main:

global_data: 44, elapsed second: 0.011108
global_data: 15, elapsed second: 0.010645
global_data: 25, elapsed second: 0.012905
global_data: 27, elapsed second: 0.012914
global_data: 9, elapsed second: 0.012871
global_data: 46, elapsed second: 0.012836
global_data: 44, elapsed second: 0.011307
global_data: -2, elapsed second: 0.01286
global_data: 77, elapsed second: 0.012853
global_data: 43, elapsed second: 0.011984
global_data: 0, elapsed second: 0.011134
global_data: -3, elapsed second: 0.011571
global_data: 49, elapsed second: 0.012438
global_data: 43, elapsed second: 0.011552
global_data: -20, elapsed second: 0.010807
global_data: 0, elapsed second: 0.010514
global_data: 0, elapsed second: 0.010916
global_data: -44, elapsed second: 0.012829
global_data: 50, elapsed second: 0.011759
global_data: 9, elapsed second: 0.012873

Вероятность того, что global_data равно 0, намного больше в первом случае, чем во втором.Я пробовал много раз.Это не просто совпадение.

Итак, похоже, что мьютекс может немного подействовать на после блока, в котором он получил через std::lock_guard.Зачем?

Спасибо.

Ответы [ 2 ]

4 голосов
/ 08 июня 2019

std::lock_guard вызывается в блоке перед критической секцией.Мьютекс должен быть освобожден после блока, следуя механизму в стиле RAII.

Нет, мьютекс освобождается в конце блока, в который вы его вставили. И это до доступа к global_data ..Следовательно, он не защищен, и это неопределенное поведение.Все результаты, которые вы видите, возможны при неопределенном поведении, и вы не должны тратить слишком много усилий, пытаясь понять эти результаты.

Если вы удалите скобки вокруг lock_guard, все будет работать нормально.

3 голосов
/ 08 июня 2019

У вас гонка данных по переменной global_data (потому что вы снимаете блокировку до того, как начнете манипулировать ею). Из-за этого результаты выполнения вашего кода непредсказуемы.

Если вы измените global_data с int на std::atomic <int>, то вы получите одинаковые выходные данные как с sleep (все нули), так и без него.

Живая демоверсия

...