pthread包的mutex实现分析

pthread包

pthread是POSIX thread,一个在类UNIX系统下广泛使用的并发包,linux系统下在glibc库里实现。
pthread包里常用的mutex相关接口有:
pthread_mutex_init
pthread_mutex_lock
pthread_mutex_trylock
pthread_mutex_unlock
pthread_mutex_destroy
我们逐一分析之。

CAS

CAS是Compare And Swap的缩写,假设当前值V,期望值E,新值为N,用伪码表示是:

if V == E:
    V = N
    return true
else:
    return false    

CAS包含一个读操作(读取V与E比较)和一个写操作(设置V=N),其特殊处在于它是原子的,用一条机器指令实现(intel处理器中是cmpxchg),不可被其他处理器打断,这在并发编程中是很有用的。CAS的实现依赖于硬件支持,需要所在的CPU暂时锁住内存总线,不让其他CPU访问内存。CAS效率高(因为就一条指令),相比锁(mutex)而言是一种更轻量的并发保护机制。
java的AtomicLong.compareAndSet底层就是用CAS实现的。

mutex

理论上讲,mutex可用初始值=1的信号量表示,只需一个整数表示其状态:0表示未占用,1表示占用。那么,mutex的资源占用就只是一个int型了?
当然不是,我们可以看一下pthread包中mutex的定义:

typedef union
{
  struct __pthread_mutex_s
  {
    int __lock;
    unsigned int __count;
    int __owner;
    unsigned int __nusers;
    /* KIND must stay at this position in the structure to maintain
       binary compatibility.  */
    int __kind;
    int __spins;
    __pthread_list_t __list;
  } __data;
  ......
} pthread_mutex_t;

这是x86-64处理器下的mutex定义(32位处理器下的定义基本类似),占用32字节的空间。几个比较关键的成员定义如下:
__lock mutex状态,0表示未占用,1表示占用
__count 用于可重入锁,记录owner线程持有锁的次数
__owner owner线程ID
__kind 记录mutex的类型,有以下几个取值:

  PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。

  PTHREAD_MUTEX_RECURSIVE_NP,可重入锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。

  PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程重复请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型相同。

  PTHREAD_MUTEX_ADAPTIVE_NP,自适应锁,自旋锁与普通锁的混合。

pthread_mutex_init就是初始化上述的pthread_mutex_t内存结构。

pthread_mutex_lock处理了几种类型的mutex,先看普通锁:

if (__builtin_expect (type, PTHREAD_MUTEX_TIMED_NP)
      == PTHREAD_MUTEX_TIMED_NP)
    {
    simple:
      /* Normal mutex.  */
      LLL_MUTEX_LOCK (mutex);
      assert (mutex->__data.__owner == 0);
    }

就是调用LLL_MUTEX_LOCK宏获得锁。LLL_MUTEX_LOCK宏定义我们稍后再看。

这是可重入锁:

else if (__builtin_expect (type == PTHREAD_MUTEX_RECURSIVE_NP, 1))
    {
      /* Recursive mutex.  */

      /* Check whether we already hold the mutex.  */
      if (mutex->__data.__owner == id)
    {
      /* Just bump the counter.  */
      if (__builtin_expect (mutex->__data.__count + 1 == 0, 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;
    }

当发现owner就是自身,只是简单的自增__count成员即返回。否则,调用LLL_MUTEX_LOCK宏获得锁,若能成功获得,设置__count = 1,否则挂起。

这是检错锁:

else
    {
      assert (type == PTHREAD_MUTEX_ERRORCHECK_NP);
      /* Check whether we already hold the mutex.  */
      if (__builtin_expect (mutex->__data.__owner == id, 0))
    return EDEADLK;
      goto simple;
    }

它会侦测一个线程重复申请锁的情况,如遇到,报EDEADLK,从而避免这种最简单的死锁情形。若无死锁情形,goto simple语句会跳到普通锁的处理流程。

这是自适应锁:

else if (__builtin_expect (type == 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;
        }

#ifdef BUSY_WAIT_NOP
          BUSY_WAIT_NOP;
#endif
        }
      while (LLL_MUTEX_TRYLOCK (mutex) != 0);

      mutex->__data.__spins += (cnt - mutex->__data.__spins) / 8;
    }
      assert (mutex->__data.__owner == 0);
    }

从代码看,这种锁分两个阶段。第一阶段是自旋锁(spin lock),忙等待一段时间后,若还不能获得锁,则转变成普通锁。
所谓“忙等待”,在x86处理器下是重复执行nop指令,nop是x86的小延迟函数:

/* Delay in spinlock loop.  */
#define BUSY_WAIT_NOP   asm ("rep; nop")

获取锁的核心是LLL_MUTEX_LOCK宏,我们来看其定义:

# define LLL_MUTEX_LOCK(mutex) \
  lll_lock ((mutex)->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex))

PTHREAD_MUTEX_PSHARED宏表示该锁是进程锁还是线程锁,0表示线程锁,128表示进程锁,因mutex使用的核心算法既可适用于进程也可适用于线程。
从宏定义可知,获取锁的动作就是尝试修改锁的状态字段:__lock

lll_lock定义如下,我们只看线程锁部分的代码:

#define lll_lock(futex, private) \
  (void)                                      \
    ({ int ignore1, ignore2;                              \
       if (__builtin_constant_p (private) && (private) == LLL_PRIVATE)        \
     __asm __volatile ("cmpxchgl %1, %2\n\t"                   \
               "jnz _L_lock_%=\n\t"                   \
               ".subsection 1\n\t"                    \
               ".type _L_lock_%=,@function\n"             \
               "_L_lock_%=:\n"                    \
               "1:\tleal %2, %%ecx\n"                 \
               "2:\tcall __lll_lock_wait_private\n"           \
               "3:\tjmp 18f\n"                    \
               "4:\t.size _L_lock_%=, 4b-1b\n\t"              \
               ".previous\n"                      \
               LLL_STUB_UNWIND_INFO_3                 \
               "18:"                          \
               : "=a" (ignore1), "=c" (ignore2), "=m" (futex)     \
               : "0" (0), "1" (1), "m" (futex),           \
                 "i" (MULTIPLE_THREADS_OFFSET)            \
               : "memory");                       \
       else 

这是gcc里嵌入汇编的语法,其中:
: “=a” (ignore1), “=c” (ignore2), “=m” (futex)
是输出的寄存器列表,这里的意思表示ignore1使用EAX寄存器,ignore2使用ECX寄存器,futex使用的存储器。
另外,每个操作数会有一个Number与之对应。如果我们一共使用了n个操作数,那么输出操作里的第一个操作数就是0号,之后递增,所以,%0代表ignore1,%1代表ignore2,%2代表futex。

: “0” (0), “1” (1), “m” (futex)
是输入寄存器,”0”表示%0操作数,其值为0,亦即设置ignore1=0,同理ignore2=1

这样cmpxchgl %1, %2等价于:
cmpxchgl ignore2 futex
ignore2就是CAS里的新值N,N=1,futex是当前值V,但E又是什么呢?原来cmpxchgl使用了一个隐藏参数EAX代表E,前面已分析出来,EAX是ignore1,其值为0。则现在一切都清晰了,cmpxchgl检查futex(也就是__lock成员)是否为0(表示锁未占用),如是,赋值1(表示锁被占用),同时ZF标志位设置为1(ZF=1,JZ跳转,JNZ不跳转);否则(说明锁已被占用),ZF标志位为0,JNZ跳转。

归纳起来就是:先使用CAS判断_lock是否占用,若未占用,直接返回。否则,通过__lll_lock_wait_private调用SYS_futex系统调用迫使线程进入沉睡。
上述过程就是所谓的FUTEX同步机制,CAS是用户态的指令,若无竞争,简单修改锁状态即返回,非常高效,只有发现竞争,才通过系统调用陷入内核态。所以,FUTEX是一种用户态和内核态混合的同步机制,它保证了低竞争情况下的锁获取效率。

<think>我们正在讨论pthread_mutex_lock的实现原理和使用示例。根据提供的引用,我们可以总结出一些关键点。 首先,引用[1]提到pthread_mutex_lock是多线程编程中的一个重要函数,用于定互斥,确保同一时间只有一个线程可以访问临界区。引用[2]给出了一个简单的使用示例,展示了如何声明和初始化一个互斥,以及如何使用pthread_mutex_lock和pthread_mutex_unlock。引用[3]则通过一个实际的例子说明了在没有的情况下,多个线程竞争会导致计数错误,而使用后可以保证计数的正确性。 关于实现原理,由于pthread库是系统相关的,其具体实现可能因操作系统而异。在Linux系统中,pthread_mutex_lock通常是通过系统调用(如futex)来实现的。futex(快速用户空间互斥)是一种在用户空间和内核空间之间协作的机制,它通过在用户空间检查一个变量来避免不必要的系统调用,从而在无竞争的情况下提高效率。当发生竞争时,会通过系统调用让内核来挂起等待的线程。 使用示例: 1. 声明和初始化互斥:可以使用静态初始化(PTHREAD_MUTEX_INITIALIZER)或动态初始化(pthread_mutex_init)。 2. 在进入临界区之前调用pthread_mutex_lock加。 3. 在离开临界区之后调用pthread_mutex_unlock解。 下面我们给出一个简单的代码示例,展示如何使用pthread_mutex_lock来保护一个共享变量。 ```c #include <pthread.h> #include <stdio.h> // 静态初始化互斥 static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static int shared_counter = 0; void *thread_func(void *arg) { for (int i = 0; i < 10000; i++) { // 加 pthread_mutex_lock(&mutex); shared_counter++; // 解 pthread_mutex_unlock(&mutex); } return NULL; } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, thread_func, NULL); pthread_create(&t2, NULL, thread_func, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("shared_counter = %d\n", shared_counter); // 应该输出20000 return 0; } ``` 在这个例子中,两个线程分别对共享变量shared_counter增加10000次,由于使用了互斥,最终结果将是20000。如果没有互斥,结果可能会小于20000,因为两个线程可能会同时修改shared_counter导致部分更新丢失。 关于实现原理的更多细节,由于涉及操作系统内核,我们不会在这里深入展开,但可以简要说明: - 当线程调用pthread_mutex_lock时,它会尝试通过原子操作获取。如果当前没有被占用,那么线程将获得并继续执行。 - 如果已经被其他线程占用,那么当前线程会被阻塞,直到被释放。在底层,这通常通过futex系统调用来实现,将线程放入等待队列,并在释放时唤醒。 注意:使用互斥时需要注意避免死,例如确保加顺序一致,以及及时释放。 §§相关问题§§ 1. pthread_mutex_lock 和 pthread_mutex_trylock 有什么区别? 2. 在多线程编程中,除了互斥,还有哪些同步机制? 3. 如何避免使用互斥时出现死?</think>### pthread_mutex_lock 实现原理及使用示例 #### 一、实现原理 `pthread_mutex_lock` 是 POSIX 线程库中的互斥实现,其核心原理基于原子操作和系统调用: 1. **用户空间优化** 通过 **futex(快速用户空间互斥)** 机制实现[^1]: - 无竞争时:完全在用户空间通过原子操作(如 `cmpxchg`)获取,无需系统调用 - 有竞争时:通过 `futex(FUTEX_WAIT)` 系统调用让线程休眠,等待唤醒 2. **状态管理** 使用 32 位整型表示状态: - `0`:未定 - `1`:定(无等待线程) - `2`:定(有等待线程) 3. **加流程** ```c while (true) { if (原子操作设置状态为"定") 成功: 返回成功; else: futex(FUTEX_WAIT); // 线程休眠 } ``` 4. **解流程** ```c 原子操作设置状态为"未定"; if (有等待线程): futex(FUTEX_WAKE); // 唤醒一个等待线程 ``` #### 二、使用示例 ```c #include <pthread.h> #include <stdio.h> // 声明共享资源和互斥 static int counter = 0; static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化[^2] void* thread_func(void* arg) { for (int i = 0; i < 10000; i++) { // 加保护临界区 pthread_mutex_lock(&mutex); counter++; pthread_mutex_unlock(&mutex); } return NULL; } int main() { pthread_t t1, t2; // 创建两个线程 pthread_create(&t1, NULL, thread_func, NULL); pthread_create(&t2, NULL, thread_func, NULL); // 等待线程结束 pthread_join(t1, NULL); pthread_join(t2, NULL); printf("Final counter: %d\n", counter); // 正确输出 20000 return 0; } ``` #### 三、关键注意事项 1. **顺序** 多个使用时需固定加顺序,避免死[^1] 2. **临界区精简** 保持临界区代码尽可能短,例如: ```c // 正确示例(临界区仅含共享操作) pthread_mutex_lock(&mutex); shared_var++; pthread_mutex_unlock(&mutex); // 错误示例(含非共享操作) pthread_mutex_lock(&mutex); printf("Debug: %d", shared_var); // I/O操作会阻塞其他线程 shared_var++; pthread_mutex_unlock(&mutex); ``` 3. **错误处理** 检查返回值: ```c if (pthread_mutex_lock(&mutex) != 0) { perror("Lock failed"); // 错误处理 } ``` 4. **的初始化** - 静态初始化:`pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;` - 动态初始化:`pthread_mutex_init(&mutex, NULL);` #### 四、性能对比 引用[^3]中的测试结果: ``` 无时计数: 2935 // 因竞争导致数据丢失 有时计数: 3000 // 结果正确 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值