40

In Java:

Lock lock = new ReentrantLock();
try{
  lock.lock();
  someFunctionLikelyToCauseAnException();
}
catch(e){...}
finally {
  lock.unlock();
}

My question is with this above example we know that the lock WILL always get unlocked because finally always executes, but what is the guarantee with C++?

mutex m;
m.lock();
someFunctionLikelyToCauseAnException();
/// ????

How will this work and why?

walen
  • 7,103
  • 2
  • 37
  • 58
Brijendar Bakchodia
  • 1,567
  • 1
  • 9
  • 15
  • 24
    Use [std::lock_guard](https://en.cppreference.com/w/cpp/thread/lock_guard) for RAII approach. `std::lock_guard` object will unlock whatever mutex it had in destructor. – Yksisarvinen Sep 06 '18 at 14:55
  • 7
    You should read about [stack unwinding](https://stackoverflow.com/questions/2331316/what-is-stack-unwinding), a very important feature of c++. – François Andrieux Sep 06 '18 at 14:56
  • 16
    in a nutshell: c++ does not need finally, because it has destructors – 463035818_is_not_an_ai Sep 06 '18 at 14:58
  • 6
    @user4581301 Where have you read that Java has destructors? It has finalizers. It's called something different specifically because it's not a destructor. The issue is that the Finalizer is often called a Destructor, which I have a problem with. – Andrew T Finnell Sep 06 '18 at 17:24
  • @AndrewTFinnell I stand corrected and blame learning Java 15-20 years ago from a Deitel and Deitel book. Smurf. It's 2018. Closer to 22 years ago. – user4581301 Sep 06 '18 at 17:31
  • 1
    This is not to answer the question but rather to caution you to think carefully about your approach in both Java and C++ (and other languages like C# that have similar issues.) First, your claim that the finally *always* runs is false. A finally block only runs *if the try terminates normally or abnormally*, and there is no guarantee that a try block terminates. Second, and more importantly: your question implies that you believe that "unlock on abnormal termination of try block" is a *good thing* but in fact it is a *bad thing*. – Eric Lippert Sep 06 '18 at 17:37
  • 14
    A lock is typically used with the pattern: "(1) state is consistent but wrong (2) enter the try (3) wait until you can take the lock (4) make state inconsistent (5) make state right and consistent, (6) enter the finally (7) release the lock (8) state is now consistent and right." **What happens if a throw occurs after step 4 and before step 5?** We go directly to step 6 but state is now both inconsistent and wrong! We then unlock the lock, and *code that is waiting now has access to inconsistent wrong state*, and *it crashes*. This pattern of unlocking in a finally is *super dangerous*. – Eric Lippert Sep 06 '18 at 17:39
  • 1
    The above comment is more important than the question's answers. – user4581301 Sep 06 '18 at 19:13
  • @Yksisarvinen Please. Stop answering in the comments section. Write an answer, or don't. – Lightness Races in Orbit Sep 07 '18 at 11:08
  • @EricLippert I don't think that's necessarily true, unless I'm missing something fundamental. If you need to recover your state to something consistent (if wrong) after an exception during these steps, then that's what `catch` is for. You seem to be suggesting that no exceptional case is ever recoverable, which is not true whether you're locking around the whole process or not. – Lightness Races in Orbit Sep 07 '18 at 11:10
  • What @EricLippert is saying, in a nutshell, is that the lock probably is not the _only_ thing that needs to be cleaned up in case of an exception. Some authors would suggest two levels of exception handling: An outer function with a try-catch that calls an inner function with a try catch. The outer fn would be responsible for managing the lock, and ensuring that it will be released before the function returns, no matter _how_ it returns. The inner fn would be responsible for operating on the shared data, and ensuring that shared data are valid before it returns, no matter how it returns. – Ohm's Lawman Sep 07 '18 at 13:27
  • @besmirched: But that doesn't really solve the problem that Eric Lippert describes. Either the inner function's catch-block successfully restores the consistent state (in which case it might as well release the lock as well) or it doesn't (in which case you're still not in a consistent state, so still shouldn't unlock the lock). – ruakh Sep 07 '18 at 16:19
  • @ruakh, "Valid state" doesn't always mean "successful result." Some times, it only means that the program can continue to run and do useful things. Maybe the function was supposed to add a record to the data structure. Maybe, by the time the exception was thrown, it's too late to restore the original state; but maybe the function could clean up by marking the new record as _bogus_. As long as the data structure is still well-formed, the program's threads still would be able to traverse it. And, if they know how to recognize and work around the bogus record, then the program could keep running. – Ohm's Lawman Sep 07 '18 at 17:09
  • Right, I'm not saying that this problem is impossible to solve. I'm saying that *it's a problem that you need to be aware of*. The fundamental problem is actually a language design problem: we use exceptions for way, way too many things. That the same mechanism handles both violations of program invariants, like null dereferences, and domain violations, like divides by zero, and exogenous rare conditions, like disks being full, and genuinely fatal exogenous conditions, like being out of stack, is what makes this pattern dangerous. – Eric Lippert Sep 07 '18 at 17:20
  • It's like there's a protocol for sharing a bathroom: you lock the door when you're in there and unlock it when you're done and let the next person in. But you also unlock it when you run out of soap. And when the hot water runs out. And when the house collapses due to an unforeseen structural fault. And when terrorists have set off a bomb and the whole thing is smoking rubble. The protocol of "let the next person in immediately" doesn't actually make sense for some of those scenarios; sometimes you want to evacuate the building. – Eric Lippert Sep 07 '18 at 17:25

3 Answers3

66

For this we use the RAII-style construct std::lock_guard. When you use

std::mutex m;
{ // start of some scope
    std::lock_guard lg(m);
    // stuff
} // end of scope

lg will ensure that m will be unlocked no matter what path the scope is left as it is destroyed at scope exit and std::lock_guards destructor will call unlock

Even if an exception is thrown the stack will be unwound (stack unwinding) and that process destroys lg which in turn will call unlock guaranteeing that the lock is released.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • 9
    Just in case the OP doesn't see what's really going on here; `lg` is a _local variable._ The `lg(m)` expression calls the constructor of the `std::lock_guard` class, and C++ guarantees that the destructor of any local variable will be promptly called when the thread exits from the variable's scope--no matter _how_ the thread exits. The `lock_guard` constructor locks the given lock `m`, and the destructor unlocks it. – Ohm's Lawman Sep 06 '18 at 15:42
  • 12
    Note to future readers: RAII is one of C++'s most important idioms and describes one of the biggest ideological differences between C++ and Java. If you are coming to C++ from Java you life will be hundreds of times easier once you start taking advantages of it. Read the given link and [this one.](https://stackoverflow.com/questions/2321511/what-is-meant-by-resource-acquisition-is-initialization-raii) – user4581301 Sep 06 '18 at 16:38
  • So `lock_guard` is wrapping the mutex `m` basically like a smart pointer? – Brijendar Bakchodia Sep 06 '18 at 17:28
  • @BrijendarBakchodia Yes. A smart pointer is another RAII-style object. `std::lock_guard`s constructor takes a reference to the mutex and calls `lock` on it. Then when the `std::lock_guard` is destroyed it's destructor is called and `unlock` is called. Just like how a smart pointer called `delete` in its destructor to make sure the memory is released. – NathanOliver Sep 06 '18 at 17:31
  • Thanks and about about a special case when your *initialized the mutex on the HEAP*? Something like `mutex * m = new mutex();`? How exactly do you use RAII and `std::lock_guard` in this case? – Brijendar Bakchodia Sep 06 '18 at 17:55
  • 4
    @BrijendarBakchodia First, you often don't need to allocate the mutex on the heap. If you do, you should be using a smart pointer rather than raw `new` or `delete`, so you'd write it `auto m = std::make_unique();` Second, notice that [`std::lock_guard`'s constructor](https://en.cppreference.com/w/cpp/thread/lock_guard/lock_guard) takes a `std::mutex&`. So just turn that pointer into a reference by dereferencing it: `std::lock_guard lg(*m)` – Justin Sep 06 '18 at 18:00
  • 1
    @BrijendarBakchodia making sure that a mutex eventually gets unlocked and making sure that a heap-allocated object eventually gets freed are two separate problems. They are separate problems even if the heap-allocated thing happens to be a mutex. They are separate problems even though the preferred solutions, `std::lock_guard` and `std::unique_ptr`, both happen to use the same design pattern (RAII). An important skill for software engineering is learning to recognize and untangle separate problems that appear in the same space, and to keep their solutions separated. – Ohm's Lawman Sep 06 '18 at 19:12
  • Also, there is more powerful `std::unique_lock`. It allow to lock/unlock without destruction. – val - disappointed in SE Sep 06 '18 at 19:27
  • @BrijendarBakchodia Just like a smart pointer makes sure that an object is allocated and deallocated properly, the lock guard makes sure the lock is locked and unlocked properly. The same principle used for a very different purpose. – gnasher729 Sep 06 '18 at 23:44
  • @BrijendarBakchodia, both lock_guard and smart pointer are instances of RAII pattern and generally of managing scoped resources. I wouldn't say either of them wraps anything though. – Jan Hudec Sep 07 '18 at 05:41
30

what is the guarantee with C++?

The relevant guarantee in C++ works a bit differently in comparison to the one you mention in Java. Instead of a finally block, it's relying on the destruction of automatic variables that happens upon the scope's exit, as the stack frame gets unwound. This stack unwinding occurs regardless of how the scope was exited, whether gracefully or due to an exception.

The preferred approach for the scenario concerning such locks is to use RAII, as implemented for example by std::lock_guard. It holds a mutex object passed to its constructor -- inside of which it calls the mutex's lock() method, after which the thread owns the mutex -- and upon stack unwinding at the scope's exit its destructor is called -- inside of which it calls the mutex's unlock() method, thus releasing it.

The code will look like this:

std::mutex m;
{
    std::lock_guard lock(m);
    // Everything here is mutex-protected.
}
// Here you are guaranteed the std::mutex is released.
Geezer
  • 5,600
  • 18
  • 31
0

If an exception is thrown during execution of a piece of code protected by a critical section, that is, codes between "lock()" and "unlock()", it means the associated object that piece of code is operating on is no longer in a valid state. This, may or may not be rolled back by automatic unwinding of the stack triggered by the exception, because some side effect might have taken place before the exception is thrown ( a message has been sent through socket, a machine has been started, for example). At this point, the bigger issue here is not if the mutex will be released (the only guarantee from using lock_guard instead). There might well be some cases that the mutex still being locked is the desirable behavior, and can be explicitly reset after the caller cleaning all mess up.

My point is: this is not a language issue. No language feature can guarantee correct error handling. Don't take lock_guard and RAII as a silver bullet.

John Z. Li
  • 1,893
  • 2
  • 12
  • 19