======================== How-To use lock policies ======================== One of C++'s strengths is that you can write very generic code with templates -- but sometimes that also turns into a weakness. Each template *does* make some assumptions about how it is going to be used. Problem ======= When you're building relatively simple things like containers, etc. that may not matter too much. But when you write algorithms, sometimes they might be used in a multi-threaded context, and sometimes single-threaded. In order to remain thread-safe, it's best the algorithm is prepared for this, but it shouldn't incur a cost in single-threaded usage. So why not parametrize your code with how it's intended to be used? That's what lock policies are. They're based on the understanding that thread safety is usually built upon two primitives: a `mutex` and a `lock` for the mutex. In order to parametrize your code, you need both of them. Naively, this would work something like this: .. sourcecode:: cpp :linenos: :dedent: :caption: Naive lock policies #1 template inline void my_algorithm() { mutexT mutex; // enter critical section { lockT lock{mutex}; // critical section } // exit critical section; lock unlocks mutex at end of scope. } As shown here, the example makes little sense. The only reason to lock a mutex is if there is data to be accessed that could be shared in a separate thread. Let's improve this a bit: .. sourcecode:: cpp :linenos: :dedent: :caption: Naive lock policies #2 template inline void my_algorithm(dataT & data) { mutexT mutex; // enter critical section { lockT lock{mutex}; // critical section - do something with "data" } // exit critical section; lock unlocks mutex at end of scope. } Unfortunately, we'll have to work on this a little more. Sure, we've now got some reference to some data that we're working on -- but the *mut*\ ual *ex*\ clusion exists to, well, exclude some other party from working on that same data at the same time. Which means that party needs access to the same mutex for locking to work. .. note:: As a general rule many of you already know, data and its mutex "live" together; algorithms must lock the mutex that belongs to its data. So let's work on this more. .. sourcecode:: cpp :linenos: :dedent: :caption: Naive lock policies #3 template inline void my_algorithm(dataT & data, mutexT & mutex) { // enter critical section lockT lock{mutex}; // critical section - do something with "data" // exit critical section; lock unlocks mutex at end of scope. } Now there are only a few problems remaining with this code. - First off, the lock and mutex types must *match*. It's not going to work to use a ``std::lock`` with some hand-rolled mutex type. - Second, this code hardcodes that the mutex is available for use via a reference. It could also be a pointer! - Lastly, while this code works when you hand it a ``std::mutex`` and a ``std::unique_lock``, what about when you want to use it lock free in a single threaded context? How do policies help here? Solution with Policies ====================== The first thing we should do is pass the muxex and lock types together, as a policy. .. sourcecode:: cpp :linenos: :dedent: :caption: Policy preparation template inline void my_algorithm(dataT & data, typename lock_policyT::mutex_type & mutex) { // enter critical section typename lock_policyT::lock_type lock{mutex}; // critical section - do something with "data" // exit critical section; lock unlocks mutex at end of scope. } Now this doesn't change the code *a lot*, but it does make clear that the mutex and lock type get selected together. You can now instanciate this algorithm with a lock policy that does... well, nothing. This gets rid of overhead in your single-threaded use case. .. sourcecode:: cpp :linenos: :dedent: :caption: Policy usage (null_lock_policy) std::vector data; // or whatever using namespace liberate::concurrency; typename null_lock_policy::mutex_type mutex; my_algorithm(data, mutex); The mutex of the :cpp:struct:`liberate::concurrency::null_lock_policy` struct is empty, and the policy itself does nothing with it. What about when you *do* want mutual exclusion? You can construct a lock policy out of any mutex/lock combination you'd like (at least the ones from the standard library). .. sourcecode:: cpp :linenos: :dedent: :caption: Policy usage (lock_policy) using namespace liberate::concurrency; using my_policy = lock_policy>; std::mutex mutex; // someplace else my_algorithm(data, mutex); Now in this example, we've defined the policy type and created the mutex just before using them. In real code, remember that the mutex and data will be shared between threads (or potentially so). That usually implies that the policy is also decided upon further away from this call to your algorithm. By simply passing on the policy type through your code, you can write completely generic code that can work with or without locking. Pointers ======== Now we do have references in C++, but sometimes we still need to use pointers -- sometimes raw, smart preferred. If your code is complex enough that the ownership of the mutex is not so easily defined, fear not, the policy approach still works. .. sourcecode:: cpp :linenos: :dedent: :caption: Policy usage with pointers using namespace liberate::concurrency; using my_policy = lock_policy< std::smart_ptr, std::unique_lock >; auto mutex = std::make_shared(); // someplace else my_algorithm(data, mutex); See? No code change. This works, because :cpp:struct:`liberate::concurrency::lock_policy` is fairly clever, and *does not* use ``std::unique_lock`` here. Instead, it provides a very simply proxy object that accepts the pointer in its constructor, then dereferences it, and passes that on to a lock instance it holds. This approach works for simple usages, but it doesn't give access to any of the lock's member functions. You can use the :cpp:func:`liberate::concurrency::lock_ref` function for this: .. sourcecode:: cpp :linenos: :dedent: :caption: Usage of lock_ref template inline void my_algorithm(dataT & data, typename lock_policyT::mutex_type & mutex) { // enter critical section typename lock_policyT::lock_type lock{mutex}; // critical section - do something with "data" // Call a lock function liberate::concurrency::lock_ref(lock).unlock(); } .. warning:: Not all lock implementations have member functions, let alone the same ones. Using ``lock_ref`` is not the way to write generic code, but it may nonetheless prove useful for your case.