Back to Home

On the RAII idiom and locks

The RAII idiom (Resource Acquisition Is Initialization) originates in C ++ and consists of the fact that some resource is captured in the object's constructor and released in its destructor ....

On the RAII idiom and locks

    The RAII idiom (Resource Acquisition Is Initialization) originates in C ++ and consists in the fact that some resource is captured in the object's constructor and released in its destructor. And since the destructor of local objects is automatically called when the method exits (or just out of scope), regardless of the cause (normal completion of the method or when an exception is thrown), using this idiom is the simplest and most effective way to write followed C ++ code safe from a point view of exceptions.

    When moving to “managed” platforms, such as .NET or Java, this idiom in some way loses its relevance, since the garbage collector deals with the release of memory, namely memory was the most popular resource that had to be taken care of in C ++. However, since the garbage collector deals only with memory and does not contribute to the deterministic release of resources (such as operating system descriptors), the RAII idiom is still used in .NET and in Java, even if few developers know about this intricate name.

    Starting with the first version of the C # language, we had at our disposal a using construct that provided automatic release of resources by calling the Dispose method . Another method of deterministic release of resources was (and remains) the manual use of a try / finally block . Let's look at the following couple of simple examples of using the ReaderWriterLockSlim class , which is designed to more effectively share a common resource between “readers” and “writers”: ManualLockManagerment



    method uses manual locking control, while UsingBasedMethod methodbuilt on the basis of a small shell. A full example of this shell can be found here , but it’s not difficult to guess how it works : the UseReadLock extension method creates some object, the constructor of which takes the lock for reading, and the Dispose method frees it. The question is how much the two fragments are equivalent and what should be preferred? Of course, a “bicycle” with a using block looks more readable, but is this the only difference between them?

    OK. Let's complicate the first example a little. That if in our code there is the possibility of recursive calls, when the method after the lock is captured for reading, it calls the method that locks the read lock again. I don’t think that every reader remembers the behavior of the ReaderWriterLockSlim object from the point of view of re-capture (default reentrancy mode), so I’ll immediately say that unlike the lock construction , ReaderWriterLockSlim objects do not support recursive captures by default:



    In this case, we get mismatched state of object of blocking and exception generation SynchronizationLockException, but how obvious is exactly which point in the code will generate it and what consequences will it lead to? The problem with the above code is that it does not match the behavior of the RAII idiom and the using block : resources should be freed in the finally block only if they were successfully captured before .

    In this case, the following occurs: since the ReaderWriterLockSlim object does not support recursive captures, when you try to call the EnterReadLock method a second time (line 3 of the AnotherMethod method ), a LockRecusionException will be thrown , but since this call is located inside the try block, The unit will be called finally method AnotherMethod followed by challenge ExitReadLock . As a result, in line 4 we will get a free lock, which in itself is not healthy, because we did not capture it; after that, the control is returned to the SomeMethod method and goes to the finally block , where the ExitReadLock method will be called again.

    Deadlock and other troubles



    This is where the fun begins. When I thought about the problem of using the construction using vs manual resource management via try / finally , I assumed that this code would fall with the exception in the line for the first time, and then fall again in line 2 of the SomeMethod method . I reasoned like this: since ReaderWriterLockSlim does not support recursive captures, the original exception will occur in line 3, but since the lock will be released in line 4, then when you try to re-release the lock, the first method will generate another exception that will “mask” the original exception.

    In such an artificial example, with this behavior, finding the real reason will be quite simple, but such behavior on the production server may make you feel good about the blood, because the code may not be so obvious, why does it say that the lock is not captured when it is necessarily captured in the beginning of the method.
    However, in fact, the behavior will be somewhat different, or rather, it will be exactly like this in .NET 4.5, but it will be completely different in previous versions of the platform. Let's take it in order.

    The problem is that on .NET 4.0 (and below) the behavior is as follows: an attempt to call the ExitReadLock method before calling the EnterReadLock method results in an exception, but two calls to ExitReadLock after one callEnterReadLock succeed!



    The most unpleasant thing in this matter is that in the current old versions of the framework, this code will not just succeed, it will lead to a mismatch of the state of the lock object, with the result that we will see the following on the console: ReadCount = 4294967295, ReadLockHeld = False . In fact, in line 3 we reduced the value of the lock counter to 0, and in line 4 we reduced it again; as a result, the counter became -1, and 4294967295 is just a representation of the value “-1” in an unsigned format. But most importantlyany subsequent attempt to capture a lock on a record will stick forever, since the fake will consider that the lock on reading is still captured.

    As it turned out, this is a known bug in the .NET Framework, which is finally fixed in .NET 4.5. After the VS2012 installation, we get quite expected, though not very pleasant behavior: the original exception that occurred when the lock was re-captured was masked by a new exception that occurs when trying to release an un-captured lock!

    Use using or capture resources correctly!



    Now let's remember how the using block is arranged :



    The using construct unfolds in such a way that the resource is captured before the try block , so that when an exception is thrown when it is captured, the release will not be performed. This behavior can lead to unpleasant consequences if the using construct is combined with the object initializer (details in the note Initializers of objects in the using block ), but it fully corresponds to the behavior of constructors / destructors in C ++, in which the destructor was called only if the object was successfully created .

    NOTE
    Here we are confronted with another distinction between destructors in C ++ and a finalizer in C #. Unlike the destructor, the finalizer is called even if the constructor of the created object fell with an exception. This behavior is quite logical, as it simplifies the creation of a resource management code in the C # language, when the finalizer only needs to check the fact of successful resource capture (by checking for null , IntPtr . Zero , etc.).

    Since the use of the structure using the method Dispose invoked only upon successful seizure of the resource, then the following code behaves predictably maximum: code for causing method SomeMethodwill get an exception that occurred on line 2 of the AnotherMethod method when it tries to lock again, and the lock object will be in a completely normal state in any version of the .NET Framework:

    Read Next