Detailed explanation of raid mechanism in C++

  • 2020-04-02 02:46:57
  • OfStack

preface

The design of the GC class is clever when it comes to writing (link: #), when it comes to instance destruction, and the clever design is based on the fact that an object's destructor is automatically called at the end of its life cycle. So I'm going to talk about RAII in its entirety, around RAII.

What is RAII?

RAII, short for Resource Acquisition Is Initialization, Is an idiom in the C++ language for managing resources and avoiding leaks. It takes advantage of the principle that objects constructed in C++ will eventually be destroyed. The RAII approach is to take an object, acquire the corresponding resource at construction time, control access to the resource during the object's lifetime to keep it valid, and finally release the resource acquired at construction time at object destructor time.

Why raid?

It says that RAII is a way to manage resources and avoid resource leaks. So, after all this time, after all this programming, it's often said resources, how do you define resources? In a computer system, resources are a limited number of elements that play a role in the normal operation of the system. Network sockets, mutexes, file handles, and memory are among the system resources. Because the resources of the system are limited, just like the oil and iron ore in nature, they are not inexhaustible. Therefore, when we use the system resources in programming, we must follow a step:

1. Apply for resources;
2. Use resources;
Release resources.

The first step and the second step are indispensable, because the resources must be applied to be able to use, after the completion of use, must be released, if not released, will cause resource leakage.

A simple example:


#include <iostream>
 
using namespace std;
 
int main()
 
{
    int *testArray = new int [10];
    // Here, you can use the array
    delete [] testArray;
    testArray = NULL ;
    return 0;
}

We use the memory resources created by new, and if we don't release them, they leak. So, when programming, the new and delete operations always match the operations. If you always request resources without releasing them, you will end up in a situation where all the resources are occupied and no resources are available. But in actual programming, we tend to forget all sorts of things about the release, and even the most experienced programmers, in thousands of lines of code, in tens of thousands of lines of code, make this kind of low-level error.

Here's another example:


#include <iostream>
using namespace std;
 
bool OperationA();
bool OperationB();
 
int main()
{
    int *testArray = new int [10];
 
    // Here, you can use the array
    if (!OperationA())
    {
        // If the operation A failed, we should delete the memory
        delete [] testArray;
        testArray = NULL ;
        return 0;
    }
 
    if (!OperationB())
    {
        // If the operation A failed, we should delete the memory
        delete [] testArray;
        testArray = NULL ;
        return 0;
    }
 
    // All the operation succeed, delete the memory
    delete [] testArray;
    testArray = NULL ;
    return 0;
}
 
bool OperationA()
 
{
    // Do some operation, if the operate succeed, then return true, else return false
    return false ;
}
 
bool OperationB()
 
{
    // Do some operation, if the operate succeed, then return true, else return false
    return true ;
}

The model of the above example is often used in practice. We cannot expect every operation to return successfully, so we need to make a judgment on each operation. In the above example, when the operation fails, then, the memory is released and the program is returned. The above code is extremely bloated and inefficient, and what's more, the program is significantly less understandable and maintainable. As the number of operations increases, the code handling the release of resources will become more and more messy. What if an operation has an exception and the statement that frees the resource is not invoked? This is where the RAII mechanism comes in handy.

How do you use RAII?

When we use a local variable inside a function, when the local variable is out of scope, the variable is not destroyed; When the variable is a class object, the destructor of that class is automatically called, and this happens automatically, without the programmer showing up to call it. That's great. That's how RAII works. Because the resources of the system do not have the function of automatic release, and C++ classes have the function of automatic call destructor. If the resource is encapsulated with a class, the resource operations are encapsulated inside the class and the resource is released in the destructor. When a defined local variable's life is over, its destructor is called automatically, so that the operation to free the resource is not called as shown by the programmer. Now, let's use the RAII mechanism to complete the above example. The code is as follows:


#include <iostream>
using namespace std;
 
class ArrayOperation
{
public :
    ArrayOperation()
    {
        m_Array = new int [10];
    }
 
    void InitArray()
    {
        for (int i = 0; i < 10; ++i)
        {
            *(m_Array + i) = i;
        }
    }
 
    void ShowArray()
    {
        for (int i = 0; i <10; ++i)
        {
            cout<<m_Array[i]<<endl;
        }
    }
 
    ~ArrayOperation()
    {
        cout<< "~ArrayOperation is called" <<endl;
        if (m_Array != NULL )
        {
            delete[] m_Array;  //Thank you very much for the very sharp review of yikeda, details can be included in the comments of yikeda in this article 2014.04.13 < br / >             m_Array = NULL ;
        }
    }
 
private :
    int *m_Array;
};
 
bool OperationA();
bool OperationB();
 
int main()
{
    ArrayOperation arrayOp;
    arrayOp.InitArray();
    arrayOp.ShowArray();
    return 0;
}

The above example doesn't have much practical significance, just to illustrate the mechanics of RAII. Here's a practical example:


/*
** FileName     : RAII
** Author       : Jelly Young
** Date         : 2013/11/24
** Description  : More information, please go to //www.jb51.net
*/
 
#include <iostream>
#include <windows.h>
#include <process.h>
 
using namespace std;
 
CRITICAL_SECTION cs;
int gGlobal = 0;
 
class MyLock
{
public:
    MyLock()
    {
        EnterCriticalSection(&cs);
    }
 
    ~MyLock()
    {
        LeaveCriticalSection(&cs);
    }
 
private:
    MyLock( const MyLock &);
    MyLock operator =(const MyLock &);
};
 
void DoComplex(MyLock &lock ) //Thank you very much for your insightful review 2014.04.13 < br / > {
}
 
unsigned int __stdcall ThreadFun(PVOID pv)
{
    MyLock lock;
    int *para = (int *) pv;
 
    // I need the lock to do some complex thing
    DoComplex(lock);
 
    for (int i = 0; i < 10; ++i)
    {
        ++gGlobal;
        cout<< "Thread " <<*para<<endl;
        cout<<gGlobal<<endl;
    }
    return 0;
}
 
int main()
{
    InitializeCriticalSection(&cs);
 
    int thread1, thread2;
    thread1 = 1;
    thread2 = 2;
 
    HANDLE handle[2];
    handle[0] = ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void *)&thread1, 0, NULL );
    handle[1] = ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void *)&thread2, 0, NULL );
    WaitForMultipleObjects(2, handle, TRUE , INFINITE );
    return 0;
}

This example can be said to be a model of a real project, when multiple processes access critical variables, in order to avoid errors, the critical variables need to be locked; The above example is the locking implemented in the critical region of Windows used. However, when using CRITICAL_SECTION, EnterCriticalSection and LeaveCriticalSection must be used in pairs, and many times, LeaveCriticalSection is often forgotten to be called, and a deadlock occurs. When I encapsulated the access to CRITICAL_SECTION in the MyLock class, I only needed to define a MyLock variable after that, instead of manually displaying the call to the LeaveCriticalSection function.

Both of the above examples are applications of the RAII mechanism, and once you understand the above examples, you should be able to understand the use of the RAII mechanism.

Use RAII traps

There are some issues that require special attention when using RAII. Let me take my time.

Here's an example:


#include <iostream>
#include <windows.h>
#include <process.h>
 
using namespace std;
 
CRITICAL_SECTION cs;
int gGlobal = 0;
 
class MyLock
{
public:
    MyLock()
    {
        EnterCriticalSection(&cs);
    }
 
    ~MyLock()
    {
        LeaveCriticalSection(&cs);
    }
 
private:
    //MyLock(const MyLock &);
    MyLock operator =(const MyLock &);
};
 
void DoComplex(MyLock lock)
{
}
 
unsigned int __stdcall ThreadFun(PVOID pv) 
{
    MyLock lock;
    int *para = (int *) pv;
 
    // I need the lock to do some complex thing
    DoComplex(lock);
 
    for (int i = 0; i < 10; ++i)
    {
        ++gGlobal;
        cout<< "Thread " <<*para<<endl;
        cout<<gGlobal<<endl;
    }
    return 0;
}
 
int main()
{
    InitializeCriticalSection(&cs);
 
    int thread1, thread2;
    thread1 = 1;
    thread2 = 2;
 
    HANDLE handle[2];
    handle[0] = ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void*)&thread1, 0, NULL );
    handle[1] = ( HANDLE )_beginthreadex(NULL , 0, ThreadFun, ( void*)&thread2, 0, NULL );
    WaitForMultipleObjects(2, handle, TRUE , INFINITE );
    return 0;
}

This example is a modification of the previous one. Added a DoComplex function that is called in a thread. This function is very ordinary, but the argument of this function is the class we encapsulated. If you run the code, you will find that the access to the gGlobal global variable is completely messed up by adding the function. Have you ever wondered why? There are a lot of RAII articles on the web that just talk about this, but don't say why, but here I'm going to do a little bit of analysis here.

Because of the passed value used by the parameters of the DoComplex function, a copy of the value occurs, the copy constructor of the class is called, and a temporary object is generated. Since MyLock does not implement the copy constructor, it is the default copy constructor used, and then the temporary variable is used in DoComplex. When the call is completed, the destructor of this temporary variable will be called. As LeaveCriticalSection is called in the destructor, it leaves CRITICAL_SECTION early, which causes access conflict to the gGlobal variable. If the following code is added to the MyLock class, the program will run correctly again:


MyLock( const MyLock & temp )
{
    EnterCriticalSection(&cs);
}

This is because CRITICAL_SECTION allows EnterCriticalSection to be repeated, but LeaveCriticalSection must match EnterCriticalSection to avoid deadlock.

In order to avoid falling into this trap, and considering that what is encapsulated is a resource, since resources do not have copy semantics in many cases, in the actual implementation process, the MyLock class should be as follows:


class MyLock
{
public:
    MyLock()
    {
        EnterCriticalSection(&cs);
    }
 
    ~MyLock()
    {
        LeaveCriticalSection(&cs);
    }
 
private:
    MyLock(const MyLock &);
    MyLock operator =(const MyLock &);
};

This prevents the underlying resource from replicating and keeps everything under control. If you want to know about the invocation of copy constructors and assignment operators, take a good look at the book exploring the C++ object model in depth.

conclusion

Having said that, the essence of RAII is to use objects to represent resources, transform the task of managing resources into the task of managing objects, and correspond the acquisition and release of resources with the construction and destruction of objects, so as to ensure that the resources are always valid during the lifetime of the objects, and the resources must be released when the objects are destroyed. In short, if you have an object, you have a resource. If the object is there, the resource is there. Therefore, RAII is a powerful tool for resource management, and C++ programmers rely on RAII to write code that is not only clean and elegant, but also exceptionally safe. In future programming practice, you can use the RAII mechanism to make your code more beautiful.


Related articles: