0

When an exception is thrown inside a lock, should we release the lock?

Of course, if this exception is expected and we can recovery from it, we should perform proper actions to make lock protected data in consistent and release the lock. If the exception is unexpected, eg,., OutOfMemoryExceptions, LinkErrorExceptions, NullPointerExceptions, which might be thrown everywhere and we want to terminate the program to stop further data corruption. In this case, should we release the lock before terminate the program?

I prefer not release the lock. And there is a good post here:

Does a locked object stay locked if an exception occurs inside it?

I note that no one has mentioned in their answers to this old question that releasing a lock upon an exception is an incredibly dangerous thing to do. Yes, lock statements in C# have "finally" semantics; when control exits the lock normally or abnormally, the lock is released. You're all talking about this like it is a good thing, but it is a bad thing! The right thing to do if you have a locked region that throws an unhandled exception is to terminate the diseased process immediately before it destroys more user data, not free the lock and keep on going.

Look at it this way: suppose you have a bathroom with a lock on the door and a line of people waiting outside. A bomb in the bathroom goes off, killing the person in there. Your question is "in that situation will the lock be automatically unlocked so the next person can get into the bathroom?" Yes, it will. That is not a good thing. A bomb just went off in there and killed someone! The plumbing is probably destroyed, the house is no longer structurally sound, and there might be another bomb in there. The right thing to do is get everyone out as quickly as possible and demolish the entire house.

I mean, think it through: if you locked a region of code in order to read from a data structure without it being mutated on another thread, and something in that data structure threw an exception, odds are good that it is because the data structure is corrupt. User data is now messed up; you don't want to try to save user data at this point because you are then saving corrupt data. Just terminate the process.

If you locked a region of code in order to perform a mutation without another thread reading the state at the same time, and the mutation throws, then if the data was not corrupt before, it sure is now. Which is exactly the scenario that the lock is supposed to protect against. Now code that is waiting to read that state will immediately be given access to corrupt state, and probably itself crash. Again, the right thing to do is to terminate the process.

No matter how you slice it, an exception inside a lock is bad news. The right question to ask is not "will my lock be cleaned up in the event of an exception?" The right question to ask is "how do I ensure that there is never an exception inside a lock? And if there is, then how do I structure my program so that mutations are rolled back to previous good states?"

C++ RRID pattern in this case will not release the lock:

...
{
   // the destructor will release the lock when
   // we go out of this block
   AutoReleseLock lock(&mLock);

   ...
   // Exception thrown here.
   foo();
   ...
}
...

If the thrown exception is caught somewhere, the lock will be released. Otherwise, std::terminate() will be called directly without release the lock.

But in Java:

synchronized (monitor) {
    ...
    // Exception thrown here.
    foo();
    ...
}

or

 lock();
 try {
    ...
    // Exception thrown here.
    foo();
    ...
 } finally {
     unlock();
 }

The lock will be released no matter the exception is caught or not, is expected or not before we get to the unhandled exception handler. And other threads may read corrupted inconsistent data and perform unpredictable actions, for example, remove wrong files.

So, how can I get the similar behaviour as in C++ to terminate the program without release the lock on unexpected exceptions without a lot of ugly code?

update:

Be exception safe or kill itself to prevent data corruption are both good ideas. But permit unpredictable behaviour is not a good idea, even if it's rare.

But, be exception safe as holding locks may require a lot of tedious logic. For example, as OutOfMemoryException in Java.

synchronized {
   nameSet.add(newPeople.name);
   ageSet.add(newPeople.age);
   jobSet.add(newPeople.job);
}

will become:

synchronized {
   nameSet.add(newPeople.name);

   try {
      ageSet.add(newPeople.age);
   } catch (OutOfMemoryException e) {
      nameSet.remove(newPeople.name);
      throw e;
   }

   try {
      jobSet.add(newPeople.job);
   } catch (OutOfMemoryException e) {
      nameSet.remove(newPeople.name);
      ageSet.remove(newPeople.age);
      throw e;
   }
}

And, still bugs here. We should determine whether the name/age/job is already here before we add it to the set.

Most systems expect OutOfMemoryException to be a program kill action rather than a general exception or we end up catching them almost every where. In this case, this kill action are not as clean as in C++ and will release locks to leave inconsistent data to be read by other threads. And other threads may persist this data to disk.

Leo
  • 71
  • 8
  • @VTT Java has no `noexcept` or similar – Leo Nov 20 '17 at 09:24
  • 3
    The analogy of a bomb in a bathroom is ridiculous. In all my development career I've never even heard of this being a realistic problem. If you have proper design, a simple exception inside a lock won't result in any corruption. This is the programming equivalent of being afraid of the boogeyman. – Kayaman Nov 20 '17 at 09:41
  • @Kayaman This is not about rare or often, is about right or wrong. It not a good reason to not handle an error because it not often happens. – Leo Nov 20 '17 at 10:22
  • Talking about right and wrong in relation to programming is like talking about bombs in the bathroom. Besides, I wasn't talking about not handling errors. I don't know how you managed to get that idea. – Kayaman Nov 20 '17 at 10:25
  • You cannot release the lock unless you are confident that whatever the lock protects is in a consistent state. If the code that manipulates what the lock protects didn't handle the exception, there is no way you can be confident that what the lock protects is in a consistent state. – David Schwartz Nov 20 '17 at 11:08
  • @DavidSchwartz I agree. But with synchronized blocks in Java, how can I conditionally release a lock? – Leo Nov 20 '17 at 11:29
  • @Leo You don't have to. If you're in the `synchronized` block, then you know what you've done to the shared state that the lock protects. Do not exit that block until/unless you can return the shared state to a consistent state. – David Schwartz Nov 20 '17 at 11:35
  • @DavidSchwartz but that might bring in a lot of tedious login, as state above in the update section. – Leo Nov 20 '17 at 11:40
  • 1
    There is no other magic solution. You need to ensure a consistent state, that's your responsibility as a programmer. – rustyx Nov 20 '17 at 11:50
  • Right. You can't (in general) allow the lock to be released if the shared state is inconsistent. That would cause unpredictable results when the next thread gets the lock. This is why you don't want to do dangerous things while holding a lock. – David Schwartz Nov 20 '17 at 11:53

2 Answers2

1

If an exception occurs "inside a lock" there is no general answer about whether the lock should be released or not. More often than not, however, it makes sense to release the lock.

It depends on requirements of the application. Requirements might include transaction rollback semantics (if an exception is thrown during a transaction, program state and associated data storage are rolled back to a point where there is no evidence the operation was even attempted) or other exception safety guarantees (e.g. if an exception is thrown, all objects are in a state so they may be safely destroyed). In such cases, it makes perfect sense for a lock to be released - either during the process of stack unwinding, or in an exception handler.

Conversely, the application requirement may be that - if an exception is thrown - all affected functionality is disabled. In that case, it may make sense for the lock to be held - at least, until error recovery occurs. Practically, such circumstances are rare. And, if the application itself is able to do error recovery, it would make sense for the application to eventually release the lock. If not, then that would mean there are other system components that are required to work around the error condition without correcting it.

I agree with Kayaman's comment that the "lock in the bathroom" example is ridiculous. It presumes that the only allowed action by people outside, when a door is unlocked after a bomb explodes, is going inside like lemmings. That's not true in real life, and it is not true in system design.

Generally speaking, if I was given a requirement for a lock to remain locked after an error condition, I would take that as a cue that requirements development is incomplete. So I would seek to develop requirements for either preventing that error condition entirely, or appropriately responding to it (corrective actions, initiating system refresh, etc etc).

Peter
  • 35,646
  • 4
  • 32
  • 74
  • "if I was given a requirement for a lock to remain locked after an error condition, I would take that as a cue that requirements development is incomplete." This means, every exception is expected and designed to be handled, that might not be true in real life Java. Most systems expect OutOfMemoryException to be a program kill action rather than a general exception or we end up catching them almost every where. In this case, this kill action are not as clean as in C++ and will release locks to leave inconsistent data to be read by other threads. And other threads may persist this data to disk. – Leo Nov 20 '17 at 10:46
  • "lock in the bathroom" example may be ridiculous. Be exception safe or kill itself to prevent data corruption are both good ideas. But permit unpredictable behaviour is not a good idea, even if it's rare. – Leo Nov 20 '17 at 10:48
  • I updated my original post to response to your idea. – Leo Nov 20 '17 at 11:07
  • I don't see how this can work reliably. If the code that generated the exception guaranteed that whatever the lock protects was in a consistent state, wouldn't it also handle the exception? If it didn't handle the exception, how can you know that whatever the lock protects is in a consistent state? It's a *very* common pattern to acquire a lock and then manipulate what the lock protects through an inconsistent state and only return it to a sane state just prior to releasing the lock. (You could validate the state then conditionally unlock, but that requires code that understands the state.) – David Schwartz Nov 20 '17 at 11:11
  • @Leo - you're misinterpreting my meaning. I did not suggest that every exception is expected and handled, or that you need to resort to fine-grained exception handling. In C++, consider the difference between `{AutoReleaseLock lock(&mx); delete px; px = new X;}` and `{AutoReleaseLock lock(&mx); X *npx = new X; delete px; px = npx;}`. If `new X` throws, the first leaves `px` in an invalid state, the second doesn't. If no exception is thrown, the two have the same net effect (`px` reset to point at a new object, and old object released). Should the lock be released if an exception is thrown? – Peter Nov 20 '17 at 11:47
  • @Peter The point is, the lock can only be safely released if the person who wrote the code where the exception was generated made sure it was safe to release the lock if an exception was generated there. In general, it can't be safely released. – David Schwartz Nov 20 '17 at 11:55
  • @Peter Yep, I got your idea to be exception safe. But most time it is not as easy as your new/del example, but as hard as I said in the -update- section. How would you deal with the case in my -update- section? BTW, c++'s crash-without-unlock behaviour is just as expected as I said in my post. – Leo Nov 20 '17 at 11:59
  • Your "update" section is not that hard. You're doing operations on three disconnected sets. First, ensure each of the sets has exception-safe copying and `add` operations and a non-throwing `swap`. Then keep the three sets in (or copy them to) a single structure, and implement that structure with exception-safe copying and non-throwing `swap`. In the synchoronised code, create a copy of the structure, and then do the three add operations on members of the copy. If any of those steps throws discard the copy. Then swap the copy back to the original. Done. – Peter Nov 20 '17 at 13:01
  • @Peter won"t this approach significantly slow down the system? – Leo Nov 20 '17 at 14:22
  • Significantly compared with what? You asserted it was not possible to achieve exception safety without "tedious logic". I gave an example otherwise, and you respond by introducing a spurious performance concern, despite no previous mention of any such requirement. I'll leave it there. End of discussion. – Peter Nov 21 '17 at 06:53
1

[in these Java examples] The lock will be released no matter the exception is caught or not,... And other threads may read corrupted inconsistent data and perform unpredictable actions.

It is the author's responsibility to ensure that the code performs as expected under all circumstances. If the program is intended to continue functioning after an exception is thrown, then it is the programmer's job to make that happen.

In both of your Java examples, the lock is released, but the code shown does not perform any other cleanup when an exception is thrown. But, the whole point of a mutex is to prevent other threads from seeing shared data in a temporary, inconsistent state. So, if it's possible for the exception to be thrown while shared data is in such a state, then the programmer will have to write a handler that puts things right again before possibly re-throwing the exception or leaving the mutex by other means.

Solomon Slow
  • 25,130
  • 5
  • 37
  • 57
  • If the program intends to die on unexpected exceptions, I think to die without unlocking will save a lot of tedious logic. The question is, is there a concise way to do so in Java? – Leo Nov 20 '17 at 15:09
  • `System.exit(1)` ? – Solomon Slow Nov 20 '17 at 15:29
  • Not that easy, maybe. Firstly, where should I put this System.exit(1)? Secondly, how do I put a System.exit(1) inside a library that uses locks? – Leo Nov 20 '17 at 15:34
  • You can call `System.exit()` from anywhere within a Java program, and it terminates the process then and there. But, calling it from within a _library_ that you expect other people to use would be uber unfriendly. If there's no way your library can reset itself to some sane state and throw a "try again" exception, then I would recommend putting it into some "broken" state where every call to the library immediately throws an "I'm dead" exception. Let the client do the actual exiting from the process. – Solomon Slow Nov 20 '17 at 15:45