`
gstarwd
  • 浏览: 1536741 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

关于信号量与线程互斥锁的区别与实现

阅读更多
之前一直没有怎么关注过这个问题,前些日子在面试一家公司的时候,面试官提到了pthread_cond_wait/pthread_cond_signal的实现,当时答的不是很好,回来就查了nptl的代码。前天,水木上又有人问到了信号量和互斥锁的问题,我想还是对它们的区别与实现总结一下

首先了解一些信号量和线程互斥锁的语义上的区别:

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

援引CU上一篇帖子的内容:
“信 号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在sem_wait的时候,就阻塞在那 里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个 资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的”
也就是说,信号量不一定是锁定某一个资源,而是流 程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或 者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。

两者之间的区别:

作用域
信号量 : 进程间或线程间(linux仅线程间)
互斥锁 : 线程间

上锁时
信号量 : 只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait阻塞,直到sem_post释放后value值加一。一句话,信号量的value>=0
互斥锁 : 只要被锁住,其他任何线程都不可以访问被保护的资源。如果没有锁,获得资源成功,否则进行阻塞等待资源可用。一句话,线程互斥锁的vlaue可以为负数

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

接下来,我们需要分析一下信号量和线程互斥锁的实现机制。

在Linux下,信号量和线程互斥锁的实现都是通过futex系统调用。

futex(快速用户区互斥的简称)是一个在Linux上实现锁定和构建高级抽象锁如信号量和POSIX互斥的基本工具。它们第一次出现在内核开发的2.5.7版;其语义在2.5.40固定下来,然后在2.6.x系列稳定版内核中出现。

Futex 是fast userspace mutex的缩写,意思是快速用户空间互斥体。Linux内核把它们作为快速的用户空间的锁和信号量的预制构件提供给开发者。Futex非常基础,借助其 自身的优异性能,构建更高级别的锁的抽象,如POSIX互斥体。大多数程序员并不需要直接使用Futex,它一般用来实现像NPTL这样的系统库。

Futex 由一块能够被多个进程共享的内存空间(一个对齐后的整型变量)组成;这个整型变量的值能够通过汇编语言调用CPU提供的原子操作指令来增加或减少,并且一 个进程可以等待直到那个值变成正数。Futex 的操作几乎全部在应用程序空间完成;只有当操作结果不一致从而需要仲裁时,才需要进入操作系统内核空间执行。这种机制允许使用 futex 的锁定原语有非常高的执行效率:由于绝大多数的操作并不需要在多个进程之间进行仲裁,所以绝大多数操作都可以在应用程序空间执行,而不需要使用(相对高代 价的)内核系统调用。

----------------------------------------------------------------
插播一段关于x86原子操作指令的说明:

cmpxchg 比较交换指令,其语义为:
int CompareAndExchange(int *ptr, int old, int new)
{
int actual = *ptr;
if (actual == old)
*ptr = new;
return actual;
}

Intel白皮书上的说明如下:
(* Accumulator = AL, AX, EAX, or RAX depending on whether a byte, word, doubleword, or
quadword comparison is being performed *)
IF accumulator = DEST
    THEN
        ZF ← 1;
        DEST ← SRC;
    ELSE
        ZF ← 0;
        accumulator ← DEST;
FI;

使用此原子操作可以实现自旋锁,之前有一篇文章中描述了实现:
void lock(lock_t *lock) {
while (CompareAndExchange(&lock->flag, 0, 1) == 1)
; // spin
}
void unlock(lock_t *lock) {
lock->flag = 0;
}

关于smp下的原子操作的一些说明:
  原子操作是不可分割的,在执行完毕不会被任何其它任务或事件中断。在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为 是" 原子操作",因为中断只能发生于指令之间。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源 互斥的原因。在对称多处理器(Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。
  在x86 平台上,CPU提供了在指令执行期间对总线加锁 的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK" ,经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
  当然,并不是所有的指令前面都可以加lock前缀的,只有ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG,DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, 和 XCHG指令前面可以加lock指令,实现原子操作。
----------------------------------------------------------------

广告回来了,我们继续。

futex保存在用户空间的共享内存中,并且通过原子操作进行操作。在大部分情况下,资源不存在争用的情况下,进程或者线程可以立刻获得资源成功,实际上就没有必要调用系统调用,陷入内核了。实际上,futex的作用就在于减少系统调用的次数,来提高系统的性能。

线程互斥锁pthread_mutex_t的实现原理:
pthread_mutex_lock:
atomic_dec(pthread_mutex_t.value);
if(pthread_mutex_t.value!=0)
   futex(WAIT)
else
   success

pthread_mutex_unlock:
atomic_inc(pthread_mutex_t.value);
if(pthread_mutex_t.value!=1)
   futex(WAKEUP)
else
   success
信号量sem_t的实现原理(直接从glibc/nptl/DESIGN-sem.txt中摘的):
sem_wait(sem_t *sem)
{
  for (;;) {

    if (atomic_decrement_if_positive(sem->count))
      break;

    futex_wait(&sem->count, 0)
  }
}

sem_post(sem_t *sem)
{
  n = atomic_increment(sem->count);
  // Pass the new value of sem->count
  futex_wake(&sem->count, n + 1);
}

对 比,pthread_mutex_unlock()和sem_post()的实现,我们发现一个不同点,sem_post()无论如何都会调用 futex_wake(),进行系统调用。但是pthread_mutex_unlock()却符合futex的初衷,只有在需要仲裁的时候才调用 futex_wake()。那么什么是仲裁条件呢?

前面说过信号量和线程互斥锁语义上的区别在于信号量的value>=0,而线程互斥锁的value可以为负数。
对于lock操作,这两个倒是没有多少差别。信号量只要value>0就可以获得资源,线程互斥锁需要value=1。
但 是对于unlock操作,这两个就有一些差别了。信号量和线程互斥锁,都会增加对应的value。如果加1后,value为1,对于线程互斥锁来讲,实际 上表明资源可用,并且之前没有其他的线程在等待这个资源;否则说明还有其他线程在等待这个资源,需要调用futex系统调用唤醒它们。但是对于信号量,由于value必须>=0。那么加1后,即使value为1,也无法判定现在没有其他的进程或线程正在等待资源,所以必须调用futex系统调用。 例如:
#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>

sem_t sem_a;
void *task1();

int main(void)
{
 int ret=0;
 pthread_t thrd1;
 pthread_t thrd2;
 sem_init(&sem_a,0,1);
 ret=pthread_create(&thrd1,NULL,task1,NULL); //创建子线程
 ret=pthread_create(&thrd2,NULL,task1,NULL); //创建子线程
 pthread_join(thrd1,NULL); //等待子线程结束
 pthread_join(thrd2,NULL); //等待子线程结束
}

void *task1()
{
  int sval = 0;
  sem_wait(&sem_a); //持有信号量
  sleep(5); //do_nothing
  sem_getvalue(&sem_a,&sval);
  printf("sem value = %d\n",sval);
  sem_post(&sem_a); //释放信号量
}
上面sem的value初始化为1,但是有两个线程争用资源。那么第一个线程获得资源成功,当它unlock的时候,sem的value变为1。但是,这个时候,实际上还有一个线程在等待资源。因此,必须要进行futex_wake()系统调用,唤醒等待资源的线程。

感兴趣的同学可以使用strace跟踪一下,进行验证。要注意忽略程序运行初始化的那个futex_wake ;-)

参考:
http://www.eetop.cn/blog/html/04/343504-14125.html
http://javadino.blog.sohu.com/99256728.html
http://javadino.blog.sohu.com/99256835.html
http://javadino.blog.sohu.com/99256921.html
分享到:
评论

相关推荐

    Linux互斥锁、条件变量和信号量

    在多线程编程中,确保线程安全是至关重要的,特别是在Linux系统中,为了管理共享资源,Linux提供了互斥锁、条件变量和信号量这三种同步机制。它们都是用于协调多个线程对共享数据的访问,防止数据不一致性和竞态条件...

    信号量与互斥锁示例代码

    信号量和互斥锁是操作系统中用于进程同步和资源管理的重要机制,它们在多线程编程和并发控制中发挥着至关重要的作用。本示例代码来源于《深入理解计算机系统》这本书,通过具体代码来帮助读者深入理解这两种机制的...

    信号量、互斥体和自旋锁的区别

    - **二值信号量**通常用于实现互斥访问,即同一时刻只允许一个进程(或线程)访问某一资源。它实际上是一种简单的互斥锁实现方式,其内部维护了一个整数值,当该值为1时,表示资源未被占用;当值为0时,表示资源已被...

    C语言并发控制:信号量与互斥锁的实现与应用

    ### C语言并发控制:信号量与互斥锁的实现与应用 #### 一、引言 C语言作为一种经典的编程语言,自20世纪70年代初由丹尼斯·里奇在贝尔实验室开发以来,就因其高效性、灵活性以及强大的硬件控制能力而受到广泛欢迎...

    多线程互斥锁和条件变量demo

    在“多线程互斥锁demo”中,你可能会看到以下关键步骤: 1. 初始化互斥锁:使用pthread_mutex_init()函数初始化一个互斥锁,设置其属性。通常,无需特殊属性,直接使用默认值即可。 2. 加锁与解锁:在访问共享资源...

    多线程 教程 各种锁 半成品的CAS 临界区 信号量 事件 互斥锁 队列

    本教程将深入探讨多线程相关的知识点,包括各种锁机制、条件变量、半成品的CAS操作、临界区、信号量、事件、互斥锁以及队列。 **多线程**:多线程是操作系统提供的一种并发执行机制,一个进程可以包含多个线程,每...

    3.线程间同步和通信之互斥锁(静态)

    在STM32平台上,RT-thread提供了一套完善的线程管理、信号量和互斥锁等同步机制。RT-thread中的互斥锁API包括`rt_mutex_init`、`rt_mutex_take`、`rt_mutex_release`和`rt_mutex_destroy`等函数。 1. `rt_mutex_...

    使用信号量和关键段实现多线程的同步与互斥

    通过以上知识点,我们可以理解如何使用C++中的信号量和关键段来解决读者、写者问题,实现不同数量的读者和写者之间的同步与互斥。项目中的`ReaderWriter`文件可能包含了具体的代码实现,包括线程创建、信号量初始化...

    互斥锁与事件锁

    在多线程编程中,互斥锁(Mutex)和事件锁(Event)是两种常见的同步机制,用于确保多个线程安全地访问共享资源。本文将深入探讨这两种锁的工作原理、使用场景及其优缺点。 首先,互斥锁是线程同步的基础工具之一。...

    互斥锁、条件变量、信号量总结

    互斥锁、条件变量和信号量是操作系统中用于线程同步和资源管理的重要工具,尤其在多线程和多进程编程中发挥着关键作用。这些机制确保了共享资源的有序访问,防止数据竞争和死锁等问题的发生。 首先,互斥锁(Mutex...

    C语言信号量同步与互斥生产者消费者互斥锁读写者问题哲学家就餐问题课程设计

    C语言信号量同步与互斥生产者消费者互斥锁读写者问题哲学家就餐问题课程设计 本课程设计旨在通过C语言编程来解决信号量机制下的同步、互斥、生产者消费者问题、哲学家就餐问题和读写者问题。 一、信号量机制 信号...

    互斥锁示例代码

    除了互斥锁,Linux还提供了信号量(Semaphore)、条件变量(Condition Variable)等同步机制,可以根据不同的应用场景选择合适的方法。 总之,互斥锁是保证多线程并发环境下数据安全的重要工具,通过理解其原理和...

    linux和win32下通用的互斥锁Mutex封装

    互斥锁是一种二状态信号量,状态为锁定或未锁定。当一个线程获得锁后,其他试图获取该锁的线程将被阻塞,直到锁被释放。这样就保证了对共享资源的独占访问,避免了数据竞争问题。在Linux中,互斥锁通常通过pthread_...

    用有名信号量和匿名信号量实现进程互斥

    信号量是一种在多进程或多线程环境下控制资源访问的重要同步机制,主要用来解决进程间的互斥问题。在本文中,我们将深入探讨如何使用有名信号量和匿名信号量来实现进程互斥,以及相关的C语言函数。 首先,信号量...

    线程互斥和同步代码样本滴水三期信号量布置的作业

    信号量分为两种:二进制信号量(计数值只能为0或1,类似于互斥锁)和计数信号量(计数值可以大于1,允许多个线程同时访问)。 在滴水三期的作业中,你可能需要编写代码来演示如何使用信号量实现线程的同步。这可能...

    linux上实现多进程和多线程实现同步互斥(源代码)

    同步和互斥在多进程中主要通过信号量和消息队列等机制实现。 二、多线程 多线程则是在一个进程中创建多个执行流,它们共享同一块内存空间,资源利用率高,但数据同步和互斥问题更复杂。在Linux中,可以使用`...

    linux无亲缘关系间进程同步通信实现(互斥锁+条件变量+共享内存模式)

    它与互斥锁配合使用,可以在等待某个条件时,释放锁并进入休眠状态,条件改变时,由其他进程唤醒。在C语言中,`pthread_cond_t`表示条件变量,`pthread_cond_init`初始化,`pthread_cond_wait`等待,`pthread_cond_...

    windowsC++多线程加锁信号量共享内存

    与互斥锁不同,信号量允许一定数量的线程同时访问资源。`std:: sempahore`或`std::binary_semaphore`(C++20引入)是C++中的实现。当资源数量减少到0时,进一步尝试获取资源的线程会被阻塞,直到其他线程释放资源。 ...

    Linux互斥锁、条件变量和信号量[归类].pdf

    在 Linux 多线程编程中,为了防止多个线程同时访问共享资源,Linux 提供了互斥锁、条件变量和信号量来保护共享资源。 一、互斥锁 互斥锁是一种信号量,常用来防止两个进程或线程在同一时刻访问相同的共享资源。...

Global site tag (gtag.js) - Google Analytics