Briefly, I have a class that lazily initializes one of its data members and I'd like to figure out the best way to do this in a multithreaded environment.
In more detail, my class currently looks something like this:
#include <algorithm>
#include <optional>
#include <vector>
class A_single_threaded
{
public:
bool query(const int val) const
{
// if opt_vec is not initialized, do that now
if (!(opt_vec.has_value()))
initialize_vec();
// return true if opt_vec contains val
return std::find(std::cbegin(opt_vec), std::cend(opt_vec), val) != std::cend(opt_vec);
}
private:
mutable std::optional<std::vector<int>> opt_vec;
// sets opt_vec
void initialize_vec() const;
};
initialize_vec is the only method that modifies opt_vec, and query is the only method that calls initialize_vec. opt_vec can potentially be empty after initialize_vec returns, so giving the data member std::optional type helps distinguish when it's unset and when it's set and empty. In other instances opt_vec winds up being large and initializing it is time-consuming. And since not every A_single_threaded instance will need to run query anyway, it makes sense to avoid initializing opt_vec until a user call to query makes clear that initialization is necessary.
The approach above seems OK for a single thread, but I think it isn't naturally multithread-able. The calls to opt_vec.has_value() and initialize_vec are necessarily unsynchronized, and the gap between their return times allows for a data race that I don't think can be fixed with a mutex. Instead I think the correct solution involves replacing the std::optional with std::call_once, something like the below:
#include <algorithm>
#include <mutex>
#include <vector>
class A_multi_threaded
{
public:
bool query(const int val) const
{
// if opt_vec is not initialized, do that now
std::call_once(opt_vec_flag, initialize_vec, this);
// return true if opt_vec contains val
return std::find(std::cbegin(opt_vec), std::cend(opt_vec), val) != std::cend(opt_vec);
}
private:
mutable std::vector<int> opt_vec;
mutable std::once_flag opt_vec_flag;
// sets opt_vec
void initialize_vec() const;
};
I'd appreciate answers to a couple of questions:
- Is the implementation I sketched for
A_multi_threadedactually thread-safe? - Will
A_multi_threadedhave the same behavior asA_single_threadedin a single-threaded environment? - Is there a sensible implementation of
A_multi_threadedthat mimics the implementation ofA_single_threaded?
opt_vec. (As long as no thread is writing no sync. is needed but the initialization is the (exceptional) writing. So...) However, I found (in this link) this means that all concurrent calls to call_once are guaranteed to observe any side-effects made by the active call, with no additional synchronization. (Emphasize mine.) IMHO, this describes that my doubts are wrong.std::call_once(opt_vec_flag, initialize_vec);needs to bestd::call_once(opt_vec_flag, initialize_vec, this);and using the flag like this will make the class non-movable. Aside from that I don't see any issue as far as the shown code goes. You also potentially need to make sure that destruction ofA_multi_threadedis synchronized, e.g. by using astd::shared_ptr.opt_vec_flagalso needs to bemutable(so I am not sure whether it really makes sense to have the functions beconst).A_multi_threadedis supposed to be a singleton, but doesn't work if there are supposed to be multipleA_multi_threadedinstances.