-
Notifications
You must be signed in to change notification settings - Fork 1
Using Mutexes
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, forlockin this function, is up to line 12. - Line 8; if
someFunction(data)returnsERROR, 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 willlockget destroyed. - Line 12; this is the end of the scope of
lockso 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.
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:
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
blrf:
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
blrIn 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.