Friday, May 17, 2013

RIFL vs. RAII

In previous posts, I introduced a resource-management pattern I call RIFL (pronounced "rifle"), or Resource Is Function-Local.  Here I will talk about the standard C++ way of managing resources, which is called RAII, or Resource Allocation Is Initialization.  RAII is usually pronounced "are eh aye aye", but I prefer "rah eeeeeeeeeee".

In RAII, you create a class in which the resource is obtained in the constructor and released in the destructor.  Thus the resource necessarily has exactly the same lifetime as the instance of the class, so the instance is a proxy for the resource.  And, if you allocate that instance on the stack, it is released when the stack frame is destroyed.  Actually, in C++, it is released when control exits the lexical scope, but that's not too far different.

class RawResource {
public:
    static RawResource Obtain();
    void Use();
    void Release();
};

class RaiiResource {
public:
    RaiiResource() : raw_(RawResource.Obtain()) {}
    ~RaiiResource() { raw_.Release(); }
    void Use() { raw_.Use(); }
private:
    RawResource &raw_;
};

This is a bit simpler than a RiflResource.  Using the resource is also easy:

void ExampleRaiiUsage() {
    RaiiResource resource;
    resource.Use();
}

This is very similar to using a RiflResource, but again a bit simpler.  As with RIFL, there is no reference to the raw resource, and no likelihood of releasing the resource twice.

The next example usage is both a strength and a weakness of RAII.  If the RAII object is not allocated on the stack, the lifetime of the resource is still the lifetime of the object, whatever it may be.  The strength is that if you need a more flexible lifetime, you can create one:

RaiiResource *RaiiEscape() {
    RaiiResource *resource = new RaiiResource();
    resource->Use();
    return resource;
}

Before I get into the technical weaknesses of RAII, let me just warn you that confusing it with the RIAA may get you sued.

There's a weakness of RAII which is almost identical to the strength; you can accidentally fail to release the resource:

void RaiiLeak() {
    RaiiResource *resource = new RaiiResource();
    resource->Use();
}

This is a memory leak of the resource object, and therefore a resource leak as well.  The biggest problem with this weakness is that the code looks like a cross between the other two valid usages.  In this simple example, of course, it is easy to see the leak; but it takes a lot of discipline to avoid the leaks in large programs.  Of course, C++ offers tools (such as std::shared_ptr) to help manage memory (and therefore, with RAII, other resources).

If you recall, I only showed fake C++ code (using a "finally" clause) to implement RIFL in C++.  The actual way to implement RIFL in C++ is on top of RAII.

class RiflResource {
public:
    static void WithResource(void useResource(RiflResource &resource)) {
        RaiiResource resource; // Obtain
        RiflResource riflResource(resource); // wrap
            useResource(riflResource); // use
    } // implicit release when RAII resource exits scope
    void Use() { low_.Use(); }
private:
    RiflResource(RaiiResource &raii) : raii_(raii) {}
    RaiiResource &raii_;
};

RAII turns managing any resource into managing memory.  Managing memory is hard in the general case, but easy when the memory is on the stack.  RIFL turns managing any resource into managing the stack, which is always easy, but more limiting.

Holding a lock (not the lock itself) is an unusual resource, because there are no Use() methods, just Obtain (Lock) and Release (Unlock), in the usual implementation.  C++ has an RAII wrapper for locking, which gets used like this:

void AccessWhileRaiiLocked() {
    std::lock_guard<std::mutex> myLock(myMutex);
    x.Use(); // use some class data safe in the knowledge that it is locked.
    y.Use();
} // implicit unlock when lock goes out of scope

This is roughly equivalent to this:

void AccessWhileUnsafelyLowLevelLocked() {
    myMutex.lock();
    x.Use();
    y.Use()
    myMutex.unlock(); // not executed if either Use() throws
}

And to this RIFL example:

void AccessWhileRiflLocked() {
    nonstd::lock_rifl<std::mutex>::HoldingLock(myMutex, []() {
        x.Use();
        y.Use();
    });
}

Ignoring the fact that nothing keeps you from using "x" or "y" outside any of the locking mechanisms, I would argue that RIFL has an advantage over RAII in this case.  It's not that it's simpler; it's slightly more verbose.  But the RAII example looks like it simply has an unused variable.  It's not at all obvious from the code that the x.Use is somehow nested within the lock; or that it's at all related.  Or even, that the lock is needed.

Better would be to use RIFL to actually restrict access to the variables:

class RiflControlled {
public:
    void Sync(void useFunc(usable &x, usable &y)) {
        std::lock_guard<std::mutex> raiiLock(mutex_);
        useFunc(x_, y_);
    } // implicit unlock
private:
    RiflControlled(usable const &x0, usable const &y0) : x_(x0), y_(y0) {}
    usable x_;
    usable y_;
    std::mutex mutex_;
};

void AccessWhileRiflControlled(RiflControlled &rifl) {
    rifl.Sync([](usable &x, usable &y) {
        x.Use();
        y.Use();
    });
}

With RiflControlled, there is simply no way to access "x" or "y" without holding the lock.  You've guaranteed that the locking is correct.  Well, that's a bit overstated, but you really have to deliberately undermine it; it's not going to happen by accident.  Note that in this case, the RIFL function is a (non-static) method on the object, unlike all the previous RIFL examples.  This again suggests that RIFL is a flexible approach to resource management.

With RAII, you can't limit the access to the variables to the scope where the lock is held.  Challenge: prove me wrong.

RAII is necessary in C++ because of the lack of a "finally" clause on a try; there's really no good way around it.  However, it is also a relatively low-level mechanism, which can be abused.  RIFL in C++ can be used as a wrapper around RAII, providing more rigid control over resources (which is good); but less flexible control over resources (which is bad).  But RAII is a step up from the raw resource; one might say that RAII is semi-automatic, while RIFL is fully-automatic.

In the next post, I'll compare RIFL to another resource management pattern similar to RAII, which is IDisposable in C#, and about implementing RIFL in C#.

No comments:

Post a Comment