Lock Strategy
The above mutex use illustrated in the previous sections is fairly crude; you might say that the granularity isn't very fine. In other words, the locking strategy is all-or-nothing. It's possible to have a more forgiving approach in the form of try-and-fail, as shown in Listing 8. In this approach to locking, we try to acquire a lock; if the lock fails, we can try again later.
Listing 8A more granular approach to locking.
void vectorWork() { int array[4] = { 1, 2, 3, 4 }; std::vector<int> integers(array, array + 4); std::ostream_iterator<int> output(cout, " "); std::unique_lock<std::timed_mutex> lk(m1, std::chrono::milliseconds(5)); if (lk) { cout << "Got the lock" << endl; integers.push_back(5); integers.push_back(6); integers.push_back(7); } else { cout << "Didn't get the lock" << endl; } cout << "The integers vector contains: "; std::copy(integers.begin(), integers.end(), output); }
In Listing 8, the locking mechanism can fail (because the lock is already in use), but the code continues past that point. This design can help code performance, but the work that would have been done under lock may need to be revisited; in other words, some sort of retry may be required. Listing 9 shows an excerpt of the run of the code in Listing 8 where the lock has been acquired in the main program.
Listing 9Continuing after a lock has not been acquired.
Person1 modified age: 101 Didn't get the lock The integers vector contains: 1 2 3 4
Notice in Listing 9 that the integers vector hasn't been modified, because the calls to push_back() occur inside the critical section. Because the lock was acquired elsewhere, the critical section can't run. However, the code in Listing 8 doesn't grind to a halt waiting to acquire the lock; it just doesn't execute the critical section.
Over-Engineering Your Code
It's always important to remember that in software development nothing is ever free. Once you start to use features such as critical sections, you may incur a cost in the form of reducing the performance of your code. I recall once looking at a large block of Java code in which every single method was synchronizedevery method ran as a critical section! More than likely this is a case of over-engineering. The code will work, but performance probably will be dire, or the code won't scale well in the future.
This is where knowledge of your particular problem domain comes in handy. It's possible that you'll know ahead of time which classes need critical sections and which don't. Don't use critical sections where they aren't needed.
If you're running the code supplied with this article, look at the speed of performance when using mutexes in the manner I described earlier. Even for simple code, the performance cost of mutex use can be high. Of course, the cost of not using mutexes can be high as wellcoming, as it often does, in the form of virtually irreproducible race conditions!