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; // (лишняя косвенность)
Меня не покидает чувство, что вы только что переизобрели 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_;
};
Конечно, если желание узнать состояние асинхронной задачи (запускаем - запустили - отменили - закончили с отменой - закончили без отмены) входит в ТЗ, то реализация несколько усложнится.
Но тут мы встанем на очень скользкую дорожку конкурентного выполнения. Когда информация о статусе будет зависеть от моментов, когда мы этот статус спрашиваем и параллельно изменяем.
Поэтому - а насколько часто нам это в коде нужно? Может быть, "выстрелил и забыл" тоже подойдёт?
Спасибо за развёрнутый анализ нашего кода и предложения, действительно выглядит красиво и лаконично.
В действительности мы тыкались и с async и с пресловутым thread в наши задачи и ... всё как-то не нравилось. Возможно действительно мы плохо изучили стандарт, чтобы удобно оперировать уже имеющимся функционалом.
Однако получившийся кусок кода настолько удачно встал у меня во все места в имеющимся проекте, что возникло ощущение надобности про это написать :) В любом случае задачка была интересная и опыта прибавила
Поэтому - а насколько часто нам это в коде нужно?
В теории где-нибудь в деструкторе можно посмотреть статус, если работает — дать стоп и подождать немного. Если всё ещё работает после ожидания — прибить поток.
Зумеры переизобрели thread pool? https://en.m.wikipedia.org/wiki/Thread_pool
Вижу что выше коллеги уже написали.
Чем не подошли уже существующие и готовые библиотеки?
честно говоря не до конца понимаю при чём тут упоминание 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 устройство со своими приколами. Так как железка была сугубо самодельная - работа с ней вынуждена стала многопоточной (на что повлиять мы не могли, поскольку протокол и железка делается вообще не нами). И по факту эти методы класса являлись обработчиками разнородной информации.
Схема примерно такая:
Получить данные от железки в буфер
Прочитаать заголовок
Определить тип оьработки
Направить буффер в конкретный обработчик в отдельном потоке
Проверить очередь команд для железки
Схема же обработчика следующая:
Принять буффер
Обработать данные
Сгенерировать ответ
Добавить ответ в очередь
Более того (почему не подходит async), обработка иногда занимает несколько секунд / минут и/или является накопительным алгоритмом от входных данных, что вынуждает делать метод практически полностью автономным в своём while (true) цикле.
Волшебство ООП или как упростить многопоточное программирование C++