I am facing the following problem:
My program uses the concept of "tasks", where a task exists of code (functions) that together make up a critical area. Each task (object) knows what its "key" is, but that key might be shared with other tasks. The "key" (a pointer to yet another resource-- called B below) does not change for the lifetime of the task. Yet, it has to exist before I can create the second resource.
To clarify, from the point of the view of a task:
- Create resource A, if that does not already exist.
- Once we have A, create resource B(A), if that does not already exist.
where B is a function of A: only one B should be created per A. Multiple such tasks exist and some might create different A's, or see that their A already exists and use the same A as other tasks.
The creation of B hence requires its own mutex: all tasks run in parallel, but only one B (per A) should be created.
Once B is created however, I do not want to have to lock that mutex every time - just in case it is still being created. The creation only happens once and reading will happen very often the rest of the execution of the program.
The program flow is therefore as follows:
class Application {
std::map<A*, B> all_B;
std::mutex all_B_mutex;
B* B_broker(A*);
};
struct A {
std::atomic<B*> cached_B_ptr;
};
Each task, as soon as it has A, calls
B_broker(A)
- once. This function will lock a mutex and create B, if that doesn't exist yet, and return a pointer to that new B.The thread that gets a non-NULL pointer from
B_broker(A)
stores thisB*
in a member variable of A:A::cached_B_ptr
.If B(A) already exists then
B_broker(A)
will return nullptr. The calling thread then waits until it sees thatA::cached_B_ptr
is non-null.All accesses to
A::cached_B_ptr
are withmemory_order_relaxed
, so that there is no difference with just reading normal variables without a lock (hence the "lock free" in the title). During normal operation of the program this variable is only being read, so there is no contention.
Although a given task runs inside its own critical area, different tasks might share the same A, so access to A::cached_B_ptr
is not protected by a mutex (or, the same mutex anyway).
The question now is: can this be done safely? Where,
- thread A writes to
A::cached_B_ptr
(relaxed) - no related mutexes. - thread B runs a task, unrelated to A, and wants to use
A::cached_B_ptr
. B does not write toA::cached_B_ptr
(B_broker(A)
returnednullptr
) but sinceA::cached_B_ptr
is not protected by a mutex is also can't rely on it already being set; so B waits until it reads a non-null value. - thread C continuous to run the task of B and also wants to use
A::cached_B_ptr
, but this time without any checks or waiting. What B did is a once-per-task operation that is only done when a task is started, and that only shortly after the program was started (when there is still a possible race with A writing toA::cached_B_ptr
).
PS The follow two questions might be related: