I wanted to create a class that would represent a task that can be started running asynchronously and will run continuously (effectively in a detached thread) until a stop signal is received. The usage for the sake of this question would look like this:
auto task = std::make_shared<Task>();
task->start(); // starts the task running asynchronously
... after some time passes ...
task->stop(); // signals to stop the task
task->future.get(); // waits for task to stop running and return its result
However, a key feature of this Task
class is that I cannot guarantee that the future will be waited/got... i.e. the last line may not get called before the shared pointer is destroyed.
A stripped-down toy version of the class I wrote is as follows (please ignore that everything is in public, this is just for this example's simplicity):
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
~MyClass() { std::cout << "Destructor called" << std::endl; }
void start() {
future = std::async(std::launch::async, &MyClass::method, this->shared_from_this());
}
void stop() { m_stop = true; }
void method() {
std::cout << "running" << std::endl;
do {
std::this_thread::sleep_for(std::chrono::seconds(1));
} while(m_stop == false);
std::cout << "stopped" << std::endl;
return;
}
std::future<void> future;
std::atomic<bool> m_stop = false;
};
However, I discovered an undesirable feature of this code: if instead of get
on the future, I just wait
(e.g. if I don't care about the result of method
, which in this case is a void anyway), then when task
is deleted, the instance doesn't get destroyed.
I.e. doing task->future.get()
gives:
running
stopped
Destructor called
But task->future.wait()
gives:
running
stopped
From reading answer to What is the lifetime of the arguments of std::async? I believe the problem here is the this->shared_from_this()
argument to std::async
won't be destroyed until the future from the async has been made invalid (through get
or destruction or otherwise). So this shared_ptr is keeping the class instance alive.
Solution Attempt 1:
Replace the line in start
with:
future = std::async(std::launch::async, [this]() {
return this->shared_from_this()->method();
});
This ensures shared_ptr it creates is destroyed when the method completes, but I have been worried that there's nothing to stop this
being destroyed between the time of it being captured by the lambda capture (which happens at this line, correct?) and the time the lambda is executed in the new thread. Is this a real possibility?
Solution Attempt 2:
To protect the this
(task
) being destroyed before the lambda function runs, I add another member variable std::shared_ptr<MyClass> myself
then my start method can look like this:
myself = this->shared_from_this();
future = std::async(std::launch::async, [this]() {
auto my_ptr = std::move(this->myself);
return myself->method();
});
Here the idea is that myself
will ensure that if I delete the task
shared_ptr, I don't destroy the class. Then inside the lambda, the shared_ptr is transferred to the local my_ptr
variable, which is destroyed on exit.
Are there issues with this solution, or have I overlooked a much cleaner way of achieving the sort functionality I'm after?
Thanks!