Locking in function hierarchies
I am currently facing some concurrent programming design issues in C ++ and I was wondering if you could help me:
Suppose that some function func
acts on some object obj
. During these operations, a lock (which can be a member variable obj
) must be held . Suppose now what
func
is calling the subfunction func_2
while it is holding the lock. Now func_2
works with an object that is already locked. However, what if I also want to call func_2
from another location without holding the lock? Should I func_2
block obj
or not? I see 3 possibilities:
- I could pass
bool
infunc_2
to indicate if a lock is required. This seems to introduce a lot of boilerplate code though. - I could use a recursive lock and always lock
obj
infunc_2
. Recursive locks seem to be problematic, see here . - I could assume that every caller is
func_2
already holding a lock. I would like to document this and maybe provide it (at least in debug mode). Is it smart to have functions that make assumptions about which locks are / are not being held by the calling thread? More generally, how should I decide from a design standpoint whether a functionobj
should block and should it assume that it is already blocked? (Obviously, if a function assumes that some locks are held, then it can only call functions that make at least equally strong assumptions, but other than that?)
My question is this: which of these approaches is used in practice and why?
Thank you in advance
hfhc2
source to share
1. Transmitting the blocking or missing indicator:
You provide the choice of blocking to the caller. This is mistake:
- the caller cannot make the right choice
- the caller needs to know the implementation details of the object, thereby violating the principle of encapsulation.
- the caller needs access to the mutex
- If you have multiple objects, you end up making the conditions easier for blocking.
2.recursive lock:
You have already highlighted the problem.
3. Pass the blocking of responsibility to the caller:
Among the various alternatives you suggest, this seems to be the most consistent. Unlike 1, ou gives no choice, but you are solely responsible for blocking. This is part of the contract for using func_2.
You can even argue that the lock is set on the object to prevent errors (although the validation check will be limited because you don't have to be sure who owns the lock).
4. Write down your design:
If you need to make sure that the object is locked in func_2, it means that you have a critical section that you must protect. There is a chance that both functions should block because they perform some lower-level operation on obj and should prevent data jumps in the instable state of the object.
I would highly recommend seeing if it will be possible to extract these low-level routines from func and func_2 and encapsulate them into simpler primitive functions on obj. This approach can also help block shorter sequences, which increases the opportunity for real concurrency.
source to share
Okay, just like the other observation. I recently read the glib API documentation , specifically the section on message queues. I've found that most of the functions running on these queues have two options: function
and function_unlocked
. The idea is that if a programmer wants to perform one operation, for example, jump out of the queue, this can be done with g_async_queue_pop()
. The function will automatically take care of blocking / unblocking the queue. However, if the programmer wants, for example, to pop two items without interruption, the following sequence can be used:
GAsyncQueue *queue = g_async_queue_new();
// ...
g_async_queue_lock(queue);
g_async_queue_pop_unlocked(queue);
g_async_queue_pop_unlocked(queue);
g_async_queue_unlock(queue);
This is similar to my third approach. In addition, assumptions are made about the state of certain locks, they are required by the API and require documentation.
source to share