自旋锁、互斥体和信号量

版权声明:转载标明出处 https://blog.csdn.net/qq_38289815/article/details/82979107

自旋锁

Linux内核中最常见的锁是自旋锁(spin lock)。自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被已经持有的自旋锁,那么该线程就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被争用,请求锁的执行线程便能立刻得到它,继续执行。在任意时间,自旋锁都可以防止多于一个的执行线程同时进入临界区。同一个锁可以用在多个位置。例如,对于给定数据的所有访问都可以得到保护和同步。

自旋锁相当于上厕所时,在门外等待的过程。如果你到在厕所门外,发现里面没有人,就可以推开门进入厕所。如果你到了厕所门口发现门是关着的(里面有人),就必须在门口等待(此时你很着急),不断地检查厕所是否为空。当厕所为空时,你就可以进入了。正是因为有了门(相当于自旋锁),才允许一次只有一个人(相当于执行线程)进入厕所里(相当于临界区)。

一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋(特别浪费处理器时间),这种行为是自旋锁的要点。所以自旋锁不应该被长时间的持有。事实上,这点正是使用自旋锁的初衷:在段期间内进行轻量级加锁。还可以采取另外的方式来处理对锁的争用:让请求线程睡眠,直到锁重新可用时再唤醒它。这样处理器就不必循环等待,可以去执行其他代码。这也会带来一定的开销——这里有两次明显的上下文切换,被阻塞的线程要换出和换入,与实现自旋锁的少数几行代码相比,上下文切换当然有较多的代码。因此,持有自旋锁的时间最好小于完成两次上下文切换的耗时。当然我们大多数人都不会无聊到去测量上下文切换的耗时,所以我们让持有自旋锁的时间应尽可能的短就可以了。

自旋锁的实现和体系结构密切相关,代码往往通过汇编实现。这些与体系结构相关的代码定义在文件<asm/spinlock.h>中,实际需要用到的接口定义在文件<linux/spinlock.h>中。本文参考的书籍是Linux内核设计与实现,其讨论的是2.6.34内核版本。自旋锁的基本使用方式如下:

DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/*临界区...*/
spin_unlock(&mr_lock);

因为自旋锁在同一时刻最多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区内,这就为多处理器机器提供了防止并发访问所需的保护机制。注意:在单处理器机器上,编译的时候并不会加入自旋锁。它仅仅被当做一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁会被完全剔除出内核。

注意:自旋锁是不可递归的

Linux内核实现的自旋锁是不可递归的,这点不同于自旋锁在其他操作系统中的实现。所以如果你试图得到一个你正持有的锁,你必须自旋,等待你自己释放这个锁。由于你处于自旋忙等待,所以你永远没有机会释放锁,于是你被自己锁死了。

自旋锁可能带来的问题

(1)死锁。试图递归地获得自旋锁必然会引起死锁:例如递归程序的持有实例在第二个实例循环,以试图获得相同自旋锁时,就不会释放此自旋锁。所以,在递归程序中使用自旋锁应遵守下列策略:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。此外如果一个进程已经将资源锁定,那么,即使其它申请这个资源的进程不停地疯狂“自旋”,也无法获得资源,从而进入死循环。
(2)过多占用CPU资源。如果不加限制,由于申请者一直在循环等待,因此自旋锁在锁定的时候,如果不成功,不会睡眠,会持续的尝试,单cpu的时候自旋锁会让其它process动不了。因此,一般自旋锁实现会有一个参数限定最多持续尝试次数。超出后,自旋锁放弃当前time slice,等下一次机会。

自旋锁的操作

spin_lock_init():可以使用该方法来初始化动态创建的自旋锁(此时你只有一个指向spinlock_t类型的指针,没有它的实体)。

spin_try_lock():试图获得某个特定的自旋锁,如果该锁已经被争用,那么该方法会立即返回一个非0值,而不会自旋等待锁被释放;如果成功地获得了这个自旋锁,该函数返回0.同理,spin_is_locked()方法用于检查特定的锁当前是否已被占用,如果被占用,返回非0值;否则返回0。该方法只做判断,并不实际占用。

标准的自旋锁操作的完整列表:

spin_lock_init(lock)

初始化自旋锁,将自旋锁设置为1,表示有一个资源可用。

spin_is_locked(lock)

如果自旋锁被置为1(未锁),返回0,否则返回1。

spin_unlock_wait(lock)

等待直到自旋锁解锁(为1),返回0;否则返回1。

spin_trylock(lock)

尝试锁上自旋锁(置0),如果原来锁的值为1,返回1,否则返回0。

spin_lock(lock)

循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。

spin_unlock(lock)

将自旋锁解锁(置为1)。

spin_lock_irqsave(lock, flags)

循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关中断,将状态寄存器值存入flags。

spin_unlock_irqrestore(lock, flags)

将自旋锁解锁(置为1)。开中断,将状态寄存器值从flags存入状态寄存器。

spin_lock_irq(lock)

循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关中断。

spin_unlock_irq(lock)

将自旋锁解锁(置为1)。开中断。

spin_unlock_bh(lock)

将自旋锁解锁(置为1)。开启底半部的执行。

spin_lock_bh(lock)

循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。阻止软中断的底半部的执行。

spin_lock和spin_lock_irq的区别

(1)spin_lock
spin_lock 的实现关系为:spin_lock -> raw_spin_lock -> _raw_spin_lock -> __raw_spin_lock ,而__raw_spin_lock 的实现为:

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    preempt_disable();
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

(2)spin_lock_irq

spin_lock_irq 的实现关系为:spin_lock_irq -> raw_spin_lock_irq -> _raw_spin_lock_irq -> __raw_spin_lock_irq,而__raw_spin_lock_irq 的实现为:

static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
    local_irq_disable();
    preempt_disable();
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

注意“preempt_disable()”,这个调用的功能是“关抢占”(在spin_unlock中会重新开启抢占功能)。从中可以看出,使用自旋锁保护的区域是工作在非抢占的状态;即使获取不到锁,在“自旋”状态也是禁止抢占的。了解到这,我想咱们应该能够理解为何自旋锁保护 的代码不能睡眠了。试想一下,如果在自旋锁保护的代码中间睡眠,此时发生进程调度,则可能另外一个进程会再次调用spinlock保护的这段代码。而我们 现在知道了即使在获取不到锁的“自旋”状态,也是禁止抢占的,而“自旋”又是动态的,不会再睡眠了,也就是说在这个处理器上不会再有进程调度发生了,那么死锁自然就发生了。

由此可见,这两者之间只有一个差别:是否调用local_irq_disable()函数,即是否禁止本地中断。这两者的区别可以总结为:在任何情况下使用spin_lock_irq都是安全的。因为它既禁止本地中断,又禁止内核抢占。spin_lock比spin_lock_irq速度快,但是它并不是任何情况下都是安全的。

举例来说明:进程A中调用了spin_lock(&lock)然后进入临界区,此时来了一个中断(interrupt),该中断也运行在和进程A相同的CPU上,并且在该中断处理程序中恰巧也会spin_lock(&lock)试图获取同一个锁。由于是在同一个CPU上被中断,进程A会被设置为TASK_INTERRUPT状态,中断处理程序无法获得锁,会不停的忙等,由于进程A被设置为中断状态,schedule()进程调度就无法再调度进程A运行,这样就导致了死锁!但是如果该中断处理程序运行在不同的CPU上就不会触发死锁。因为在不同的CPU上出现中断不会导致进程A的状态被设为TASK_INTERRUPT,只是换出。当中断处理程序忙等被换出后,进程A还是有机会获得CPU,执行并退出临界区。所以在使用spin_lock时要明确知道该锁不会在中断处理程序中使用。

自旋锁可以使用在中断处理程序中(此处不能使用信号量,因为它们会导致睡眠)。在中断处理程序中使用自旋锁时,一定要在获取锁之前,先禁止本地中断(在当前处理器上的中断请求),否则,中断处理程序会打断正在持有的锁的内核代码,有可能会试图去争用这个已经被持有的自旋锁。这样一来,中断处理程序就会自旋,等待该锁重新可用,但是锁的持有者在这个中断处理程序执行完毕前不可能运行(双重请求死锁)。注意,需要关闭的只是当前处理器上的中断。如果中断发生在不同的处理器上,即使中断处理程序在同一锁上自旋,也不会妨碍锁的持有者(在不同处理器上)最终释放锁。

内核提供的禁止中断同时请求锁的接口spin_lock_irqsave的实现关系spin_lock_irqsave------>__raw_spin_lock_irqsave

static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
{
    unsigned long flags;
    local_irq_save(flags);
    preempt_disable();
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    /*
     * On lockdep we dont want the hand-coded irq-enable of
     * do_raw_spin_lock_flags() code, because lockdep assumes
     * that interrupts are not re-enabled during lock-acquire:
     */
#ifdef CONFIG_LOCKDEP
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
#else
    do_raw_spin_lock_flags(lock, &flags);
#endif
    return flags;
}

使用方法:

DEFINE_SPINLOCK(mr_lock);
unsigned long flags;
spin_lock_irqsave(&mr_lock, flags);
/*临界区*/
spin_unlock_irqrestore(&mr_lock, flags);

函数spin_lock_irqsave()保存中断的当前状态,并禁止本地中断,所以再去获取指定的锁。反过来spin_unlock_irqrestore()对指定的锁解锁,然后让中断恢复到加锁前的状态。所以即使中断最初是被禁止的,代码也不会错误地激活它们,相反,会继续让它们禁止。注意,flags变量看起来像是由数值传递的,这是因为这些锁函数有些部分是通过宏的方式实现的。在单处理器系统上,虽然在编译时抛弃掉了锁机制,但在上面的例子中仍需要关闭中断,以禁止中断处理程序访问共享数据。加锁和解锁分别可以禁止和允许内核抢占。

由此可见,自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。

自旋锁为什么广泛用于内核

自旋锁是一种轻量级的互斥锁,可以更高效的对互斥资源进行保护。自旋锁本来就只是一个很简单的同步机制,在SMP之前根本就没这个东西,一切都是Event之类的同步机制,这类同步机制都有一个共性就是:一旦资源被占用都会产生任务切换,任务切换涉及很多东西的(保存原来的上下文,按调度算法选择新的任务,恢复新任务的上下文,还有就是要修改cr3寄存器会导致cache失效)这些都是需要大量时间的,因此用Event之类来同步一旦涉及到阻塞代价是十分昂贵的,而自旋锁的效率就远高于互斥锁。

总结自旋锁在不同CPU下工作的特点:

(1)单CPU非抢占内核下:自旋锁会在编译时被忽略(因为单CPU且非抢占模式情况下,不可能发生进程切换,时钟只有一个进程处于临界区(自旋锁实际没什么用了)。

(2)单CPU抢占内核下:自选锁仅仅当作一个设置抢占的开关(因为单CPU不可能有并发访问临界区的情况,禁止抢占就可以保证临街区唯一被拥有)。

(3)多CPU下:此时才能完全发挥自旋锁的作用,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。

 

POSIX提供的与自旋锁相关的函数

使用自旋锁时要注意:由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在哪里自旋,这就会浪费CPU时间。

持有自旋锁的线程在sleep之前应该释放自旋锁以便其他咸亨可以获得该自旋锁。内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起。(下面会解释)使用任何锁都需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:1.建立锁所需要的资源2.当线程被阻塞时所需要的资源

int pthread_spin_init(pthread_spinlock_t*lock,int pshared);

初始化spin lock,当线程使用该函数初始化一个未初始化或者被destroy过的spin lock有效。该函数会为spin lock申请资源并且初始化spin lock为unlocked状态。有关第二个选项是这么说的:If the Thread Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_SHARED,the implementation shall permit the spin lock to be operated upon by any thread that has access to the memory where the spin lock is allocated,even if it is allocated in memory that is shared by multiple processes.If the Thread Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_PRIVATE,or if the option is not supported,the spin lock shall only be operated upon by threads created within the same process as the thread that initialized the spin lock.If threads of differing processes attempt to operate on such a spin lock,the behav‐ior is undefined.

所以,如果初始化spin lock的线程设置第二个参数为PTHREAD_PROCESS_SHARED,那么该spin lock不仅被初始化线程所在的进程中所有线程看到,而且可以被其他进程中的线程看到,PTHREAD_PROESS_PRIVATE则只被同一进程中线程看到。如果不设置该参数,默认为后者。

int pthread_spin_destroy(pthread_spinlock_t*lock); 销毁spin lock,作用和mutex的相关函数类似。
int pthread_spin_lock(pthread_spinlock_t*lock); 加锁函数,不过这么一点值得注意:EBUSY A thread currently holds the lock。These functions shall not return an error code of[EINTR].
int pthread_spin_trylock(pthread_spinlock_t*lock); 试图获取指定的锁
int pthread_spin_unlock(pthread_spinlock_t*lock); 解锁函数。不是持有锁的线程调用或者解锁一个没有lock的spin lock这样的行为都是undefined的。

 

信号量

Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个不可用(已经被占用)的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用(被释放)后,处于等待队列中的那个任务将被唤醒,并获得该信号量。例如:当某个人到了门前(钥匙在门外,进去房间的人持有钥匙),此时房间(临界区)里没有人,于是他就进入房间并关上了门。最大的差异在于当另外一个人想进入房间,但无法进入时,这家伙不是在徘徊,而是把自己的名字写在一个列表中,然后去打盹。当里面的人离开房间(释放钥匙)时,就在门口查看一下列表。如果列表上有名字,他就对第一个名字仔细检查,并叫醒那个人让他进入房间,在这种方式中,钥匙(相当于信号量)确保一次只有一个人(相当于执行线程)进入房间(临界区)。这就比自旋锁提供了更好的处理器利用率,因为没有把时间花费在忙等待上,但是,信号量比自旋锁有更大的开销。可以从信号量的睡眠特性中得出以下结论:

(1)由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况。 

(2)如果锁被短时间持有时,此时不建议使用信号量。因为睡眠、维护、等待队列以及唤醒等操作,其所花费的开销可能要比锁持有的全部时间还要长。

(3)由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中是不能进行调度的。

(4)可以在持有信号量时去睡眠,因为当其他进程试图获得同一信号量时不会因此而死锁(因为该进程也只是去睡眠而已,而前一个进程最终会继续执行)

(5)占用信号量的同时不能占用自旋锁。因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。

在使用信号量的大多数时候,选择余地并不大。往往在需要和用户空间同步时,当代码需要睡眠,此时使用信号量是唯一的选择。由于不受睡眠的限制,使用信号量通常来说更加容易一些。信号量不同于自旋锁,它不会禁止内核抢占,所以持有信号量的代码可以被抢占。这意味着信号量不会对调度的等待时间带来负面影响。

用户抢占在以下情况下产生:
从系统调用返回用户空间
从中断处理程序返回用户空间
内核抢占会发生在:
当从中断处理程序返回内核空间的时候,且当时内核具有可抢占性
当内核代码再一次具有可抢占性的时候(如:spin_unlock时)
如果内核中的任务显示的调用schedule()

计数信号量和二值信号量

信号量同时允许的持有者数量可以在声明信号量时指定。这个值成为使用者数量或简单的叫数量。通常情况下,信号量和自旋锁一样,在一个时刻仅允许有一个锁的持有者。这时计数等于1,这样的信号量被称为二值信号量(因为它或者由一个任务特有,或者根本没有任务持有它)或者称为互斥信号量(因为它强制进行互斥)。另一方面,初始化时也可以把数量设置为大于1的非0值。这种情况,信号量被称为计数信号量,它允许在一个时刻最多有count个锁持有者。计数信号量不能用来进行强制互斥,因为它允许多个执行线程同时访问临界区。相反,这种信号量用来对特定代码加以限制,内核中使用它的机会不多。在使用信号量时,基本上用到的都是互斥信号量(计数等于1的信号量)。

信号量是一种常见的锁机制,它支持两个原子操作P()和V(),后来的系统把这两个操作分别叫做down()和up(),Linux也遵从这种叫法。down()操作通过对信号量计数减1来请求获得一个信号量。如果结果是0或者大于0,获得信号量锁,任务就可以进入临界区。如果结果是负数,任务会被放入等待队列,处理器执行其他任务。该函数如同一个动词,降低(down)一个信号量就等于获取该信号量。相反,当临界区中的操作完成后,up()操作用来释放信号量,该操作也被称作提升信号量,因为它会增加信号量的计数值。如果在该信号量上的等待队列不为空,那么处于队列中等待的任务在被唤醒的同时会获得该信号量。

创建信号量和初始化信号量

信号量的实现是与体系结构相关的,具体实现定义在文件<asm/semaphore.h>中。struct semaphore类型用来表示信号量。可以通过以下方式静态地声明信号量——其中name是信号量变量名,count是信号量的使用数量:

struct semaphore name;
sema_init(&name, count);

创建更为普通的互斥信号量可以使用以下快捷方式:

static DECLARE_MUTEX(name);//Linux 2.6.36以后,将#define DECLARE_MUTEX(name)改成了#define DEFINE_SEMAPHORE(name)

更常见的情况是,信号量作为一个数据结构的一部分动态创建。此时,只有指向该动态创建的信号量的间接指针,可以使用如下函数来对其进行初始化:

sema_init(sem, count);   //sem是指针,count是信号量的使用者数量。

与前面情况类似,初始化一个动态创建的互斥信号量时,使用如下函数:

init_MUTEX(sem);

 

互斥体(互斥锁)

内核中唯一允许睡眠的锁是信号量。多数用户使用信号量只使用计数1,说白了是把其作为一个互斥的排他锁使用。信号量的用途更通用,没有多少使用限制。这点使得信号量适合用于那些较为复杂的、未明情况下的互斥访问,比如内核与用户空间复杂的交互行为。但这也意味着简单的锁定而使用信号量并不方便,并且信号量也缺乏强制的规则来行使任何形式的自动调试,即便受限的调试也不可能。为了找到一个更简单睡眠锁,内核开发者们引入了互斥体(mutex)。互斥体这个称谓所指的是任何可以睡眠的强制互斥锁,比如使用计数是1的信号量。但是在Linux内核2.6.34中,互斥体这个称谓也用于一种实现互斥的特定睡眠锁。也就是说,互斥体是一种互斥信号。

mutex在内核中对应数据结构mutex,其行为和使用计数为1的信号量类似,但操作接口更简单,实现也更为高效,而且使用限制更强。静态定义mutex,你需要做:

DEFINE_MUTEX(name);
动态初始化mutex:
mutex_init(&mutex);

对互斥锁加锁和解锁:

mutex_lock(&mutex);
/*临界区*/
mutex_unlock(&mutex);

Mutex方法:

mutex_lock(struct mutex*)       为指定的mutex上锁,如果锁不可用则睡眠
mutex_unlock(struct mutex*)     为指定的mutex解锁
mutex_trylock(struct mutex*)    试图获取指定的mutex,成功返回1;否则,返回0
mutex_is_lock(struct mutex*)    如果锁已被争用,则返回1;否则返回0

C语言的多线程编程中,互斥锁的初始化 

头文件:#include <pthread.h>

函数原型:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_init() 函数是以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性。如果参数attr为空,则使用默认的互斥锁属性,默认属性为快速互斥锁 。互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  初始化一个快速锁的宏定义
pthread_mutex_lock(&mutex);
/*中间代码*/
pthread_mutex_unlock(&mutex);
函数成功执行后,互斥锁被初始化为未锁住态。

互斥锁例子:

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>

/*全局变量*/
int sum = 0;
/*互斥量 */
pthread_mutex_t mutex;
/*声明线程运行服务程序*/
void* pthread_function1 (void*);
void* pthread_function2 (void*);

int main (void)
{
    /*线程的标识符*/
    pthread_t pt_1 = 0;
    pthread_t pt_2 = 0;
    int ret = 0;
    /*互斥初始化*/
    pthread_mutex_init (&mutex, NULL);
    /*分别创建线程1、2*/
    ret = pthread_create( &pt_1,                  //线程标识符指针
                           NULL,                  //默认属性
                           pthread_function1,     //运行函数
                           NULL);                 //无参数
    if (ret != 0)
    {
        perror ("pthread_1_create");
    }

    ret = pthread_create( &pt_2,                  //线程标识符指针
                          NULL,                   //默认属性
                          pthread_function2,      //运行函数
                          NULL);                  //无参数
    if (ret != 0)
    {
        perror ("pthread_2_create");
    }
    /*等待线程1、2的结束*/
    pthread_join (pt_1, NULL);
    pthread_join (pt_2, NULL);

    printf ("main programme exit!\n");
    return 0;
}

/*线程1的服务程序*/
void* pthread_function1 (void*a)
{
    int i = 0;
    printf ("This is pthread_1!\n");
    for( i=0; i<3; i++ )
    {
        pthread_mutex_lock(&mutex); /*获取互斥锁*/
        /*注意,这里以防线程的抢占,以造成一个线程在另一个线程sleep时多次访问互斥资源,所以sleep要在得到互斥锁后调用*/
        sleep (1);
        /*临界资源*/
        sum++;
        printf ("Thread_1 add one to num:%d\n",sum);
        pthread_mutex_unlock(&mutex); /*释放互斥锁*/
    }
    pthread_exit ( NULL );
}

/*线程2的服务程序*/
void* pthread_function2 (void*a)
{
    int i = 0;
    printf ("This is pthread_2!\n");
    for( i=0; i<5; i++ )
    {
        pthread_mutex_lock(&mutex); /*获取互斥锁*/
        /*注意,这里以防线程的抢占,以造成一个线程在另一个线程sleep时多次访问互斥资源,所以sleep要在得到互斥锁后调用*/
        sleep (1);
        /*临界资源*/
        sum++;
        printf ("Thread_2 add one to num:%d\n",sum);
        pthread_mutex_unlock(&mutex); /*释放互斥锁*/
    }
    pthread_exit ( NULL );
}

mutex的简洁性和高效性源于相比使用信号量更多的受限性。它不同于信号量,其使用场景相对而言更严格、更定向了。

(1)任何时刻中只有一个任务可以持有mutex,也就是说,mutex的使用计数永远是1。

(2)给mutex上锁者必须负责给其再解锁——你不能再上下文中锁定一个mutex,而在另一个上下文中给它解锁。这个限制使得mutex不适合内核同用户空间复杂的同步场景。最常使用的方式是:在同一个上下文中上锁和解锁。

(3)递归地上锁和解锁是不允许的。也就是说,你不能递归地持有同一个锁,同样你也不能再去解一个已经解开的mutex。

(4)当持有一个mutex时,进程不可以退出。

(5)mutex不能再中断或者下半部中使用,即使使用mutex_trylock()也不行。整个中断处理流程被分为两个部分,中断处理程序为上半部。而下半部的任务是执行与中断处理密切相关但中断处理程序本身不执行的工作。

(6)mutex只能通过官方API管理:它不可被拷贝、手动初始化或者重复初始化。

从实现原理上来讲,Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过 pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞(blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。

 

信号量和互斥体

互斥体和信号量很相似,内核中两者共存会令人混淆。所幸,它们的标准使用方式都有简单的规范:除非mutex的某个约束妨碍你使用,否则相比信号量要优先使用mutex。如果你所写的是很底层的代码,才会需要使用信号量。如果发现不能满足其约束条件,且没有其他别的选择时,再考虑选择信号量。

自旋锁和互斥体

对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。
对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用 时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。
因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。还有一点是在中断上下文中只能使用自旋锁,而在任务睡眠时只能使用互斥体。

需求                          建议的加锁方式
低开销加锁                     优先使用自旋锁
短期锁定                       优先使用自旋锁
长期加锁                       优先使用互斥体
中断上下文中加锁                使用自旋锁
持有锁需要睡眠                  使用互斥体

 

参考:

《Linux内核设计与实现》

https://www.cnblogs.com/kuliuheng/p/4064680.html

https://www.cnblogs.com/aaronLinux/p/5890924.html

https://blog.csdn.net/wh_19910525/article/details/11536279

https://blog.csdn.net/freeelinux/article/details/53695111

猜你喜欢

转载自blog.csdn.net/qq_38289815/article/details/82979107
今日推荐