10.3 The ACE Mutex Classes

I l @ ve RuBoard

Motivation

Most operating systems provide some form of mutex mechanism that concurrent applications can use to serialize access to shared resources. As with most of the other platform-specific capabilities we've seen in this book, there are subtle variations in syntax and semantics between different OS platforms. Mutexes also have different initialization requirements. The ACE mutex wrapper facades were designed to overcome all these problems in a convenient way.

Class Capabilities

ACE uses the Wrapper Facade pattern to guide the encapsulation of native OS mutex synchronization mechanisms with the ACE_Process_Mutex and ACE_Thread_Mutex classes, which implement nonrecursive mutex semantics portably at system scope and process scope, respectively. [1] They can therefore be used to serialize thread access to critical sections across processes or in one process. The interface for the ACE_Thread_Mutex class is identical to the ACE_LOCK* pseudo-class shown in Figure 10.1 on page 209. The following C++ class fragment illustrates how the ACE_Thread_Mutex can be implemented using Pthreads:

[1] Process-scoped recursive mutexes are implemented in the ACE_Recursive_Thread_Mutex class, which is shown in Section 10.6 on page 229.

 class ACE Thread_Mutex { public:   ACE_Thread_Mutex (const char * = 0, ACE_mutexattr_t *attrs = 0)   {pthread_mutex_init (&lock_, attrs);}   ~ACE_Thread_Mutex () { pthread_mutex_destroy (&lock_); }   int acquire () { return pthread_mutex_lock (&lock_); }   int acquire (ACE_Time_Value *timeout) {     return pthread_mutex_timedlock       (&lock_, timeout == 0 ? 0 : *timeout);   }   int release () { return pthread_mutex_unlock (&lock_); }   //... Other methods omitted... private:   pthread_mutex_t lock_; // Pthreads mutex mechanism. }; 

All calls to the acquire() method of an ACE_Thread_Mutex object will block until the thread that currently owns the lock has left its critical section. To leave a critical section, a thread must invoke the release() method on the mutex object it owns, thereby enabling another thread blocked on the mutex to enter its critical section.

On the Win32 platform, the ACE_Thread_Mutex class is implemented with a CRITICAL_SECTION , which is a lightweight Win32 lock that serializes threads within a single process. In contrast, the ACE_Process_Mutex implementation uses the HANDLE -based mutex on Win32, which can work within or between processes on the same machine:

 class ACE_Process_Mutex { public:   ACE_Process_Mutex (const char *name, ACE_mutexattr_t *)   { lock_ = CreateMutex (0, FALSE, name); }   ~Thread_Mutex () { CloseHandle (lock_); }   int acquire ()   { return WaitForSingleObject (lock_, INFINITE); }   int acquire (ACE_Time_Value *timeout) {     return WaitForSingleObject       (lock_, timeout == 0 ? INFINITE : timeout->msec {});   }   int release () { return ReleaseMutex (lock_); }   // ... Other methods omitted ... private:   HANDLE lock_; // Win32 serialization mechanism. }; 

The ACE_Thread_Mutex class is implemented using the native process-scoped mutex mechanisms on all OS platforms. Depending on the characteristics of the OS platform, however, the ACE_Process_Mutex can be implemented with different mechanisms, for example:

  • A native mutex is used on Win32, as shown above.

  • Some UNIX threading implementations , such as Pthreads and UI threads, require that interprocess mutexes be allocated from shared memory. On these platforms, ACE prefers to use a UNIX System V semaphore if available, since that provides a more reliable mechanism for recovering system resources than the native interprocess mutexes.

  • Some applications require more semaphores than the System V IPC kernel parameters allow, or they require the ability to do a timed acquire() operation. In such cases, ACE can be configured to use a native mutex in shared memory, which puts more responsibility on the application developer to ensure proper cleanup.

Regardless of which underlying mechanism ACE uses, the class interface is identical, which allows application developers to experiment with different configurations without changing their source code.

The ACE_Null_Mutex is another type of mutex supported by ACE. Part of its interface and implementation are shown below:

 class ACE_Null_Mutex { public:   ACE_Null_Mutex (const char * = 0, ACE_mutexattr_t * = 0) {}   int acquire () { return 0; }   int release () { return 0; }   //... }; 

The ACE_Null_Mutex class implements all of its methods as "no-op" inline functions, which can be removed completely by a compiler optimizer. This class provides a zero-overhead implementation of the pseudo ACE_LOCK* interface shared by the other ACE C++ wrapper facades for threading and synchronization.

ACE_Null_Mutex is an example of the Null Object pattern [Woo97]. It can be used in conjunction with the Strategized Locking pattern [SSRB00] so that applications can parameterize the type of synchronization they require without changing application code. This capability is useful for cases where mutual exclusion isn't needed, for example, when an application configuration runs in a single thread and/or never contends with other threads for access to resources.

Example

The following code illustrates how the ACE_Thread_Mutex can be applied to address some of the problems with the UNIX International mutex solution shown on page 135 in Section 6.5.

 #include "ace/Synch.h" typedef u_long COUNTER; static COUNTER request_count; // File scope global variable. // Mutex protecting request _count (constructor initializes). static ACE_Thread_Mutex m; // ...   virtual int handle_data (ACE_SOCK_Stream *) {     while (logging_handler_.log_record () != -1) {       // Try to acquire the lock.       if (m.acquire () == -1) return 0;       ++request_count; // Count # of requests       m.release (); // Release lock.     }     m.acquire ();     int count = request_count;     m.release ();     ACE_DEBUG ((LM_DEBUG, "request_count = %d\n", count)); } 

The use of the ACE_Thread_Mutex C++ wrapper class cleans up the original code somewhat, improves portability, and initializes the underlying mutex object automatically when it's instantiated .

Sidebar 22: Overview of the ACE _ GUARD Macros

ACE defines a set of macros that simplify the use of the ACE_Guard , ACE_Write_Guard , and ACE_Read_Guard classes. These macros test for deadlock and detect when operations on the underlying locks fail. As shown below, they check to make sure that the lock Is actually locked before proceeding:

 # define ACE_GUARD (MUTEX, OBJ, LOCK) \   ACE_Guard< MUTEX > OBJ (LOCK); \     if (OBJ.locked () == 0) return; # define ACE_GUARD_RETURN (MUTEX, OBJ, LOCK, RETURN) \   ACE_Guard< MUTEX > OBJ (LOCK); \     if (OBJ.locked () == 0) return RETURN; # define ACE_WRITE_GUARD (MUTEX, OBJ, LOCK) \   ACE_Write_Guard< MUTEX > OBJ (LOCK); \     if (OBJ.locked () _= 0) return; # define ACE_WRITE_GUARD_RETURN (MUTEX, OBJ, LOCK, RETURN) \   ACE_Write_Guard< MUTEX  > OBJ (LOCK); \     if (OBJ.locked () == 0) return RETURN; #  define ACE_READ_GUARD (MUTEX, OBJ, LOCK) \   ACE_Read_Guard< MUTEX > OBJ (LOCK); \     if (OBJ.locked () == 0) return; # define ACE_READ_GUARD_RETURN (MUTEX, OBJ, LOCK, RETURN) \   ACE_Read_Guard< MUTEX > OBJ (LOCK); \     if (OBJ.locked () == 0) return RETURN; 

The ACE concurrency wrapper facades don't solve all the problems identified in Section 6.5, however. For example, programmers must still release the mutex manually, which can yield bugs due to programmer negligence or due to the occurrence of C++ exceptions. Moreover, it is tedious and error prone for programmers to check explicitly whether the mutex was actually acquired . For instance, you'll notice that we neglected to check if the ACE_Thread_Mutex::acquire() method succeeded before assigning request_count to count . We can simplify this example by applying the Scoped Locking idiom [SSRB00] via the ACE_GUARD* macros described in Sidebar 22.

 virtual int handle_data (ACE_SOCK_Stream *) {   while (logging_handler_.log_record () !_ -1) {     // Acquire lock in constructor.     ACE_GUARD_RETURN (ACE_Thread_Mutex, guard, m, -1);     ++request_count; // Count # of requests     // Release lock in destructor.   }   int count;   {     ACE_GUARD_RETURN (ACE_Thread_Mutex, guard, m, -1);     count = request_count;   }   ACE DEBUG ((LM DEBUG, "request_count = %d\n", count)); } 

By making a slight change to the code, we've now ensured that the ACE_Thread_Mutex is acquired and released automatically. We've also ensured that we'll handle mutex failures properly.

Although we've addressed many issues outlined in Section 6.5, the following two problems still remain :

  • The solution is still obtrusive, that is, we must insert the ACE_GUARD macros manually within a new statement scope delimited by curly braces. Section 10.4 shows how to eliminate this obtrusiveness.

  • The solution is also error prone, that is, the static ACE_Thread_Mutex global object m must be initialized properly before the handle_data() method is called. It's surprisingly hard to ensure this since C++ doesn't guarantee the order of initialization of global and static objects in different files. Sidebar 23 describes how ACE addresses this problem via its ACE_Object_Manager class.

The ACE_Object_Manager class provides a global recursive mutex that we could use in our logging server example:

 //... while (logging_handler_.log_record () != -1) {   // Acquire lock in constructor.   ACE_GUARD_RETURN (ACE_Recursive_Thread_Mutex, guard,                     ACE_Static_Object_Lock::instance (),                     -1);   ++request_count;   // Count # of requests   // Release lock in destructor. } // ... 

Sidebar 23: The ACE_Object_Manager Class

To ensure consistent initialization order of global and static objects, ACE provides the ACE_Object_Manager class, which implements the Object Lifetime Manager pattern [LGS00]. This pattern governs the entire lifetime of objects, from creating them prior to their first use to ensuring they are destroyed properly at program termination. The ACE_Object_Manager class provides the following capabilities that help replace static object creation/destruction with automatic dynamic object preallocation/deallocation:

  • It manages object cleanup (typically singletons) at program termination. In addition to managing the cleanup of the ACE library, it provides an interface for applications to register objects to be cleaned up .

  • It shuts down ACE library services at program termination so that they can reclaim their storage. This works by creating a static instance whose destructor gets called along with those of all other static objects. Hooks are provided for application code to register objects and arrays for cleanup when the program terminates. The order of these cleanup calls is in the reverse (LIFO) order of their registration; that is, the last object/array to register gets cleaned up first.

  • It preinitializes certain objects, such as ACE_Static_Object_Lock , so that they will be created before the main() function runs.

Since the ACE_Static_Object_Lock instance is controlled by the ACE_Object_Manager it's guaranteed to be initialized when a program starts and will be destroyed properly when the program exits.

I l @ ve RuBoard


C++ Network Programming
C++ Network Programming, Volume I: Mastering Complexity with ACE and Patterns
ISBN: 0201604647
EAN: 2147483647
Year: 2001
Pages: 101

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net