Деструктор и безопасность потока - PullRequest
0 голосов
/ 07 сентября 2018

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

Идея заключается в следующем:

MyClass{
   ...
   public:
      ...
      void send(string s){
          lock_guard<mutex> lock(m); 
          my_list.push_back(s);
      }
      ~MyClass(){
          lock_guard<mutex> lock(m); 
          for(string s:my_list)
              process(s);
      }
}

Синхронизация правильная?

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

Что касается деструктора, существует ли вероятность того, что поток вызовет send между снятием блокировки и фактическим уничтожением экземпляра? то есть. for (и последующее lock_guard уничтожение) - последняя инструкция, которая будет выполнена перед фактическим уничтожением, или возможны условия гонки после выполнения деструктора?

Ответы [ 2 ]

0 голосов
/ 26 февраля 2019

У вас здесь хорошая интуиция; lock_guard в деструкторе не приносит никакой пользы.

И вот почему: Как это написано, любые вызовы send() должны быть выполнены до создания lock_guard ~MyClass() - в противном случае сообщение не будет обработано, и send() вполне может использовать m и my_list после завершения их уничтожения , что приводит к неопределенному поведению. Вызывающие абоненты send() не могут быть уверены, что это произойдет, кроме как просто убедиться, что все вызовы send() сделаны до того, как ~MyClass() даже начнется.

Это нормально. Большинство классов имеют (или должны иметь) требование, чтобы клиенты сериализовали уничтожение. То есть клиенты должны убедиться, что все вызывающие абоненты на send() выполнены до вызова ~MyClass(). Фактически, все стандартные классы библиотеки имеют это требование, если не указано иное. Некоторые классы сознательно не требуют этого; это нормально, но несколько экзотично.

К счастью, это не так уж сложно для клиентов; они могут просто использовать shared_ptr или что-то еще, как предлагает Jarod42.

ТЛ; др:

существует ли вероятность, что поток вызовет send между снятием блокировки и фактическим уничтожением экземпляра?

Да! Документируйте, что это ошибка клиента, если они делают это и избавляются от блокировки в деструкторе.

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

Вы можете разделить свой класс:

class MyClass
{
public:
    void send(const std::string& s){
        lock_guard<mutex> lock(m); 
        my_list.push_back(s);
    }

    void process_all_messages()
    {
        lock_guard<mutex> lock(m);
        for (const string& s : my_list)
            process(s);
        //my_list.clear();
    }

    void process(const std::string& s);

// ... mutex, list, ...
};

И на нем есть обертка

class MyClassPerTHread
{
public:
    explicit MyClassPerTHread(std::shared_ptr<MyClass> ptr) : ptr(ptr) {}
    ~MyClassPerTHread(){ ptr->process_all_messages(); }

    // MyClassPerTHread(const MyClassPerTHread&);
    // MyClassPerTHread(MyClassPerTHread&&);
    // MyClassPerTHread& operator=(const MyClassPerTHread&);
    // MyClassPerTHread& operator=(MyClassPerTHread&&);

    void send(const std::string& s) { ptr->send(s); };
private:
    std::shared_ptr<MyClass> ptr;
};

Итак, в main вы создаете экземпляр std::shared_ptr<MyClass>. Вы передаете это каждому потоку, который обернет это в MyClassPerTHread.

Когда MyClassPerTHread уничтожено, Вы обрабатываете сообщения, как и ожидалось.

Возможно, вы захотите адаптировать MyClassPerTHread для перемещения / копирования.

...