Launching threads
A new thread is launched by passing an object of a callable type that can be
invoked with no parameter to the constructor. The object is then copied
into internal storage, and invoked on the newly-created thread of
execution. If the object must not be copied, then boost::ref can be
used to pass in a reference to the function object.
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
If you want to construct an instance of boost::thread with a function or
callable object that requires arguments to be suppied, this can be done
by passing additional arguments to the boost::thread constructor.
?
1 2 3 |
|
The arguments are copied into the internal thread structure.
If a reference is required, use boost::ref.
There is an unspecified limit on the number of additional arguments
that can be passed.
Join and detach a thread
Our thread has launched another thread. There are a couple of things that we
can do with it now: wait for its termination or let it go.
We can wait newly-created thread for some times using
timed_join().
Or we can wait indefinitely for it, by calling join().
A third alternative is detaching the thread from the boost::thread object. In
this way, we are saying that we don‘t have anything to do with the newly-created
thread any more. It would do its job till the end, and we are not
concerned with its life and death.
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
Synchronization
This chapter gonna be interesting.
Consider designing a BankAccount class which have two member functions
Deposit(int), Withdraw(int) and one member variable balance_.
With RAII idiom we can write code like this: (for
boost::lock_guard, refer to
http://stackoverflow.com/questions/2276805/boostlock-guard-vs-boostmutexscoped-lock)
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
The object-level locking idiom doesn‘t cover the entire richness of a
threading model.
Internal and external locking
The BankAccount class above uses internal locking. Basiclly, a class that
uses internal locking guarantees that any concurrent calls to its public member
functions don‘t corrupt an instance of that class. This is tyically ensured by
having each public member function acquire a lock on the object upon entry. This
way, for any object of that class, there can be only one member function all
active at any moment, so the operation are nicely serialized.
Unfortunately, "simple" might sometimes morph into "simplistic".
Internal locking is insufficient for many real-world synchronization tasks.
Imagine that you want to implement an ATM transaction with BankAccount class.
The requirements are simple. The ATM transaction consists of two withdrawals-
one for the actural money and one for $2 commission. The two withdrawls must
appear in strict sequence; that is, atomic.
The obvious implementation is erratic:
?
1 2 3 4 |
|
The problem is that between the two calls above, another thread can
perform another operation on the account, thus breaking atomic.
In an attempt to solve this problem, let‘s lock the account from outside
during the two operations:
?
1 2 3 4 5 |
|
Notice that the code above doesn‘t compile, the mtx_ field is private. We
have two possibilities:
1) make mtx_ public
2) make the BankAccount lockable by adding the lock/unlock functions
We can add functions explicitly:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Or inherit from a class which add these lockable functions.
The basic_lockable_adaptor class helps to define the BankAccount class as
?
1 2 3 4 5 6 7 8 9 10 |
|
and the code that doesn‘t compile becomes
?
1 2 3 4 5 |
|
// Notice that lock_guard need a member which have lock() and unlock()
function
Notice that now acct is being locked by Withdraw after it has already been
locked by guard. When running such code, one of two things happens.
1) Your mutex implementation might support the so-called recursive mutex
semantics. This means that the same thread can lock the same mutex several times
successfully. In this case, the implementation works but has a performance
overhead due to the unnecessary locking.
2) Your mutex implementation might not support recursive locking, which means
that as soon as you try to acquire it the second time. it blocks-so the
ATMWithdraw function enters the deaded deadlock.
As boost::mutex is not recursive, we need to use recursive version
boost::recursive_mutex.
?
1 2 3 4 |
|
The caller-ensured locking approach is more flexible and the
most efficient, but very dangerous. In an implementation using caller-ensured
locking, BandAccount still hold mutex(class hold mutex or make mutex globally
visiable), but its member functions don‘t manipulate it at all. Deposit and
Withdraw are not thread-safe anymore. Instead, the client code is responsible
for locking BandAccount properly.
To conclude, if in designing a multi-threaded class you settle on internal
locking, you expose yourself to inefficiency or deadlocks. On the other hand, if
you rely on caller-provided locking, you make your class error-prone and
difficult to use. Finally, external locking completely avoids the issues by
leaving it to the client code.
External locking -- strict_lock and externally_locked classes
This tutorial is an adaptation of the paper of Andrei Alexandrescu
"Multithreading and the C++ Type System" to the Boost library.
Ideally, the BankAccount class should do the following:
1) Support both locking models(internal and external)
2) Be efficient
3) Be safe
For achieving that we need one more class
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
strict_lock must adhere to a non-copy and non-alias policy.
strict_lock disables copying by making the copy constructor and the assignment
operator private.
You can create a strict_lock<T> only starting from a valid T object.
Notice that there is no other way you can create a strict_lock<T>
?
1 2 3 4 5 6 7 8 |
|
Here is the new BankAccount class:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Now, if you want the benefit of internal locking, you simply call
Desposit(int) or Withdraw(int). If you want to use external locking:
?
1 2 3 4 5 |
|
There is only one problem left, that is:
?
1 2 3 4 5 6 |
|
And that‘s why we have member function owns_lock() in the first place:
BankAccount needs to use this function compare the locked object against
this:
?
1 2 3 4 5 6 7 8 9 10 |
|
The overhead incurred by the test above is much lower than locking a
recursive mutex for the second time.
Improving External Locking
// http://www.boost.org/doc/libs/1_55_0/doc/html/thread/synchronization.html#thread.synchronization.lock_concepts
Condition Variabls
The general usage pattern is that one thread locks a mutex and then
calls wait
on an instance
of condition_variable
or condition_variable_any
. When the thread
is woken from the wait, then it checks to see if the appropriate condition is
now true, and continues if so. If the condition is not true, then the thread
then calls wait
again to resume waiting. In the
simplest case, this condition is just a boolean variable:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Notice that the lock is passed to wait: wait atomically add the thread to the
set of threads waiting on the condition variable, and unlock the
mutex. When the thread is woken, then mutex will lock again before the
call to wait returns. This allows other threads to acquire the mutex in order to
update the shared data, and ensure that the data associated with the condition
is correcyly synchronized.
I was learning the usage of boost thread when I found this tutorial. I found
this tutorial interesting and might be useful and it cost me almost an entire
day to understand, record it.