Glibc版本:2.25.取重要的主线记一下,其他的一些代码尚未研究透彻,待研究透彻再补上,方便日后查阅。
当前版本中的mutex主要有两部份完成,一个是原子操作,另一个是futex。原子已在__lll_lock博客中记下了,futex属于内核部分,将在下一篇博客中介绍。
-
int
- __pthread_mutex_lock (pthread_mutex_t *mutex)
- {
- assert (sizeof (mutex->__size) >= sizeof (mutex->__data));
- unsigned int type = PTHREAD_MUTEX_TYPE_ELISION (mutex);
- LIBC_PROBE (mutex_entry, 1, mutex);
- if (__builtin_expect (type & ~(PTHREAD_MUTEX_KIND_MASK_NP
- | PTHREAD_MUTEX_ELISION_FLAGS_NP), 0))
- return __pthread_mutex_lock_full (mutex);
- if (__glibc_likely (type == PTHREAD_MUTEX_TIMED_NP))
- {
- FORCE_ELISION (mutex, goto elision);
- simple:
- /* Normal mutex. */
- LLL_MUTEX_LOCK (mutex);
- assert (mutex->__data.__owner == 0);
- }
如上代码第6行,获取mutex类型,共有四种类型:
PTHREAD_MUTEX_TIMED_NP: 这是最基本的一种,也称为normal,也是默认的类型,当多个进程同时竞争这种类型的mutex时,它们会按照优先级在内核排序,当有其他线程unlock的时候,唤醒其中一个等待的线程。
PTHREAD_MUTEX_RECURSIVE_NP: 这种类型从名字也可以看出,是recurisive类型的。如果线程没有获得该mutex的情况下,争用该锁,那么与PTHREAD_MUTEX_TIMED_NP一样。与PTHREAD_MUTEX_TIMED_NP不一样的是,线程在已获得该锁的情况下仍能获得该锁。
PTHREAD_MUTEX_ADAPTIVE_NP: 适应锁,网上搜到的资料,感觉不准确。从下面的源码的分析中理解,这种锁主要是提高性能的。我们都知道,当临界区比较小的,自旋锁比互斥锁有更高的性能。PTHREAD_MUTEX_ADAPTIVE_NP类型的mutex在上锁的时候,会先持续尝试一定次数的try-lock,try-lock并不会导致线程切换,当一定次数的trylock不成功,才会lock,lock可能会导致线程切换。对于小的临界区,很有可能在try-lock的过程中就上锁成功,避免了线程切换带了的额外开销,当然是提高了性能了。
PTHREAD_MUTEX_ERRORCHECK_NP: 检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。
上面代码的10-12行没看懂,先不管吧。第14-20行针对的正是PTHREAD_MUTEX_TIMED_NP类型的锁。可以看到,并没有什么特别的处理,直接调用LLL_MUTEX_LOCK() ,该宏后面再解释,先把这这四种类型的锁先记完。
-
else if (__builtin_expect (PTHREAD_MUTEX_TYPE (mutex)
-
{
-
/* Recursive mutex. */
-
pid_t id = THREAD_GETMEM (THREAD_SELF, tid);
-
/* Check whether we already hold the mutex. */
-
if (mutex->__data.__owner == id)
-
{
-
/* Just bump the counter. */
-
if (__glibc_unlikely (mutex->__data.__count + 1 == 0))
-
/* Overflow of the counter. */
-
return EAGAIN;
-
-
++mutex->__data.__count;
-
-
return 0;
-
}
-
-
/* We have to get the mutex. */
-
LLL_MUTEX_LOCK (mutex);
-
-
assert (mutex->__data.__owner == 0);
-
mutex->__data.__count = 1;
- }
-
else if (__builtin_expect (PTHREAD_MUTEX_TYPE (mutex)
-
== PTHREAD_MUTEX_ADAPTIVE_NP, 1))
-
{
-
if (! __is_smp)
-
goto simple;
-
-
if (LLL_MUTEX_TRYLOCK (mutex) != 0)
-
{
-
int cnt = 0;
-
int max_cnt = MIN (MAX_ADAPTIVE_COUNT,
-
mutex->__data.__spins * 2 + 10);
-
do
-
{
-
if (cnt++ >= max_cnt)
-
{
-
LLL_MUTEX_LOCK (mutex);
-
break;
-
}
-
atomic_spin_nop ();
-
}
-
while (LLL_MUTEX_TRYLOCK (mutex) != 0); /* Try lock returns 0 means lock success */
-
-
mutex->__data.__spins += (cnt - mutex->__data.__spins) / 8;
-
}
-
assert (mutex->__data.__owner == 0);
- }
-
else
-
{
-
pid_t id = THREAD_GETMEM (THREAD_SELF, tid);
-
assert (PTHREAD_MUTEX_TYPE (mutex) == PTHREAD_MUTEX_ERRORCHECK_NP);
-
/* Check whether we already hold the mutex. */
-
if (__glibc_unlikely (mutex->__data.__owner == id))
-
return EDEADLK;
-
goto simple;
- }
LLL_MUTEX_LOCK:
从上面可以看到,无论哪一种类型的mutex,都会调用到LLL_MUTEX_LOCK来进行上锁。上面还提到了LLL_MUTEX_TRYLOCK,先解决这个简单一点的。
-
# define LLL_MUTEX_TRYLOCK(mutex) \
- lll_trylock ((mutex)->__data.__lock)
- #define lll_trylock(lock) \
-
atomic_compare_and_exchange_bool_acq (&(lock), 1, 0)
-
# define LLL_MUTEX_LOCK(mutex) \
- lll_lock ((mutex)->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex))
-
#define lll_lock(futex, private) \
- __lll_lock (&(futex), private)
- #define __lll_lock(futex, private) \
- ((void) \
- ({ \
- int *__futex = (futex); \
- if (__glibc_unlikely \
- (atomic_compare_and_exchange_bool_acq (__futex, 1, 0))) \
- { \
- if (__builtin_constant_p (private) && (private) == LLL_PRIVATE) \
- __lll_lock_wait_private (__futex); \
- else \
- __lll_lock_wait (__futex, private); \
- } \
- }))
- void
- __lll_lock_wait_private (int *futex)
- {
- if (*futex == 2)
- lll_futex_wait (futex, 2, LLL_PRIVATE); /* Wait if *futex == 2. */
-
- while (atomic_exchange_acq (futex, 2) != 0)
- lll_futex_wait (futex, 2, LLL_PRIVATE); /* Wait if *futex == 2. */
- }
- __lll_lock_wait (int *futex, int private)
-
{
-
if (*futex == 2)
- lll_futex_wait (futex, 2, private); /* Wait if *futex == 2. */
-
while (atomic_exchange_acq (futex, 2) != 0)
- lll_futex_wait (futex, 2, private); /* Wait if *futex == 2. */
- }
- /* Wait while * FUTEXP == VAL for a lll_futex_wake call on FUTEXP */
- #define lll_futex_wait(futexp, val, private) \
- ((void)) (private), \
- -__nacl_irt_futex.futex_wait_abs((volatitle int *)(futexp), val, NULL))
从上面的代码可以看到,LLL_MUTEX_LOCK会调用到__lll_lock,__lll_lock用atomic_compare_and_change_bool_acq()更改mutex->__lock的值,如果更改成功,表示mutex->__lock的原始值0,上锁成功。如果上原子比较交换指令不成功,就调用__lllock_wait/__lll_lock_wait_private()z这两个函数中的一个。
__lll_lock_wait()和__lll_lock_wait_private()这两个函数基本一样。在__lll_lock_wait()这个函数中,首先判断futex是不是2,如果是2就直接调用lll_futex_wait(),准备futex的系统调用。前面已经说过,futex(mutex->__lock)的值只有是0的时候才表示锁空着,所以2当然是已经被其他的线程获得了锁,自然需要调用futex系统调用等待。
为什么是2?这里有必要记录一下。起始观察整个pthread_mutex_lock中的源码可以看到,mutex->__lock的值只有三种可能:0,1,2。
0:很显然,没有人获得锁的情况下自然是0。
1:当只有一个线程调用pthread_mutex_lock()的时候,mutex->__lock的值被更改为1,如果在此之后没有其他的线程争用该mutex,那么这个mutex->__lock的值就会一直未1,知道unlock把他更新为0。
2:在值为1的时候,如果再次有其他的线程调用pthread_mutex_lock(),mutex->__lock的值就会被设置成2。所以当有多个线程争用同一把锁的时候,该值就会被设置成2。
所以这里,1和2表示了获得锁情况下,两种不同状态,1表示锁已被线程获得,但是没有竞争,2表示锁被线程获得,而且存在竞争。这样做的目的是为了提高pthread_mutex_unlock的效率。在pthread_mutex_unlock()中,会调用atomic_exchange_rel()无条件的把mutex->__lock的值更新为0,并且检查mutex->__lock的原始值,如果原始值为0或者1,表示没有竞争发生,自然也就没有必要调用futex系统调用,浪费时间。只有检查到mutex->__lock的值大于1的时候,才需要调用futex系统调用,唤醒等待该锁上的线程。
pthread_mutex_unlock()无条件的更新mutex->__lock的值为0,并不会造成有竞争的时候,mutex->__lock的值为0的状态。原因请看LLL_MUTEX_LOCK()代码段的20行和31行,这里是一个while循环,也就说当等待该锁的线程在被唤醒时首先会执行这个while循环,只有一个会把mutex->__lock的值更新2,而且这个成功更新的线程或获得该锁。
至此,pthread_mutex_lock的用户空间代码就分析完了,至于lll_futex_wait()怎到系统调用,这个大概需要编译器在连接的以后进行一些处理吧,现在还不清楚,但并不影响对pthread_mutex_lock()的理解。后面在分析一下内核空间futex干了些什么。