Resource management (computing)

In computer programming, resource management refers to techniques for managing resources (components with limited availability). It includes both preventing resource leaks (releasing a resource when a process has finished using it) and dealing with resource contention (when multiple processes wish to access a limited resource). Resource leaks are an issue in sequential computing, while resource contention is an issue in concurrent computing. This article discusses preventing resource leaks; see resource contention for resource management in that sense.

Memory can be treated as a resource, but memory management is usually considered separately, primarily because memory allocation and deallocation is significantly more frequent than acquisition and release of other resources, such as file handles.

Computer programs may manage their own resources, and there are various techniques for resource management, depending on the programming language; Elder, Jackson & Liblit (2008) is a survey article contrasting different approaches. Alternatively, they can be managed by a host – an operating system or virtual machine – or another program. This is known as resource tracking, and consists of cleaning up resource leaks: terminating access to resources that have been acquired but not released after use. This is known as reclaiming resources, and is analogous to garbage collection for memory. On many systems the operating system reclaims resources after the process makes the exit system call.

A key distinction in resource management within a program is between stack management and heap management – whether a resource can be handled like a stack variable (lifetime is restricted to a single stack frame, being acquired and released when execution enters and exits a particular scope), or whether a resource must be handled like a heap variable, such as a resource acquired within a function and then returned from it, which must then be released outside of the acquiring function. Stack management is a common use case, and is significantly easier to handle than heap management.

Statement of problem

Formally, resource management (preventing resource leaks) consists of ensuring that a resource is released if and only if it is successfully acquired. This general problem can be abstracted as "before, body, and after" code, which normally are executed in this order, with the condition that the after code is called if and only if the before code successfully completes, regardless of whether the body code executes successfully or not. This is also known as a code sandwich, and occurs in various other contexts,[1] such as a temporary change of program state, or tracing entry and exit into a subroutine. However, resource management is the most commonly cited application.

In the terminology of control flow analysis, resource release must postdominate successful resource acquisition;[2] failure to ensure this is a bug, and a code path that violates this condition causes a resource leak. Resource leaks are often minor problems, generally not crashing the program, but instead causing some slowdown to the program or the overall system.[1] However, they may cause crashes – either the program itself or other programs – due to resource exhaustion: if the system runs out of resources, acquisition requests fail. This can present a security bug if an attack can cause resource exhaustion. Resource leaks may happen under regular program flow – such as simply forgetting to release a resource – or only in exceptional circumstances, such as when a resource is not released if there is an exception in another part of the program. Resource leaks are very frequently caused by early exit from a subroutine, either by a return statement, or an exception raised either by the subroutine itself, or a deeper subroutine that it calls. While resource release due to return statements can be handled by carefully releasing within the subroutine before the return, exceptions cannot be handled without some additional language facility that guarantees that release code is executed.

More subtly, successful resource acquisition must dominate resource release, as otherwise the code will try to release a resource it has not acquired. The consequences of such an incorrect release range from being silently ignored to crashing the program or unpredictable behavior. These bugs generally manifest rarely, as they require resource allocation to first fail, which is generally an exceptional case. Further, the consequences may not be serious, as the program may already be crashing due to failure to acquire an essential resource. However, these can prevent recovery from the failure, or turn an orderly shutdown into a disorderly shutdown. This condition is generally ensured by first checking that the resource was successfully acquired before releasing it, either by having a boolean variable to record "successfully acquired" – which lacks atomicity if the resource is acquired but the flag variable fails to be updated, or conversely – or by the handle to the resource being a nullable type, where "null" indicates "not successfully acquired", which ensures atomicity.

Basic techniques

The basic approach to resource management is to acquire a resource, do something with it, then release it, yielding code of the form (illustrated with opening a file in Python):

def work_with_file(filename):
    f = open(filename)
    ...
    f.close()

This is correct if the intervening ... code does not contain an early exit (return), the language does not have exceptions, and open is guaranteed to succeed. However, it causes a resource leak if there is a return or exception, and causes an incorrect release of unacquired resource if open can fail.

There are two more fundamental problems: the acquisition-release pair is not adjacent (the release code must be written far from the acquisition code), and resource management is not encapsulated – the programmer must manually ensure that they are always paired. In combination, these mean that acquisition and release must be explicitly paired, but cannot be placed together, thus making it easy for these to not be paired correctly.

The resource leak can be resolved in languages that support a finally construction (like Python) by placing the body in a try clause, and the release in a finally clause:

def work_with_file(filename):
    f = open(filename)
    try:
        ...
    finally:
        f.close()

This ensures correct release even if there is a return within the body or an exception thrown. Further, note that the acquisition occurs before the try clause, ensuring that the finally clause is only executed if the open code succeeds (without throwing an exception), assuming that "no exception" means "success" (as is the case for open in Python). If resource acquisition can fail without throwing an exception, such as by returning a form of null, it must also be checked before release, such as:

def work_with_file(filename):
    f = open(filename)
    try:
        ...
    finally:
        if f:
            f.close()

While this ensures correct resource management, it fails to provide adjacency or encapsulation. In many languages there are mechanisms that provide encapsulation, such as the with statement in Python:

def work_with_file(filename):
    with open(filename) as f:
        ...

The above techniques – unwind protection (finally) and some form of encapsulation – are the most common approach to resource management, found in various forms in C#, Common Lisp, Java, Python, Ruby, and Scheme, among others; they date to the late 1970s in the NIL dialect of Lisp; see history. There are many variations in the implementation, and there are also significantly different approaches.

Approaches

The most common approach to resource management across languages is to use a finally clause, which is called when execution exits a scope – by execution running off the end of the block, returning from within the block, or an exception being throw. This works for stack-managed resources, and is implemented in many languages, including C#, Common Lisp, Java, Python, Ruby, and Scheme. The main problems with this approach is that the release code in the finally clause may be very distant from the acquisition code (it lacks adjacency), and that the acquisition and release code must always be paired by the caller (it lacks encapsulation). These can be remedied either functionally, by using closures/callbacks/coroutines (Common Lisp, Ruby, Scheme), or by using an object handles both the acquisition and release, and adding a language construct to call these methods when control enters and exits a scope (C# using, Java try-with-resources, Python with).

An alternative, more imperative approach, is to write asynchronous code in direct style: acquire a resource, and then in the next line have a deferred release, which is called when the scope is exited – synchronous acquisition followed by asynchronous release. This originated in C++ as the ScopeGuard class, by Andrei Alexandrescu and Petru Marginean in 2000, [3] with improvements by Joshua Lehrer,[4] and has direct language support in D via the scope keyword (ScopeGuardStatement), where it is one approach to exception safety, in addition to RAII.[5] It has also been included in Go, as the defer statement.[6] This approach lacks encapsulation – one must explicitly match acquisition and release – but avoids having to create an object for each resource (code-wise, avoid writing a class for each type of resource).

In C++, the usual approach to resource management is Resource Acquisition Is Initialization (RAII), which ties resource management to the lifetime of an object. This has the advantage that it can also be used for heap-management resources.

In structured programming, stack resource management is done simply by nesting code sufficiently to handle all cases. This requires only a single return at the end of the code, and can result in heavily nested code if many resources must be acquired, which is considered an anti-pattern by some – the Arrow Anti Pattern,[7] due to the triangular shape from the successive nesting.

One other approach, which allows early return but consolidates cleanup in one place, is to have a single exit return of a function, preceded by cleanup code, and to use goto to jump to the cleanup before exit. This is infrequently seen in modern code, but occurs in some uses of C.

Resource management without RAII

The dispose pattern

Many garbage-collected languages, including Java, C# and Python, support various forms of the dispose pattern to simplify cleanup of resources. This results in simpler code and can serve as an alternative to RAII for "shallow" resources.

The compositional properties of RAII, however, differ significantly from scope bound resources in that it allows for full encapsulation of the resources behind an abstraction. With the dispose pattern for resource management, "being a resource" becomes a property that is transitive to composition. That is, any object that is composed using a resource that requires resource management effectively itself becomes a resource that requires resource management.

This limitation is typically encountered whenever developing custom classes. Custom classes in C# and Java have to explicitly implement the dispose method in order to be dispose-compatible for the client code. The dispose method has to contain explicit closing of all child resources belonging to the class. This limitation does not exist in C++ with RAII, where the destructor of custom classes automatically destructs all child resources recursively without requiring any explicit code.

See also

References

  1. 1.0 1.1 Elder, Jackson & Liblit 2008, p. 3.
  2. Elder, Jackson & Liblit 2008, p. 2.
  3. "Generic: Change the Way You Write Exception-Safe Code — Forever", by Andrei Alexandrescu and Petru Marginean, December 01, 2000, Dr. Dobb's
  4. ScopeGuard 2.0, Joshua Lehrer
  5. D: Exception Safety
  6. Defer, Panic, and Recover, Andrew Gerrand, The Go Blog, 4 August 2010
  7. Flattening Arrow Code, Jeff Atwood, 10 Jan 2006

External links