首页 > 代码库 > Boost Thread and Synchronization Tutorial
Boost Thread and Synchronization Tutorial
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 | struct callable { void operator()(); }; boost:: thread copies_are_safe() { callable x; return boost:: thread (x); } // x is destroyed, but the newly-created thread has a copy, so this is OK boost:: thread oops() { callable x; return boost:: thread (boost::ref(x)); } // x is destroyed, but the newly-created thread still has a reference // this leads to undefined behavior |
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 | void find_the_question( int the_answer); boost:: thread deep_thought_2(find_the_question, 42); |
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 | // source http://thisthread.blogspot.com/2010/09/joining-and-detaching-boostthread.html #include <boost/thread.hpp> #include <iostream> class Callable { private : int value_; public : Callable( int value) : value_(value) { } void operator()() { std::cout << "cout down " ; while (value_ > 0) { std::cout << value_ << " " ; boost::this_thread::sleep(boost::posix_time::seconds(1)); --value_; } std::cout << "done" << std::endl; } }; int main() { std::cout << "Launching a thread" << std::endl; boost:: thread t1(Callable(6)); t1.timed_join(boost::posix_time::seconds(2)); std::cout << std::endl << "Expired waiting for timed_join()" << std::endl; t1.join(); std::cout << "Secondary thread joined" << std::endl; Callable c2(3); boost:: thread t2(c2); t2.detach(); std::cout << "Secondary thread detached" << std::endl; boost::this_thread::sleep(boost::posix_time::seconds(5)); std::cout << "Done thread testing" << std::endl; } |
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 | class BankAccount { private : boost::mutex mtx_; int balance_; public : void Deposit( int amount) { boost::lock_guard<boost::mutex> guard(mtx_); balance_ += amount; } void Withdraw( int amount) { boost::lock_guard<boost::mutex> guard(mtx_); balance_ -= amount; } int GetBalance() { boost::lock_guard<boost::mutex> guard(mtx_); return balance_; } }; |
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 | void ATMWithdrawal(BandAccount& acct, int sum) { acct.Withdraw(sum); acct.Withdraw(2); } |
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 | void ATMWithdrawal(BandAccount& acct, int sum) { boost::lock_guard<boost::mutex> guard(acc.mtx_); acct.Withdraw(sum); acct.Withdraw(2); } |
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 | class BankAccout { private : boost::mutex mtx_; int balance_; public : void Deposit() { // same as before } void Withdraw() { // same as before } void lock() { mtx_.lock(); } void unlock() { mtx_.unlock(); } }; |
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 | class BankAccount : public basic_lockable_adaptor<mutex> { private : void Deposit( int amount) { //same as before } void Withdraw( int amount) { // same as before } int GetBalance( int amount) { // same as before } }; |
and the code that doesn‘t compile becomes
1 2 3 4 5 | void ATMWithdrawal(BandAccount& acct, int sum) { boost::lock_guard<BandAccount> guard(acct); acct.Withdraw(sum); acc.Withdraw(2); } |
// 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 | class BankAccout : public basic_lockable_adaptor<recursive_mutex> { //... }; |
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 | template < typename Lockable> class strict_lock { public : typedef Lockable lockable_type; explicit strict_lock(lockable_type& obj) : obj_(obj) { obj.lock(); } ~strict_lock() { obj_.unlock(); } bool owns_lock(mutex_type const *l) const noexcept { return l == &obj_; } private : strict_lock(); strict_lock(strict_lock const &); strict_lock& operator=(strict_lock const &); private : lockable_type &obj_; }; |
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 | BandAccount myAccount( "John snow" , "123-33" ); strict_lock<BandAccount> myLock(myAccount); strict_lock<BandAccount> Foo(); // compile error void Bar(strict_lock<BandAccount>); // compile error strict_lock<BandAccount>& Foo(); // OK void Bar(strict_lock<BandAccount>&); //OK |
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 | class BankAccount : public basic_lockable_adaptor<boost::recursive_mutex> { public : void Deposit( int amount, strict_lock<BandAccount>&) { balance_ += amount; } void Withdraw( int amount, strict_lock<BandAccount>&) { balance_ -= amount; } void Deposit( int amount) { strict_lock<boost::mutex> guard(* this ); Deposit(amount, guard); } void Withdraw( int amount) { strict_lock<boost::mutex> guard(* this ); Withdraw(amount, guard); } }; |
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 | void ATMWithdrawal(BandAccount& acct, int amount) { strict_lock<BandAccount> guard(acct); acct.Withdraw(sum, guard); acct.Withdraw(2, guard); } |
There is only one problem left, that is:
1 2 3 4 5 6 | void ATMWithdrawal(BandAccount& acct, int amount) { BandAccount fakeAcct( "John snow" , "122" ); strict_lock<BandAccount> guard(fakeAcct); acct.Withdraw(sum, guard); acct.Withdraw(2, guard); } |
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 | class BankAccount : public basic_lockable_adaptor<boost::recursive_mutex> { public : void Deposit( int amount, strict_lock<BandAccount>& guard) { if (!guard.owns_lock(* this )) throw "Locking error\n" ; balance_ += amount; } //... }; |
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 | boost::condition_variable cond; boost::mutex mut; bool data_ready; void process_data(); void wait_for_data_to_process() { boost::unique_lock<boost::mutex> lock(mut); while (!data_ready) { cond.wait(lock); } process_data(); } void prepare_data_for_process() { boost::lock_guard<boost::mutex> lock(mut); data_ready = true ; cond.notify_one(); } |
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.