Использование Mutex вешает программу - PullRequest
0 голосов
/ 13 сентября 2018

Я пытаюсь выучить параллельное программирование на C ++.

Я реализовал базовый класс стека с помощью методов push (), pop (), top () и empty ().

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

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

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

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

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

using std::cin;
using std::cout;
std::mutex mtx;
std::mutex a_mtx;


class MyStack
{
    std::vector<int> stk;
public:
    void push(int val) {
        stk.push_back(val);
    }

    void pop() {
        mtx.lock();
        stk.pop_back();
        mtx.unlock();
    }

    int top() const {
        mtx.lock();
        return stk[stk.size() - 1];
    }

    bool empty() const {
        mtx.lock();
        return stk.size() == 0;
    }
};

void func(MyStack& ms, const std::string s)
{
    while(!ms.empty()) {
        mtx.unlock();
        a_mtx.lock();
        cout << s << " " << ms.top() << "\n";
        a_mtx.unlock();
        mtx.unlock();
        ms.pop();
    }

    //mtx.unlock();
}

int main(int argc, char const *argv[])
{
    MyStack ms;

    ms.push(3);
    ms.push(1);
    ms.push(4);
    ms.push(7);
    ms.push(6);
    ms.push(2);
    ms.push(8);

    std::string s1("from thread 1"), s2("from thread 2");
    std::thread t1(func, std::ref(ms), "from thread 1");
    std::thread t2(func, std::ref(ms), "from thread 2");

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

    cout << "Done\n";

    return 0;
}

Я подумал, потому что, когда стек пуст, я не разблокировал мьютекс.Поэтому, когда я раскомментирую закомментированную строку и запускаю ее, она выдает бессмысленный вывод и segfault.

Я не знаю, где я делаю ошибку.Это правильный способ написания поточно-ориентированного стекового класса?

Ответы [ 2 ]

0 голосов
/ 13 сентября 2018

это дает бессмысленный вывод и сегфоут.

Это будет все еще потенциально даст вам сегфо при текущемсхема синхронизации, даже если вы используете предложенную блокировку в стиле RAII , например:

void pop() {
    std::lock_guard<std::mutex> lock{ mtx };
    stk.pop_back();
}

int top() const {
    std::lock_guard<std::mutex> lock{ mtx };
    return stk[stk.size() - 1];
}

bool empty() const {
    std::lock_guard<std::mutex> lock{ mtx };
    return stk.size() == 0;
}

при , вы не заботитесь о условие гонки возникающих между двумя последующими вызовами этих методов разными потоками.Например, подумайте о том, что происходит, когда в стеке остался один элемент, и один поток спрашивает, пуст ли он, и получает ли false, а затем у вас есть переключение контекста, а другой поток получает тот же false для того же вопроса.Так что они оба гоняются за этим top() и pop().В то время как первый уже выскакивает, а затем другой пытается top(), он будет делать это в ситуации, когда stk.size() - 1 дает -1.Следовательно, вы получаете segfault за попытку доступа к несуществующему отрицательному индексу стека: (

Я не знаю, где я делаю ошибку. Этоправильный способ написания поточно-безопасного стекового класса?

Нет, это не правильный путь, мьютекс только гарантирует, что другие потоки, блокирующие тот же мьютекс, не могут в данный момент выполнять этот же разделкод. Если они попадают в один и тот же раздел, им запрещается входить в него, пока мьютекс не будет освобожден. Но вы вообще не блокируетесь между вызовом до empty() и остальными вызовами. Один поток получает empty(), блокирует, получает значение, затем освобождает его, а затем другой поток может свободно вводить и запрашивать и вполне может получить то же значение. Что мешает позже ввести ваш вызов top()и что мешает первому потоку быть уже после того же pop() в это время?

В этих сценариях вам нужно быть осторожным, чтобы увидеть весь объем того, что нуждается в защитес точки зрения синхронности.То, что здесь сломано, называется атомарность , что означает свойство «быть не в состоянии разрезать посередине».Как вы можете видеть , здесь говорится , что " атомарность часто обеспечивается взаимным исключением, " - как при использовании мьютексов, как вы это сделали.Чего не хватало, так это того, что было слишком мелко слишком мелко - операция "размер атома" была слишком мала.Вы должны были защищать всю последовательность empty() - top() - pop() в целом, так как теперь мы понимаем, что не можем отделить ни одну часть от трех.В коде это может выглядеть как вызов этого внутри func() и печать в cout, только если он вернет true:

bool safe_pop(int& value)
{
    std::lock_guard<std::mutex> lock{ mtx };

    if (stk.size() > 0)
    {
        value = stk[stk.size() - 1];
        stk.pop_back();
        return true;
    }

    return false;
}

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

0 голосов
/ 13 сентября 2018

Одна ошибка в том, что MyStack::top и MyStack::empty не разблокируют мьютекс.

Используйте std::lock_guard<std::mutex> для автоматической разблокировки мьютекса и устранения риска таких случайных блокировок. E.g.:

bool empty() const {
    std::lock_guard<std::mutex> lock(mtx);
    return stk.empty();
}

И, вероятно, нужно также заблокировать мьютекс в MyStack::push.


Другая ошибка заключается в том, что блокировка на уровне метода слишком мелкозернистая, а empty(), за которыми следуют top() и pop(), не являются атомарными.

Возможные исправления:

class MyStack
{
    std::vector<int> stk;
public:
    void push(int val) {
        std::lock_guard<std::mutex> lock(mtx);
        stk.push_back(val);
    }

    bool try_pop(int* result) {
        bool popped;
        {
            std::lock_guard<std::mutex> lock(mtx);
            if((popped = !stk.empty())) {
                *result = stk.back();
                stk.pop_back();
            }
        }
        return popped;
    }
};

void func(MyStack& ms, const std::string& s)
{
    for(int top; ms.try_pop(&top);) {
        std::lock_guard<std::mutex> l(a_mtx);
        cout << s << " " << top << "\n";
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...