Search
Write a publication
Pull to refresh

Comments 19

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

    bool StopFunc() // метод для её остановки
    {
        if (!FuncThread && !FuncStatus)
            return false;
        FuncStatus = false;
        if (FuncThread.joinable()) FuncThread.join();  // не . а ->
        if (FuncThread) delete FuncThread;  // delete не нуждается в проверке на nullptr 
        FuncThread = nullptr;
        return true;
    }
    bool FuncStatus() { return FuncStatus; } // два члена с одним именем

private:
    thread* FuncThread; // (лишняя косвенность)

Спасибо большое! Намудрил при вставке кода из проекта в статью, сейчас поправлю. На счёт delete не нуждается в проверке на nullptr ... честно говоря не знал. Век живи, век учись :)

Меня не покидает чувство, что вы только что переизобрели std::promise/future/async. Да ещё и с довольно странным дизайном, в котором у вас есть явные владельцы и всё такое.

Имхо, надо было отталкиваться от описания задачи, а не от допиливания фич к существующему первичному коду. Причём замечу, что если с самого начала взять фьючерсы, то оперировать тредами вообще не придётся. (Они там будут под капотом).

Итак, хочется

  • асинхронную функцию, которая как-то сама может проверять флажок остановки

  • частный случай - функцию, которая делает это в цикле, собранном из условия и тела

Ну окей. Договоримся, что в первом случае это void(atomic_bool& /*stop*/)

Второй делается из первого нехитрым образом:

auto make_async_while_loop(auto precondition, auto body) {
  return [=](atomic_bool& stop) {
    while(!stop && precondition()) { body(); }
  };
}

auto make_async_do_while_loop(auto body, auto postcondition) {
  return [=](atomic_bool& stop) {
    while(!stop && postcondition(body())) {}
  }
}

Если body само по себе длинная операция - ну окей, переделать на body(stop)

Всё. Теперь нам нужна сущность, у которой есть две ручки: cancel и wait. Первая посылает команду остановки (мгновенно), вторая - ждёт завершения, какое бы оно ни было.

class MyAsync {
public:
  explicit MyAsync(auto async_fun):
    future_{
      std::async(
        std::launch::async,
        [this, async_fun=std::move(async_fun)]{ async_fun(stop_); }
      )
    }
  {}
  ~MyAsync() { cancel(); wait(); }
  void cancel() { stop_ = true; }
  void wait() { future_.wait(); }

  MyAsync(MyAsync&) = delete;  // поскольку лямбда связывает указатель

private:
  std::atomic_bool stop_;
  std::future<void> future_;
};

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

using MyUniqueAsyncPtr = std::unique_ptr<MyAsync>;
using MySharedAsyncPtr = std::shared_ptr<MyAsync>;
// в принципе, этого уже хватит

class MySharedAsync {
public:
  explicit MySharedAsync(auto async_fun):
    ptr_{std::make_shared<MyAsync>(std::move(async_fun))}
  {}
  void cancel() { ptr_->cancel(); }
  void wait() { ptr_->wait(); }
  // деструктор сам подождёт в деструкторе указуемого
private:
  MySharedAsyncPtr ptr_;
};

уже после написания статьи и повторного курения стандарта мне пришло в голову, что даже имеющийся код можно было значительно упростить при помощи std::function. Я и не настаивал на том, что решение идеально)

Конечно, если желание узнать состояние асинхронной задачи (запускаем - запустили - отменили - закончили с отменой - закончили без отмены) входит в ТЗ, то реализация несколько усложнится.

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

Поэтому - а насколько часто нам это в коде нужно? Может быть, "выстрелил и забыл" тоже подойдёт?

Спасибо за развёрнутый анализ нашего кода и предложения, действительно выглядит красиво и лаконично.

В действительности мы тыкались и с async и с пресловутым thread в наши задачи и ... всё как-то не нравилось. Возможно действительно мы плохо изучили стандарт, чтобы удобно оперировать уже имеющимся функционалом.

Однако получившийся кусок кода настолько удачно встал у меня во все места в имеющимся проекте, что возникло ощущение надобности про это написать :) В любом случае задачка была интересная и опыта прибавила

Поэтому - а насколько часто нам это в коде нужно?

В теории где-нибудь в деструкторе можно посмотреть статус, если работает — дать стоп и подождать немного. Если всё ещё работает после ожидания — прибить поток.

честно говоря не до конца понимаю при чём тут упоминание thread pool ... да, у нашей обёртки есть возможность помещаться в динамические STL контейнеры, но это не было самоцелью задумки. К тому же выше говорил, что действительно не сеньор и, вероятно, занимаюсь велосипедами)

Thread pool при том что это правильная абстракция для такого рода задач. Объеденить вычисление и тред в одном объекте это, наверно, хорошее упражнение чтобы попечатать код, походить на cppreference и т.д. но человечество уже придумало правильную абстракцию.

Тред это дорогой ресурс, а вычисления чаще всего короткие и их много. И даже если функция вычислительно сложная, всё равно делать абстракцию объеденяющую функцию и тред неразумно.

Я правильно понимаю, что для того, чтобы "асинхронно" остановить уже запущенный тред вы стартуете в StopAsync новый тред? Т.е. одного рабочего треда мало и нужен второй?

Да, об этом и сказано было в послесловии и ... нам самим это совершенно не нравится)

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

Почему бы просто не сделать в деструкторе что-то вроде:

if(MainThread.joinable()) {
  TFStatus = ThreadedFuncStatus_T::is_stopping;
  MainThread.join();
}

Если тред еще не завершился, то вы долждетесь его завершения. А если завершился (или не стартовал), то ничего и не будете ждать.

Дык история в том, чтобы в основном потоке программы дать команду на остановку и не ждать его завершения и идти дальше. А вот важен момент отслеживания реальной остановки треда и его очистки после.
В любом случае коллеги уже дали несколько решений как задвинуть этот код на пыльную полку и сделать сильно лучше)

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

Ну так вы будете только сигнал на остановку давать в StopAsync (т.е. только изменять значение TFStatus) и все. Не нужно ни дополнительного треда, ни ожидания.

Вызвали StopAsync, изменился TFStatus, вы пошли дальше.
Рабочий тред увидел это изменение и завершился.

Если рабочий тред завершился до вызова деструктора ThreadedFunc, то все OK.
Если не успел, то вы в деструкторе ThreadedFunc подождете этого на join-е.

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

хранил не сам поток, а ссылку на него и хотел сразу же при его завершении отчищать память по указателю

Оказывался в подобной ситуации. Мое решение было в том, что у приложения была отдельная фоновая нить, которая занималась подобной "чисткой". Когда нужно было дождаться завершения рабочего треда с последующим удалением связанного с тредом объекта, то этой фоновой нити отправлялось специальное сообщение с нужной информацией. Фоновая нить получала сообщение, делала join и дожидалась завершения рабочей нити, после чего удаляла нужный объект.

Если Вы сталкивались с многопоточными методами в классах 

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

В нашем случае мы реализовывали класс, описывающий hardware устройство со своими приколами. Так как железка была сугубо самодельная - работа с ней вынуждена стала многопоточной (на что повлиять мы не могли, поскольку протокол и железка делается вообще не нами). И по факту эти методы класса являлись обработчиками разнородной информации.

Схема примерно такая:

  1. Получить данные от железки в буфер

  2. Прочитаать заголовок

  3. Определить тип оьработки

  4. Направить буффер в конкретный обработчик в отдельном потоке

  5. Проверить очередь команд для железки

Схема же обработчика следующая:

  1. Принять буффер

  2. Обработать данные

  3. Сгенерировать ответ

  4. Добавить ответ в очередь

Более того (почему не подходит async), обработка иногда занимает несколько секунд / минут и/или является накопительным алгоритмом от входных данных, что вынуждает делать метод практически полностью автономным в своём while (true) цикле.

Sign up to leave a comment.

Articles