Самый простой мьютекс на свете. Работает ли этот пример? Это потокобезопасно?


Я хотел бы спросить о самом простом Мьютексном подходе для многопоточной обработки. Является ли следующий код потокобезопасным (quick-n-dirty)?

class myclass
{
    bool locked;
    vector<double> vals;

    myclass();
    void add(double val);
};

void myclass::add(double val)
{
    if(!locked)
    {
        this->locked = 1;
        this->vals.push_back(val);
        this->locked = 0;
    }
    else
    {
        this->add(val);
    }
}

int main()
{
    myclass cls;
    //start parallelism
    cls.add(static_cast<double>(rand()));
}

Работает ли это? Это потокобезопасно? Я просто пытаюсь понять, как можно написать простейший мьютекс.

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

Спасибо.

Спасибо, что сказали, что это не работает. Не могли бы вы Предложить исправление, которое не зависит от компилятора?
6 5

6 ответов:

Это потокобезопасно?

Конечно, нет. Если поток вытеснен между проверкой и установкой блокировки, то второй поток может получить эту блокировку; если контроль затем возвращается к первому потоку, то оба получат его. (И конечно, на современном процессоре два или более ядер могут выполнять одни и те же инструкции одновременно для еще большей веселости.)

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

std::atomic_flag locked;

if (!locked.test_and_set()) {
    vals.push_back(val);
    locked.clear();
} else {
    // I don't know exactly what to do here; 
    // but recursively calling add() is a very bad idea.
}

Или еще лучше:

std::mutex mutex;

std::lock_guard<std::mutex> lock(mutex);
vals.push_back(val);

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

Нет, это не потокобезопасно. Есть гонка между

if(!locked)

И

this->locked = 1;

Если происходит переключение контекста между этими двумя операторами, ваш механизм блокировки разваливается. Вам нужен атомный test and set инструкция, или просто использовать существующий mutex.

Этот код не обеспечивает атомарную модификацию вектора vals. Рассмотрим следующий сценарий:

//<<< Suppose it's 0
if(!locked)
{   //<<< Thread 0 passes the check
    //<<< Context Switch - and Thread 1 is also there because locked is 0
    this->locked = 1;
    //<<< Now it's possible for one thread to be scheduled when another one is in
    //<<< the middle of modification of the vector
    this->vals.push_back(val);
    this->locked = 0;
}

Работает ли это? Это потокобезопасно?

Нет. Временами он будет терпеть неудачу.

Ваш мьютекс будет работать только в том случае, если другие потоки никогда не делают ничего между выполнением этих двух строк:

if(!locked)
{
    this->locked = 1;

...и вы не обеспечили этого.

Чтобы узнать о как писать мьютексы, смотрите этот пост SO.

Нет, это не потокобезопасно.

Рассмотрим два потока, выполняющихся более или менее одновременно. Кроме того, представьте, что значение .locked равно false.

Первый поток выполняется до и включая эту строку:

if(!locked)
{
Теперь представьте, что система переключает контекст на второй поток. Он также выполняет до той же строки.

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

Они оба будут звонить vals.push_back() более или менее в одно и то же время.

Бум.

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

Если вас интересует nitty gritty (или даже если это не так - это материал , который должен знать каждый разработчик программного обеспечения ), вы должны посмотреть на алгоритм пекарни Лесли Лэмпорта и пойти оттуда.