This paper analyses the realization of LLVM libc++: http://libcxx.llvm.org/
The various mutexes, lock objects in c++11 are actually encapsulated in POSIX mutex,condition. But there are a lot of details worth learning. Std::mutex
Let's take a look at the Std::mutex:
The packet added a pthread_mutex_t __m_, very simple, what each function should do.
class mutex {pthread_mutex_t __m_;
Public:mutex () _noexcept {__m_ = (pthread_mutex_t) <strong>pthread_mutex_initializer</strong>;}
~mutex ();
Private:mutex (const mutex&);/= delete;
mutex& operator= (const mutex&);//= delete;
Public:void Lock ();
BOOL Try_lock () _noexcept;
void unlock () _noexcept;
typedef pthread_mutex_t* NATIVE_HANDLE_TYPE;
_libcpp_inline_visibility Native_handle_type Native_handle () {return &__m_;}};
Mutex::~mutex () {Pthread_mutex_destroy (&__m_);}
void Mutex::lock () {int EC = Pthread_mutex_lock (&__M_);
if (EC) __throw_system_error (EC, "mutex lock failed");
BOOL Mutex::try_lock () _noexcept {return Pthread_mutex_trylock (&__m_) = = 0;}
void Mutex::unlock () _noexcept {int ec = Pthread_mutex_unlock (&__M_);
(void) EC;
ASSERT (ec = 0); }
three lock states: std::d efer_lock, Std::try_to_lock, Std::adopt_lock These three are used to identify locks in the state of the lock when passed to some wrapper class: std::d efer_lock, has not acquired the lock Std::try_to_lock, when the wrapper class constructs, attempts to obtain the lock Std::adopt_lock, the caller has obtained the lock
These three dongdong, in fact, are for the partial specificity, is the three empty struct:
struct defer_lock_t {};
struct try_to_lock_t {};
struct adopt_lock_t {};
constexpr defer_lock_t Defer_lock = defer_lock_t ();
constexpr try_to_lock_t try_to_lock = try_to_lock_t ();
constexpr adopt_lock_t Adopt_lock = adopt_lock_t ();
In the following code, you can see how these three things are used.
Std::lock_guard
This class is more important because when we actually use lock, most of it is used.
This class is actually very simple:
Call Mutext.lock () in the constructor,
In the release function, the Mutex.unlock () function is called.
Because C + + automatically invokes the destructor of a variable within the scope when the function throws an exception, using Std::lock_guard can automatically release the lock when the exception is used, which is why you should avoid functions that use the mutex directly, but instead use the Std::lock_ The reason for the guard.
Template <class _mutex>
class Lock_guard
{public
:
typedef _mutex MUTEX_TYPE;
Private:
mutex_type& __m_;
Public:
Explicit Lock_guard (mutex_type& __m)
: __m_ (__m) {__m_.lock ();}
Lock_guard (mutex_type& __m, adopt_lock_t)
: __m_ (__m) {}
~lock_guard () {__m_.unlock ();}
Private:
Lock_guard (Lock_guard const&);//= delete;
lock_guard& operator= (Lock_guard const&);//= delete;
Note that the two constructors of Std::lock_guard, when only a mutex is passed, call Mutext.lock () at the time of the constructor to obtain the lock.
when adopt_lock_t is passed, the caller has already got the lock, so no attempt is made to acquire the lock. Std::unique_lock
Unique_lock is actually also a wrapper class, and naming it as unique may be distinguished from the Std::lock function.
Notice that there is an extra owns_lock function and a release () function, which are used in the Std::lock function.
The Owns_lock function is used to determine whether a lock is possessed;
The release () function discards the connection to the lock, and does not unlock the lock when it is destructor.
Then look at the implementation of the Unique_lock, you can find that the above three types are used to do the partial special use :
Template <class _mutex> class Unique_lock {public:typedef _mutex mutex_type; private:mutex_type* __m_;
BOOL __owns_;
Public:unique_lock () _noexcept: __m_ (nullptr), __owns_ (false) {} explicit Unique_lock (mutex_type& __m)
: __m_ (&__m), __owns_ (True) {__m_->lock ();} Unique_lock (mutex_type& __m, defer_lock_t) _noexcept: __m_ (&__m), __owns_ (false) {} unique_lock (mutex _type& __m, try_to_lock_t)///partial specificity: __m_ (&__m), __owns_ (__m.try_lock ()) {} unique_lock (mutex_type&am P
__m, adopt_lock_t)///partial specificity: __m_ (&__m), __owns_ (True) {} template <class _clock, class _duration> Unique_lock (mutex_type& __m, const chrono::time_point<_clock, _duration>& __t): __m_ (& Amp;__m), __owns_ (__m.try_lock_until (__t)) {} template <class _rep, class _period> Unique_lock (Mutex_typ
e& __m, const chrono::d uration<_rep, _period>& __d) : __m_ (&__m), __owns_ (__m.try_lock_for (__d)) {} ~unique_lock () {if (__owns_) __
M_->unlock (); } private:unique_lock (Unique_lock const&);
= delete; unique_lock& operator= (Unique_lock const&);
= delete; Public:unique_lock (unique_lock&& __u) _noexcept: __m_ (__u.__m_), __owns_ (__u.__owns_) {__u._ _m_ = nullptr;
__u.__owns_ = false;} unique_lock& operator= (unique_lock&& __u) _noexcept {if (__owns_) __M_-&G
T;unlock ();
__m_ = __u.__m_;
__owns_ = __u.__owns_;
__u.__m_ = nullptr;
__u.__owns_ = false;
return *this;
} void Lock ();
BOOL Try_lock ();
Template <class _rep, class _period> bool Try_lock_for (const chrono::d uration<_rep, _period>& __d); Template <class _clock, class _duration> bool Try_lock_until (const CHRONO::TIME_POint<_clock, _duration>& __t);
void unlock ();
void swap (unique_lock& __u) _noexcept {_vstd::swap (__m_, __u.__m_);
_vstd::swap (__owns_, __u.__owns_);
} mutex_type* Release () _noexcept {mutex_type* __m = __m_;
__m_ = nullptr;
__owns_ = false;
return __m;
BOOL Owns_lock () const _noexcept {return __owns_;}
operator BOOL () const _NOEXCEPT {return __owns_;} mutex_type* Mutex () const _noexcept {return __m_;}};
The Std::lock and Std::try_lock functions are all class objects, and these are functions.
The Std::lock and Std::try_lock functions are used to prevent deadlocks when multiple locks are used at the same time. This is really important because the handwriting code handles the synchronization of multiple locks and is prone to error.
Note The return value of the Std::try_lock function:
when successful, return-1;
when the failure, the return of the first few locks did not obtain success, with 0 start counting;
First look at the case of only two locks, although the code seems relatively simple, but there are large articles:
Template <class _l0, class _l1> void Lock (_l0& __l0, _l1& __l1) {while (true) {
Unique_lock<_l0> __u0 (__l0); if (__l1.try_lock ())//has obtained the lock l0, then attempts to obtain L1 {__u0.release ();
Both L0 and L1 have been acquired because Unique_lock releases the l0 when it is released, so call the release () function without releasing the l0 lock.
Break
}//if the L0,L1 fails at the same time, L0 is released here. Sched_yield (); The thread is placed at the end of the scheduling queue of the same priority, and the CPU switches to the other thread to execute {unique_lock<_l1> __u1 (__L1);//Because the previous attempt to obtain L1 failed, indicating that there are other threads holding L1, then
This time, try to acquire the lock L1 (only the previous thread is freed before it is possible to acquire) if (__l0.try_lock ()) {__u1.release ();
Break
} Sched_yield (); } template <class _l0, class _l1> int try_lock (_l0& __l0, _l1& __l1) {unique_lock<_l0> __u0 (__l
0, Try_to_lock);
if (__u0.owns_lock ()) {if (__l1.try_lock ())//Note the definition of the Try_lock return value, otherwise this cannot be understood here {__u0.release ();return-1;
else return 1;
return 0; }
the lock function above prevents deadlocks by trying. There are two locks in the case, then in the case of multiple parameters.
Let's take a look at the implementation of the Std::try_lock function:
It recursively calls the Try_lock function itself, and if all the locks are successful, then release all the Unique_lock in turn.
If there is a failure, count the number of failures and eventually return.
Template <class _l0, class _l1, Class _l2, class ... _l3>
int
try_lock (_l0& __l0, _l1& __l1, _l2& __l2, _l3& __l3)
{
int __r = 0;
Unique_lock<_l0> __u0 (__l0, try_to_lock);
if (__u0.owns_lock ())
{
__r = Try_lock (__l1, __L2, __l3 ...);
if (__r = = 1)
__u0.release ();
else
++__r;
}
return __r;
}
Then look at the implementation of Multi-parameter Std::lock:
Template <class _l0, class _l1, Class _l2, class ..._l3> void __lock_first (int __i, _l0& __l0, _l1& __l1, _l
2& __l2, _l3& ... __l3) {while (true) {switch (__i)//__i is used to mark the first lock in the last fetch parameter failed, counting starting from 0 {
Case 0://First execution, __i is 0 {unique_lock<_l0> __u0 (__l0);
__i = Try_lock (__l1, __L2, __l3 ...); if (__i = = 1)//acquired after L0, if the attempt to acquire the following lock succeeds, that is, if all locks are acquired, set Unique_lock to release and return {__U0.R
Elease ();
Return }} ++__i; Because __i represents a failure to acquire the first few locks, the above try_lock (__l1,__l2__l3,...)
It starts with L1, so here's +1, adjust to a lock that doesn't get successful, and start with it next time.
Sched_yield ();
Break
Case 1://Description Last acquired L1 failed, this time get L1 first.
{unique_lock<_l1> __u1 (__L1); __i = Try_lock (__l2, __l3 ..., __l0); Put the previous l0 in the end.
This time get the L1 first, and then try to get the back of the lock.
if (__i = = 1) {__u1.release ();
Return } if (__i = = sizeof ... (_L3) + 1)//Note When the L0 is placed on the last side, the final acquisition of the L0 fails.
Now that there are other threads holding L0, the next time you want to start from L0.
__i = 0; else __i = 2;
Because __i represents a failure to acquire the first few locks, the Try_lock (__l2,__l3 ..., __l0) on the top is started from L2, so here's +2 Sched_yield ();
Break Default: __lock_first (__i-2, __l2, __l3 ..., __l0, __L1);
Since this is the beginning of the L2, the __i will be reduced by 2.
Return }} template <class _l0, class _l1, Class _l2, class ..._l3> inline _libcpp_inline_visibility void Lock (_L0&A mp __l0, _l1& __l1, _l2& __l2, _l3& ... __l3) {__lock_first (0, __l0, __L1, __l2, __l3 ...);
You can see Std::lock implementations of multiple parameters:
get a lock first, then call Std::try_lock to get the rest of the lock, and if it fails, get the last failed lock the next time.
repeat the above procedure until you have successfully acquired all the locks.
The above algorithm implements the rotation of parameters in a more ingenious way. Std::timed_mutex
The Std::timed_mutex is encapsulated with mutexes and condition, so that two functions can be used:
Try_lock_for
Try_lock_until
is actually a POSIX mutex and condition wrapper.
Class Timed_mutex {Mutex __m_;
Condition_variable __cv_;
BOOL __locked_;
Public:timed_mutex ();
~timed_mutex (); Private:timed_mutex (const timed_mutex&);
= delete; timed_mutex& operator= (const timed_mutex&);
= delete;
Public:void Lock ();
BOOL Try_lock () _noexcept; Template <class _rep, class _period> _libcpp_inline_visibility bool Try_lock_for (const chrono::d Urat
Ion<_rep, _period>& __d) {return Try_lock_until (Chrono::steady_clock::now () + __d);} Template <class _clock, class _duration> bool Try_lock_until (const Chrono::time_point<_clock, _DURATION&G
t;& __t);
void unlock () _noexcept;
}; Template <class _clock, class _duration> bool Timed_mutex::try_lock_until (const Chrono::time_point<_clock, _
duration>& __t) {using namespace Chrono;
Unique_lock<mutex> __lk (__M_); BOOL No_timeout = _clock::now() < __t;
while (No_timeout && __locked_) no_timeout = __cv_.wait_until (__lk, __t) = = Cv_status::no_timeout;
if (!__locked_) {__locked_ = true;
return true;
return false; }
Std::recursive_mutex and Std::recursive_timed_mutex
These two are actually implementations of the recursive pattern of Std::mutex and Std::timed_mutex, that is, the lock () function can be called repeatedly by the recipient.
Is the same as the recursive mutex in the POSIX mutex.
Look at the Std::recursive_mutex constructor and you'll see.
Recursive_mutex::recursive_mutex ()
{
pthread_mutexattr_t attr;
int ec = Pthread_mutexattr_init (&attr);
if (EC)
goto fail;
EC = Pthread_mutexattr_settype (&attr, pthread_mutex_recursive);
if (EC)
{
Pthread_mutexattr_destroy (&attr);
goto fail;
}
EC = Pthread_mutex_init (&__m_, &attr);
if (EC)
{
Pthread_mutexattr_destroy (&attr);
goto fail;
}
EC = Pthread_mutexattr_destroy (&attr);
if (EC)
{
Pthread_mutex_destroy (&__m_);
goto fail;
}
return;
Fail:
__throw_system_error (EC, "Recursive_mutex constructor failed");
Std::cv_status
This is used to indicate the state of the condition waiting to return, and the three above represents the use of the lock state.
Enum Cv_status
{
no_timeout,
timeout
};
std::condition_variable
Packaged with POSIX condition variable.
Class Condition_variable {pthread_cond_t __cv_; Public:condition_variable () {__cv_ = (pthread_cond_t) PTHREAD_CON
D_initializer;}
~condition_variable (); Private:condition_variable (const condition_variable&);
= delete; condition_variable& operator= (const condition_variable&);
= delete;
Public:void Notify_one () _noexcept;
void Notify_all () _noexcept;
void Wait (unique_lock<mutex>& __lk) _noexcept;
Template <class _predicate> void Wait (unique_lock<mutex>& __lk, _predicate __pred);
Template <class _clock, class _duration> cv_status wait_until (unique_lock<mutex>& __lk,
Const Chrono::time_point<_clock, _duration>& __t); Template <class _clock, class _duration, class _predicate> bool Wait_until (unique_lock<mutex>& Amp
__LK, const Chrono::time_point<_clock, _duration>& __t, _predicate __pred);
Template <class _rep, class _period> cv_status wait_for (unique_lock<mutex>& __lk,
Const Chrono::d uration<_rep, _period>& __d); Template <class _rep, class _period, class _predicate> bool Wait_for (unique_lock<mutex>& _
_lk, const chrono::d uration<_rep, _period>& __d, _predicate __pred);
typedef pthread_cond_t* NATIVE_HANDLE_TYPE;
_libcpp_inline_visibility Native_handle_type Native_handle () {return &__cv_;} Private:void __do_timed_wait (unique_lock<mutex>& __lk, Chrono::time_point<chrono::system_clock, C
hrono::nanoseconds>) _noexcept; };
The functions inside are all intuitive implementations, and it's worth noting:
The cv_status is determined by the time it is judged, and if the timeout is returned cv_status::timeout, return cv_status::no_timeout If there is no timeout.
The Condition_variable::wait_until function can pass in a predicate, which is a user-defined function that determines whether the condition is eligible. This is also a very common method of template programming.
Template <class _clock, class _duration>
cv_status condition_variable::wait_until
(unique_lock< mutex>& __lk,
const chrono::time_point<_clock, _duration>& __t)
{
using namespace Chrono;
Wait_for (__lk, __t-_clock::now ());
Return _clock::now () < __t? cv_status::no_timeout:cv_status::timeout;
}
Template <class _clock, class _duration, class _predicate>
bool
condition_variable::wait_until (unique _lock<mutex>& __lk,
const Chrono::time_point<_clock, _duration>& __t,
_Predicate __ pred)
{
while (!__pred ())
{
if (Wait_until (__lk, __t) = cv_status::timeout) return
__pred ( );
}
return true;
}
Std::condition_variable_any
The Std::condition_variable_any interface is the same as std::condition_variable, except that std::condition_variable can only use Std::unique_lock <std::mutex>, and Std::condition_variable_any can use any lock object.
Let's see why Std::condition_variable_any can use any lock object.
Class _libcpp_type_vis Condition_variable_any {condition_variable __cv_;
Shared_ptr<mutex> __mut_;
Public:condition_variable_any ();
void Notify_one () _noexcept;
void Notify_all () _noexcept;
Template <class _lock> void Wait (_lock& __lock);
Template <class _lock, class _predicate> void Wait (_lock& __lock, _predicate __pred);
Template <class _lock, class _clock, class _duration> cv_status wait_until (_lock& __lock,
Const Chrono::time_point<_clock, _duration>& __t); Template <class _lock, class _clock, Class _duration, class _predicate> bool Wait_until (_lock& _ _lock, const Chrono::time_point<_clock, _duration>& __t, _predicate __pred
);
Template <class _lock, class _rep, class _period> cv_status wait_for (_lock& __lock, Const Chrono::dUration<_rep, _period>& __d);
Template <class _lock, class _rep, Class _period, class _predicate> bool Wait_for (_lock& __lock,
Const Chrono::d uration<_rep, _period>& __d, _predicate __pred); };As you can see, in the Std::condition_variable_any, shared_ptr<mutex> __mut_ are used to wrap the mutex. So everything is clear,
review Std::unique_lock<std::mutex>, which wraps the mutex and automatically releases the mutex when it is destructor. In Std::condition_variable_any, the job was made by shared_ptr<mutex>.
Therefore, it is also easy to draw a conclusion that Std::condition_variable_any will be slightly slower than std::condition_variable .
the other stuff:
The man Manual for the Sched_yield () function:
Sched_yield () causes the calling thread to relinquish the CPU. The thread is moved to the "end of" the queue for its
Static priority and a new thread gets to run.
In the c++14 there are std::shared_lock and Std::shared_timed_mutex, but there is no corresponding implementation in libc++, so do not do analysis. Summary
The various mutexes in the LLVM libc++, lock, condition variable are actually enclosing the corresponding implementations in POSIX. The technique of encapsulation and some details are worth studying carefully.
After reading the implementation of the source code, for how to use it more clearly. Reference:
Http://en.cppreference.com/w/cpp
http://libcxx.llvm.org/