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

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

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

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

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

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

#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.

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

1
Suraj Pal 13 Сен 2018 в 19:02

2 ответа

Лучший ответ

это дает бредовый вывод и segfault.

Это все еще потенциально может дать вам segfault при текущей схеме синхронизации, даже если вы выберете предложенный Блокировка стиля 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;
}

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

2
Geezer 16 Сен 2018 в 19:01

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

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

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";
    }
}
2
Maxim Egorushkin 13 Сен 2018 в 16:57