Skip to content

Using Mutexes

Rich Neswold edited this page Mar 30, 2022 · 1 revision

Mutexes are useful and necessary when data needs to be shared among several tasks. The interface provided with VxWorks, however, is designed for the C language and requires the programmer to handle all the book keeping associated with these primitives. The support for mutex behavior in the vwpp library follows the RAII (resource acquisition is initialization) rule. What this means is that the use of a resource is tied to the lifetime of an object. In this case, a lock object defines the length of time a mutex is held by the length of its lifetime.

Take the following example:

 1 SEM_ID mtx;
 2 int data;      // Shared! Must own 'mtx' before accessing!
 3
 4 void f()
 5 {
 6     semTake(mtx, WAIT_FOREVER);
 7     if (ERROR == someFunction(data)) {
 8         semGive(mtx);
 9         return;
10     }
11     data = anotherFunction();
12     semGive(mtx);
13
14     someThirdFunction();
15 }

In order to access the value in data, the mutex mtx needs to be owned. It's up to the programmer to relinquish ownership at the appropriate time -- even in the case of errors. In the above example, semGive() needs to be called if someFunction() returns an error (line 8.) If the programmer wanted to call C++ functions, then this section would also be wrapped in a try-catch block to make sure the mutex was freed when an exception was thrown.

How can we improve this situation?

The above example could be rewritten as:

 1 vwpp::Mutex sem;
 2 int data;
 3
 4 void f()
 5 {
 6     vwpp::Mutex::Lock<sem> lock;
 7
 8     if (ERROR == someFunction(data))
 9         return;
10     data = anotherFunction();
11     someThirdFunction();
12 }

Let's analyze the lines in this example:

  • Line 1; creates a global instance of a Mutex object. In VxWorks, global objects are constructed immediately after the module is loaded so this mutex is available before any code in this module will be called.
  • Line 6; creates a lock object (the template argument, sem, specifies which global mutex this lock owns.) The mutex associated with a lock object is acquired in the lock's constructor and released in its destructor, so it owns the mutex during its lifetime. An object exists only in its scope which, for lock in this function, is up to line 12.
  • Line 8; if someFunction(data) returns ERROR, we return early and, since returning from a function leaves the current scope, the lock is destroyed which releases the mutex. If it throws an exception, the compiler will make sure the lock object is destroyed before we leave the function (also releasing the mutex in the process.)
  • Line10; at this point, the lock object still exists, so the mutex is held. Again, if anotherFunction() throws an exception, the lock gets destroyed.
  • Line 11; the lock still exists here, and only if someThirdFunction() throws an exception will lock get destroyed.
  • Line 12; this is the end of the scope of lock so the mutex gets released properly.

Now, given such a simple example, it doesn't appear that we've saved many lines of code. But the benefits, though subtle, are worthwhile. In this example, the life of the lock object dictates when the mutex is acquired and released. This includes early function returns (like when someFunction() returns an error), and when a function throws an exception. In this version of the code, the compiler is responsible for destroying the lock object which guarantees that when f() returns, the mutex is released.

It needs mentioning the previous example isn't an exact replacement for the first. For one thing, the first example needs a try-catch block to handle exceptions correctly (which the second example already does.) The other difference is that the first example gives up the mutex before calling someThirdFunction(). We can accomplish the same thing by using extra curly braces to tighten the scope of the lock. The following example has the correct scope of the lock object:

vwpp::Mutex sem;
int data;

void f()
{
    {
        vwpp::Mutex::Lock<sem> lock;

        if (ERROR == someFunction(data))
            return;
        data = anotherFunction();
    }
    someThirdFunction();
}

Now lock is destroyed (releasing the mutex) before someThirdFunction() is called.

Yeah, But What About Performance?

First, write for correctness. Then check whether its performance is acceptable.

This library greatly helps you write correct code; it lets you declare how mutexes should be used and their scope of use. The compiler enforces your constraints and handles early function returns and exceptions. Fortunately for us, when templates are expanded, C++ compilers can do aggressive inlining which makes the code produced from this library quite efficient, too.

Let's compare the generated code from the VxWorks 6.7 PowerPC C++ and C compilers. Using the final example as input to the C++ compiler and the first example to the C compiler yields this comparison:

Generated from C++

f:
        mflr 0
        stwu 1,-32(1)

; vwpp::Mutex::Lock<sem> lock;

        lis 9,_ZN4vwpp13SemaphoreBase7acquireEi@ha
        li 4,-1
        stw 29,20(1)
        la 9,_ZN4vwpp13SemaphoreBase7acquireEi@l(9)
        stw 0,36(1)
        lis 29,sem@ha
        stw 31,28(1)
        la 3,sem@l(29)
        mtctr 9
        ctrl

; if (ERROR == someFunction(data))

        lis 9,someFunction@ha
        lis 31,data@ha
        la 9,someFunction@l(9)
        lwz 3,data@l(31)
        mtctr 9
        bctrl
        cmpwi 7,3,-1

;    return;

        beq- 7,.L13

; data = anotherFunction();

        lis 9,anotherFunction@ha
        la 9,anotherFunction@l(9)
        mtctr 9
        bctrl
        stw 3,data@l(31)

; ** unlock mutex **

        lis 9,semGive@ha
        la 11,sem@l(29)
        la 9,semGive@l(9)
        mtctr 9

; someThirdFunction();

        lwz 3,4(11)
        bctrl
        lis 9,someThirdFunction@ha
        la 9,someThirdFunction@l(9)
        mtctr 9
        bctrl
        lwz 0,36(1)
        lwz 29,20(1)
        lwz 31,28(1)
        mtlr 0
        addi 1,1,32
        bar

.L13:
; ** unlock mutex **

        lis 11,semGive@ha
        la 9,sem@l(29)
        la 11,semGive@l(11)
        lwz 3,4(9)
        mtctr 11
        bctrl
        lwz 0,36(1)
        lwz 29,20(1)
        lwz 31,28(1)
        mtlr 0
        addi 1,1,32
        blr

Generated from C

f:
        mflr 0
        stwu 1,-16(1)

; semTake(mtx, WAIT_FOREVER);

        lis 9,semTake@ha
        li 4,-1
        stw 30,8(1)
        la 9,semTake@l(9)
        stw 0,20(1)
        mtctr 9
        stw 31,12(1)
        lis 31,sem@ha
        lwz 3,sem@l(31)
        bctrl

; if (ERROR == someFunction(data)) {

        lis 9,someFunction@ha
        lis 30,data@ha
        la 9,someFunction@l(9)
        lwz 3,data@l(30)
        mtctr 9
        bctrl
        cmpwi 7,3,-1

;     return;

        beq- 7,.L7

; data = anotherFunction();

        lis 9,anotherFunction@ha
        la 9,anotherFunction@l(9)
        mtctr 9
        bctrl
        stw 3,data@l(30)

; semGive(mtx);

        lis 9,semGive@ha
        la 9,semGive@l(9)
        mtctr 9
        lwz 3,sem@l(31)
        bctrl

; someThirdFunction();

        lis 9,someThirdFunction@ha
        la 9,someThirdFunction@l(9)
        mtctr 9
        bctrl
        lwz 0,20(1)
        lwz 30,8(1)
        lwz 31,12(1)
        mtlr 0
        addi 1,1,16
        blr

.L7:
; semGive(mtx);

        lis 9,semGive@ha
        lwz 3,sem@l(31)
        la 9,semGive@l(9)
        mtctr 9
        bctrl
        lwz 0,20(1)
        lwz 30,8(1)
        lwz 31,12(1)
        mtlr 0
        addi 1,1,16
        blr

In this comparison, the C output resulted in, maybe, two fewer assembly instructions. It's clear that this style of programming results in the same performance as hand-written code, yet provides compile time checks which helps write bug-free software.

Clone this wiki locally