Including std::lock_guard in extra scope

Does is make sense to do something like putting a std::lock_guard in an extra scope so that the locking period is as short as possible?

Pseudo code:

// all used variables beside the lock_guard are created and initialized somewhere else
...// do something

{ // open new scope
    std::lock_guard<std::mutex> lock(mut);
    shared_var = newValue;  
} // close the scope

... // do some other stuff (that might take longer)

Are there more advantages besides having a short lock duration?

What might be negative side effects?

Yes, it certainly makes sense to limit the scope of lock guards to be as short as possible, but not shorter.

The longer you hold a lock, the more likely it is that a thread will block waiting for that lock, which impacts performance as is thus usually considered a bad thing.

However, you must make sure that the program is still correct and that the lock is held at all times when it must be, i.e. when the shared resource protected by the lock is accessed or modified.

There may be one more point to consider (I do not have enough practical experience here to speak with certainty). Locking/releasing a mutex can potentially be an operation with nontrivial performance costs itself. Therefore, it may turn out that keeping a lock for a slightly longer period instead of unlocking & re-locking it several times in the course of one operation can actually improve overall performace. This is something which profiling could show you.

Yes, it certainly makes sense to limit the scope of lock guards to be as short as possible, but not shorter. The longer you hold a lock, the more likely it is that a thread will block waiting for that lock, which impacts performance as is thus usually considered a bad thing.

There might be a disadvantage: you cannot protect initializations this way. For example:

{
    std::lock_guard<std::mutex> lock(mut);
    Some_resource var{shared_var};
} // oops! var is lost

you have to use assignment like this:

Some_resource var;
{
    std::lock_guard<std::mutex> lock(mut);
    var = shared_Var;
}

which may be suboptimal for some types, for which default initialization followed by assignment is less efficient than directly initializing. Moreover, in some situations, you cannot change the variable after initialization. (e.g. const variables)


user32434999 pointed out this solution:

// use an immediately-invoked temporary lambda
Some_resource var {
    [&] {
        std::lock_guard<std::mutex> lock(mut);
        return shared_var;
    } () // parentheses for invoke
};

This way, you can protect the retrieval process, but the initialization itself is still not guarded.

Including std::lock_guard in extra scope - xtduliu.blogspot.com 8

Yes, it makes sense.

There are no other advantages, and there are no side-effects (it is a good way to write it).

An even better way, is to extract it into a private member function (if you have an operation that is synchronized this way, you might as well give the operation its own name):

{
    // all used variables beside the lock_guard are created and initialized somewhere else
    ...// do something

    set_var(new_value);

    ... // do some other stuff (that might take longer)
}

void your_class::set_value(int new_value)
{
    std::lock_guard<std::mutex> lock(mut);
    shared_var = new_value;
}

The lock guard locks the mutex when it's constructed: that is, when control flow of your program reaches the declaration of lock.It releases (unlocks) the mutex when the guard is destructed, which happens when control flows through the } terminating the then-block (either by flowing through there naturally, or "forced" by a return, throw, or jump statement).

Using an extra scope specifically to limit the lifetime of an std::lock_guard object is indeed good practice. As the other answers point out, locking your mutex for the shortest period of time will reduce the chances that another thread will block on the mutex.

I see one more point that was not mentioned in the other answers: transactional operations. Let's use the classical example of a money transfer between two bank accounts. For your banking program to be correct, the modification of the two bank account's balance must be done without unlocking the mutex in between. Otherwise, it would be possible for another thread to lock the mutex while the program is in a weird state where only one of the accounts was credited/debited while the other account's balance was untouched!

With this in mind, it is not enough to ensure that the mutex is locked when each shared resource is modified. Sometimes, you must keep the mutex locked for a period of time spanning the modification of all the shared resources that form a transaction.

EDIT:

If for some reason keeping the mutex locked for the whole duration of the transaction is not acceptable, you can use the following algorithm: 1. Lock mutex, read input data, unlock mutex. 2. Perform all needed computations, save results locally. 3. Lock mutex, check that input data has not changed, perform the transaction with readily available results, unlock the mutex.

If the input data has changed during the execution of step 2, throw away the results and start over with the fresh input data.

When control leaves the scope in which the scoped_lock object was created, the scoped_lock is destructed and the mutexes are released, in reverse order. If several mutexes are given, deadlock avoidance algorithm is used as if by std::lock. The scoped_lock class is non-copyable.

I don't see the reason to do it. If you do something so simple as "set one variable" - use atomic<> and you don't need mutex and lock at all. If you do something complicated - extract this code into new function and use lock in its first line.

std:: lock_guard. The class lock_guard is a mutex wrapper that provides a convenient RAII-style mechanism for owning a mutex for the duration of a scoped block. When a lock_guard object is created, it attempts to take ownership of the mutex it is given. When control leaves the scope in which the lock_guard object was created, the lock_guard is destructed and the mutex is released.

It is the simplest lock, and is specially useful as an object with automatic duration that lasts until the end of its context. In this way, it guarantees the mutex object is properly unlocked in case an exception is thrown. Note though that the lock_guard object does not manage the lifetime of the mutex object in any way: the duration of the mutex object shall extend at least until the destruction of the lock_guard that locks it.

Including std::lock_guard in extra scope What is wrong with my KVL approach on a really simple circuit? Why isn't integral defined as the area under the graph of function?

During the course of the project, the client asks you to include a video overview of the company. The video is not specified in the scope of the project and is therefore out of scope. While you may be happy to do the video work for an extra charge, this will require a revision of the scope and cost and time estimation for the project.

Comments
  • You could use immediately invoked lambda
  • @KabCode It is as the scope is only due to the scheme mentioned in the question
  • Some_resource var{ [&]() { std::lock_guard<std::mutex> lock(mut); return shared_var; }() };
  • @L.F. I also think shared_var deserves a const
  • The lambda does add visual clutter. May be worth simplifying the syntax noise a bit by changing [&]() {[&]{ (i.e. removing extraneous parentheses).
  • Good addition. This will help to keep the code clean. Caveat might be if you have to set this variable often then the costs of creating new locks might be more expensive than lock/unlock (but this is not the scope of the question anymore).
  • @KabCode why would you set the value often? Each thread should minimize access to shared memory. When writing several times you should write only the last value, unless other threads are supposed to see the intermediate values, but then you have no choice other than locking for each write
  • @KabCode ah I think now I got what you wanted to say. You are worried by the overhead of creating a new lock instead of reusing the object (?). In that case I suggest you to take a look at the implementation, the lock isnt really doing much, it merely locks the mutex and realeases it, there is not much overhead
  • Good point - could you provide a (pseudo)code sample how you would solve this issue programmatically?