Как сделать суперкласс, который инкапсулирует поток с RAII - PullRequest
0 голосов
/ 02 мая 2020

Я только начал изучать C ++ и пишу симулятор гравитации n-body в качестве учебного упражнения. Чтобы узнать о потоках, я решил разделить работу между потоком рендеринга (состояние отображения на экране) и потоком симуляции (продвижение симуляции).

RAII кажется важным, поэтому поток рендеринга использует его, чтобы сделать курсор консоли видимым и невидимым, пока он жив, на основе объекта InvisibleCursor, который управляет этим состоянием в своем конструкторе и деструкторе. Это работает и не изображено. Проблема заключалась в том, что я закрывал потоки, просто позволив пузырю Ctrl- C, который вызывает terminate () для потоков, который, по-видимому, не вызывает локальные деструкторы.

Так что потоки должны выйти сами, нормально. Я решил попытаться использовать RAII для них, в результате чего получился объект, у которого был поток, метод и переменная-член флага остановки. При построении объект инициализировал свой поток для запуска своего метода, и метод проверял флаг остановки члена очень часто и останавливался, когда он был равен true. Это работает и не изображено. Проблема заключается в том, что я пытался вытянуть эту логи c (поскольку у меня 2 таких потока) в суперкласс, и каждый тип потока подкласс их. Это только вид работ, и он изображен.

Проблема, которую я считаю, заключается в том, что я вызываю виртуальный метод действия теперь в моем базовом конструкторе, который согласно inte rnet является большим нет-нет и из того, что я наблюдал, выясняется, работает ли оно или нет в состоянии гонки.

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

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

Буду также признателен за общие советы и / или ссылки на ресурсы. Если это не было очевидно, я все еще изучаю C ++.

Я использую clang ++ v6.0 с -lpthread и --std = c ++ 17 в Ubuntu 18.04

Minimal- i sh Пример воспроизведения:

#include <functional>
#include <future>
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>

class Thing {
public:
  void tick() { std::cout << "tick" << std::endl; };
};

class Threader {
protected:
  bool stop;

public:
  std::thread th;

  virtual void action() = 0;

  void run() {
    std::cout << "th run" << std::endl;
    action();
  };

  Threader() : stop(false), th(std::thread(&Threader::run, this)){};

  ~Threader() {
    std::cout << "destructed" << std::endl;
    stop = true;
    if (th.joinable()) {
      th.join();
    }
  }
};

class SimulateThread : Threader {
  std::mutex &mx;
  std::shared_ptr<Thing> thing_p;
  int sleep_millis;

public:
  SimulateThread(std::mutex &mx, std::shared_ptr<Thing> thing_p,
                 int sleep_millis)
      : mx(mx), thing_p(thing_p), sleep_millis(sleep_millis){};

  void action() {
    for (;;) {
      if (stop) {
        std::cout << "stopping" << std::endl;
        return;
      }
      {
        auto lock = std::scoped_lock(mx);
        thing_p.get()->tick();
      }
      if (sleep_millis > 0)
        std::this_thread::sleep_for(std::chrono::milliseconds(sleep_millis));
    }
  }
};

int main() {
  auto thing = Thing{};
  auto thing_p = std::make_shared<Thing>(thing);
  auto mx = std::mutex{};
  {
    auto st = SimulateThread{mx, thing_p, 100};
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }

  std::cout << "done" << std::endl;
  return 0;
}

Вывод:

$ make && ./threadthing
clang++ --std=c++17 -lpthread threadthing.cpp -o threadthing
th run
tick
tick
tick
tick
tick
tick
tick
tick
tick
tick
destructed
stopping
done

Так что это работает. Но когда я меняю основной вызов на это:

int main() {
  auto thing = Thing{};
  auto thing_p = std::make_shared<Thing>(thing);
  auto mx = std::mutex{};
  {
    auto st = SimulateThread{mx, thing_p, 100};
  }
  std::this_thread::sleep_for(std::chrono::seconds(1));

  std::cout << "done" << std::endl;
  return 0;
}

Вывод такой:

$ make && ./threadthing
clang++ --std=c++17 -lpthread threadthing.cpp -o threadthing
destructed
th run
pure virtual method called
terminate called without an active exception
Aborted

(выход с кодом 134)

Насколько я понимаю это, это должно вести себя так же, за исключением того, что никогда не печатать «галочку» или, может быть, только один раз. Тот факт, что он не говорит мне, что есть условие гонки между std :: thread, фактически начинающимся после его инициализации, и экземпляром SimulateThread, который полностью инициализируется.

Примечания к примеру:

  • В полной программе main система ждет вечно, ловит Ctrl- C и выходит из блока с объектами моего потока в них. Этот пример просто ждет и завершает работу, но это должно работать так же, как и в этом случае.

  • Я думаю, что флаг остановки должен быть заключен в std :: atomi c. Да / нет?

  • Он получает shared_ptr (по значению) в используемое совместно используемое состояние и использует scoped_lock для мьютекса, разделяемого между потоками (по ссылке), так что каждый может получить доступ без рас. Это правда?

Версия, использующая лямбды, для потомков:

#include <functional>
#include <future>
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
template <typename F> class Threader {
  volatile bool stop;
  F action;

public:
  void run() {
    std::cout << "th run" << std::endl;
    action(stop);
  };

  std::thread th;

  Threader(F action)
      : stop(false), action(action), th(std::thread(&Threader::run, this)){};

  ~Threader() {
    stop = true;
    if (th.joinable()) {
      th.join();
    }
  }
};

int main() {

  auto thing_p = std::make_shared<Thing>(thing);
  auto mx = std::mutex{};

  auto simulator = Threader{[&mx, thing_p](volatile bool &stop) {
    for (;;) {
      if (stop) {
        return;
      }
      {
        auto lock = std::scoped_lock(mx);
        thing_p.get()->tick();
      }
      std::this_thread::sleep_for(std::chrono::milliseconds(0));
    }
  }};

  std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

Что мне меньше всего нравится в подходе с лямбдами, так это то, что реализация лямбды будет в вызывающем методе. Но когда я попытался вытащить его в функцию, возвращающую экземпляр Threader, я застрял в типах.

Заранее спасибо за помощь!

1 Ответ

1 голос
/ 02 мая 2020
pure virtual method called
terminate called without an active exception

Причина, по которой вы получаете это сообщение, в том, что у вас очень тонкое состояние гонки на vptr вашего объекта. Поскольку вызовы virtual не работают в конструкторах или деструкторах (подобъекты еще не созданы), многие реализации обновляют vptr по мере ввода конструкторов / деструкторов. В вашем случае поток пытается начать работу до того, как объект будет полностью построен. У вас также есть такая же проблема в вашем деструкторе, поскольку к моменту попытки join дочерний объект уже был уничтожен.

Решение состоит в том, что вам нужно запустить и присоединиться к вашему потоку на полностью построенном объекты (например, вне вашей class иерархии); ваша лямбда-версия делает это.

...