自旋锁和互斥锁 获取多久为合适

自旋锁和互斥锁是多线程编程中的两个重要概念。他们都能用来锁定一些共享资源,以阻止影响数据一致性的并发访问。但是他们之间确实存在区别,那么这些区别是什么?

理论上,当一个线程试图获取一个被锁定的互斥锁时,该操作会失败然后该线程会进入睡眠,这样就能马上让另一个线程运行。当持有互斥锁的线程释放该锁之后,进入睡眠状态的线程就会被唤醒。但是,当一个线程试图获取一个自旋锁而没有成功时,该线程会不断地重试,直到最终成功为止;因此该线程不会将运行权交到其他线程手中(当然,一旦当前线程的时间片超时,操作系统会强行切换到另一个线程)。

互斥锁的问题在于:让线程睡眠和唤醒线程都是极为耗时的操作,完成这些操作需要大量CPU指令,因此也就需要耗费不少时间。如果只是锁定互斥锁很短一段时间,那么让线程睡眠和唤醒线程所花的时间可能会超过线程实际上睡眠的时间,甚至有可能会超过线程在自旋锁上轮询锁浪费的时间(如果使用自旋锁)。另一方面,在自旋锁上进行轮询会浪费CPU时间,如果自旋锁被锁定较长的时间,可能会浪费大量的CPU时间,这时让线程睡眠可能是一个更好的选择。

在一个单核系统中使用自旋锁是行不通的,因为只要自旋锁轮询在阻塞当前CPU,那么就没有其他线程能够运行,既然没有其他线程能够运行,那么该锁也就不会被唤醒,对,我们进入死锁了。最好情况下,自旋锁仅仅浪费那些对系统没有任何用处的CPU时间。相反,如果使用互斥锁,线程A进入睡眠,那么另外一个线程B就能够立即运行,线程B有可能会释放锁,唤醒线程A,使线程A继续运行。

在一个多核系统,如果大量的锁只持有很短一段时间,那么让线程睡眠和唤醒线程所浪费的时间有可能会极大地降低运行时性能。相反,如果使用自旋锁,线程就有机会利用完全时间片(总是阻塞很短一段时间,然后立即运行),获得更高的吞吐量。

由于大部分情况下,程序员不能预先知道使用互斥锁好还是使用自旋锁好(例如:因为不知道目标系统的CPU核心数量),同时操作系统也不知道某个片段的代码是否已经为单核或多核环境优化过,因此大部分系统不严格区分这两种锁。实际上,大部分现代操作系统都提供混合互斥锁和混合自旋锁。那么,什么是混合互斥锁和混合自旋锁?

在一个多核系统,混合互斥锁开始时会表现得像自旋锁。即如果一个线程A不能获取到互斥锁,那么线程A不会立即进入睡眠状态,因为该锁可能马上就被释放了,因此该互斥锁开始表现得像自旋锁。只有当一段固定的时间后,线程A还不能获取到该互斥锁,线程A才会进入睡眠状态。如果相同的程序运行在单核系统下,该互斥锁就不会表现出自旋锁的行为。

一个混合自旋锁开始时会表现得像一个普通的自旋锁,但为了避免浪费CPU时间,它提供了一个back-off策略。通常,混合自旋锁不会使线程进入睡眠状态(因为当你使用自旋锁时,你不希望发生这种情况),但是它能停止某个线程(立即或者一段固定的时间后),然后让另一个线程运行,以提高自旋锁的闲置率(一个纯粹的线程切换通常比使线程进入睡眠然后唤醒它效率更高,起码目前如此)。

如果你不知道该使用哪一个,那么使用互斥锁,因为大部分现代操作系统都允许他们先自旋一小段时间(提前是该自旋有益), 所以互斥锁通常是更好的选择。有时,使用自旋锁会提升性能,在某些特定情况下,你可能会觉得使用自旋锁更好。这时候,使用你自己的锁对象,该锁对象内部使用自旋锁或者互斥锁实现(这个行为可以通过配置修改),开始时全部使用互斥锁,之后,如果你觉得某个地方使用自旋锁更好,那么修改它,然后比较下结果,但是在下结论之前,一定要记得在单核和多核环境下进行测试。

描述:此类型的互斥锁不会检测死锁。如果线程在不首先解除互斥锁的情况下尝试重新锁定该互斥锁,则会产生死锁。尝试解除由其他线程锁定的互斥锁会产生不确定的行为。如果尝试解除锁定的互斥锁未锁定,则会产生不确定的行为,当一个线程加锁以后,其余请求锁的线程将形成一个,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性

描述: 检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则PTHREAD_MUTEX_TIMED_NP类型动作相同。如果线程尝试解除锁定的互斥锁已经由其他线程锁定,则会返回错误。如果线程尝试解除锁定的互斥锁未锁定,则会返回错误。

描述: 嵌套锁(递归锁,可重入锁)与 PTHREAD_MUTEX_NORMAL 类型的互斥锁不同,对此类型互斥锁进行重新锁定时不会产生死锁情况。多次锁定互斥锁需要进行相同次数的解除锁定才可以释放该锁,然后其他线程才能获取该互斥锁。如果线程尝试解除锁定的互斥锁已经由其他线程锁定,则会返回错误。 如果线程尝试解除锁定的互斥锁未锁定,则会返回错误。

方式锁定此类型的互斥锁,则会产生不确定的行为。对于不是由调用线程锁定的此类型互斥锁,如果尝试对它解除锁定,则会产生不确定的行为。对于尚未锁定的此类型互斥锁,如果尝试对它解除锁定,也会产生不确定的行为。允许在实现中将该互斥锁映射到其他互斥锁类型之一。对于 Solaris 线程,PTHREAD_PROCESS_DEFAULT 会映射到 PTHREAD_PROCESS_NORMAL。

描述: 适应锁当线程尝试申请锁,会自动根据拥有锁的线程繁忙或sleep来选择是busy wait还是sleep。这种锁只有Solaris内核提供仅等待解锁后重新竞争。

pthread_mutex_trylock()三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁和适应锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。

 线程锁机制的实现都不是取消点,因此,延迟取消类型的线程不会因收到取消信号而离开加锁等待。值得注意的是,如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在,或者设置了异步取消类型,则必须在退出中解锁。

这个锁机制同时也不是异步信号安全的,也就是说,不应该在

过程中使用互斥锁,否则容易造成死锁。

互斥锁属性使用互斥锁(互斥)可以使线程按

。通常,互斥锁通过确保一次只有一个线程执行代码的临界段来同步多个线程。互斥锁还可以保护

要更改缺省的互斥锁属性,可以对属性对象进行声明和初始化。通常,互斥锁属性会设置在应用程序开头的某个位置,以便可以快速查找和轻松修改。

可以是进程专用的(进程内)变量,也可以是系统范围内的(进程间)变量。要在多个进程中的线程之间共享互斥锁,可以在

如果互斥锁的pshared属性设置为 PTHREAD_PROCESS_PRIVATE,则仅有那些由同一个进程创建的线程才能够处理该互斥锁。

protocol 可定义应用于互斥锁属性对象的协议。

线程的优先级和调度不会受到互斥锁拥有权的影响。

此协议值(如 thrd1)会影响线程的优先级和调度。如果更高优先级的线程因 thrd1 所拥有的一个或多个互斥锁而被阻塞,而这些互斥锁是用 PTHREAD_PRIO_INHERIT 初始化的,则thrd1 将以高于它的优先级或者所有正在等待这些互斥锁(这些互斥锁是 thrd1 指所拥有的互斥锁)的线程的最高优先级运行。

如果 thrd1 因另一个线程 (thrd3) 拥有的互斥锁而被阻塞,则相同的优先级继承效应会以方式传播给 thrd3。

可以避免优先级倒置如果没有优先级继承,底优先级的线程可能会在很长一段时间内都得不到调度,而这会导致等待低优先级线程锁持有的锁的高优先级线程也等待很长时间(因为低优先级线程无法运行,因而就无法释放锁,所以高优先级线程只能继续阻塞在锁上)使用优先级继承可以短时间的提高低优先级线程的优先级,从而使它可以尽快得到调度,然后释放锁。低优先级线程在释放锁后就会恢复自己的优先级。

互斥锁的下一个属主将获取该互斥锁,并返回错误 EOWNERDEAD。

互斥锁的下一个属主会尝试使该互斥锁所保护的状态一致。如果上一个属主失败,则状态可能会不一致。如果属主成功使状态保持一致,则可针对该互斥锁调用pthread_mutex_init()并解除锁定该互斥锁。

注 –如果针对以前初始化的但尚未销毁的互斥锁调用pthread_mutex_init(),则该互斥锁不会重新初始化。

如果属主无法使状态保持一致,请勿调用pthread_mutex_init(),而是解除锁定该互斥锁。在这种情况下,所有等待的线程都将被唤醒。以后对pthread_mutex_lock()的所有调用将无法获取互斥锁,并将返回错误代码

如果已获取该锁的线程失败并返回 EOWNERDEAD,则下一个属主将获取该锁及错误代码 EOWNERDEAD。

此协议值会影响其他线程(如 thrd2)的优先级和调度。thrd2 以其较高的优先级或者以 thrd2 拥有的所有互斥锁的最高优先级上限运行。基于被 thrd2 拥有的任一互斥锁阻塞的较高优先级线程对于 thrd2 的调度没有任何影响。

如果某个线程调用sched_setparam()来更改初始优先级,则调度程序不会采用新优先级将该线程移到调度队列末尾。

可以同时拥有多个混合使用 PTHREAD_PRIO_INHERIT 和 PTHREAD_PRIO_PROTECT 初始化的互斥锁。在这种情况下,该线程将以通过其中任一协议获取的最高优先级执行。

prioceiling 指定已初始化互斥锁的优先级上限。优先级上限定义执行互斥锁保护的临界段时的最低优先级。prioceiling 位于 SCHED_FIFO 所定义的优先级的最大范围内。要避免优先级倒置,请将 prioceiling 设置为高于或等于可能会锁定特定互斥锁的所有线程的最高优先级。可锁定互斥锁(如果未锁定的话),或者一直处于,直到pthread_mutex_setprioceiling()成功锁定该互斥锁,更改该互斥锁的优先级上限并将该互斥锁释放为止。锁定互斥锁的过程无需遵循优先级保护协议。

oldceiling 包含以前的优先级上限值。

robustness 定义在互斥锁的持有者“死亡”时的行为

np后缀的,表示not portable,就是不可移值的意思,这些函数是一些系统自己实现的,而不是POSIX标准。

如果互斥锁的持有者死亡,则以后对 pthread_mutex_lock() 的所有调用将以不确定的方式被阻塞。

如果互斥锁的持有者“死亡”了,或者持有这样的互斥锁的进程unmap了互斥锁所在的共享内存或者持有这样的互斥锁的进程执行了exec调用,则会解除锁定该互斥锁。互斥锁的下一个持有者将获取该互斥锁,并返回错误 EOWNWERDEAD。

互斥锁的新属主应使该互斥锁所保护的状态保持一致。如果上一个属主失败,则互斥锁状态可能会不一致。

如果新属主能够使状态保持一致,请针对该互斥锁调用pthread_mutex_consistent_np(),并解除锁定该互斥锁。

如果新属主无法使状态保持一致,请勿针对该互斥锁调用pthread_mutex_consistent_np(),而是解除锁定该互斥锁。

所有等待的线程都将被唤醒,以后对pthread_mutex_lock()的所有调用都将无法获取该互斥锁。返回代码为

如果已获取该锁的线程失败并返回 EOWNERDEAD,则下一个属主获取该锁时将返回代码 EOWNERDEAD。

 10、互斥锁的相关实现与效率问题

互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁。

读写锁实际是一种特殊的

,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于

中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

在读写锁保持期间也是抢占失效的。

如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

一次只有一个可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性,

当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.

当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的

都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到知道所有的线程释放锁.

通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁请求长期阻塞.

读写锁适合于对的读次数比写次数多得多的情况.因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁.

成功则返回0, 出错则返回错误编号.

以上, 在释放读写锁占用的内存之前, 需要先通过pthread_rwlock_destroy对读写锁进行清理工作, 释放由init分配的资源.

成功则返回0, 出错则返回错误编号.

这3个函数分别实现获取读锁, 获取写锁和释放锁的操作. 获取锁的两个函数是阻塞操作, 同样, 非阻塞的函数为:

成功则返回0, 出错则返回错误编号.

非阻塞的获取锁操作, 如果可以获取则返回0, 否则返回错误的EBUSY.


不同于互斥锁,互斥锁主要用于上锁,而条件变量用于等待。它特别适合需要进行同步的问题,比如线程A,B存在依赖关系,B要在某个条件发生之后才能继续执行,而这个条件只有A才能满足,这个时候就可以使用条件变量来完成这个事情:
  • 创建和该条件相关联的条件变量,并初始化它
  • 对于线程A来说,它需要做的是设置这个条件,通知等待在相关联条件变量上的线程
  • 对于线程B来说,它需要做的是检查这个条件,如果不满足自己的要求,就阻塞在相关联的条件变量上
由于条件变量并没有包含任何需要检测的条件的信息,因而对这个条件需要用其它方式来保护,所以条件变量需要和互斥锁一起使用,每个条件变量总是有一个互斥锁和其关联。如果线程未持有与条件相关联的互斥锁,则调用pthread_cond_signal或pthread_cond_broadcast会产生唤醒丢失错误,满足以下所有条件时,即会出现唤醒丢失问题:

对比下信号,信号可以做到通知其它线程某件事发生了,接收信号的线程只需要注册一个信号处理函数,然后信号发生后该处理函数就会被系统调用,一旦该函数被调用了就意味着注册时关联的信号所代表的事情发生了。但要注意:

  1. POSXI要求多线程应用中信号处理程序必须在应用的多个线程之间共享(即在一个进程的多个线程之间共享),因而对于同一个进程中的多个线程来说它们必须共享信号处理程序,信号处理程序无法确定信号是被发给谁的
  2. 使用信号时只需要注册信号处理程序即可,不需要创建某种同步对象,而使用条件变量需要创建同步对象,如果要在进程间进行同步和互斥还对条件变量的作用域和属性有要求
  3. 有权限的任何用户的任何程序都可以发送信号给一个线程,而使用条件变量时,相关的线程必须可以访问同步对

如果条件变量是动态分配的,则必须在使用它之前用pthread_cond_init来初始化它。pthread_cond_init用来初始化cv所指向的条件变量,如果cattr为NULL则会用缺省的属性初始化条件变量;否则使用cattr指定的属性初始化条件变量。使用PTHREAD_COND_INITIALIZER 宏与动态分配具有null 属性的 pthread_cond_init()等效,不同之处在于PTHREAD_COND_INITIALIZER 宏不进行错误检查。多个线程决不能同时初始化或重新初始化同一个条件变量。如果要重新初始化或销毁某个条件变量,则应用程序必须确保该条件变量未被使用。

pthread_cond_wait以原子方式释放mutex所指向的互斥锁,并导致调用线程阻塞在cv所指向的条件变量上。阻塞的线程可以通过如下方式被唤醒: pthread_cond_wait返回时,由mutex指定的互斥锁被锁定并且被调用线程锁持有,即使返回错误时也是如此。pthread_cond_wait在被唤醒之前将一致保持阻塞状态。它会在被阻塞之前以原子方式释放相关的互斥锁,并在返回之前以原子方式再次获取该互斥锁
通常情况下对条件表达式的检查是在互斥锁的保护下进行的。如果条件表达式为假,线程就会基于条件变量阻塞。然后,当其它线程更改条件值时,就会唤醒它(通过pthread_cond_signal或pthread_cond_broadcast)。这种变化会导致至少一个正在等待该条件的线程解除阻塞并尝试再次获取互斥锁。
必须重新测试导致等待的条件,然后才能从 pthread_cond_wait处继续执行
。唤醒的线程重新获取互斥锁并从pthread_cond_wait返回之前,条件可能会发生变化。等待线程锁等待的条件可能并未真正发生。通常使用条件变量的方式如下:

pthread_mutex_unlock();pthread_cond_wait是一个取消点。如果有一个未决的取消请求并且该线程启用了取消功能,则该线程会被终止并在继续持有锁的状态下开始执行的清理处理函数。如果清理处理函数中未释放锁,则就会出现线程终止但是未释放锁的情形。

pthread_cond_signal解除阻塞在该条件变量上的一个线程的阻塞状态。

应在互斥锁的保护下修改相关条件,该互斥锁应该是与该条件变量相关联的那个互斥锁(即调用wati时指定的那个互斥锁)。否则,可能在条件变量的测试和pthread_cond_wait阻塞之间修改该变量,这会导致无限期等待。如果有多个线程在等待一个条件变量,则线程被唤醒的顺序由所采用的调度策略决定。

  • 如果使用的是默认的调度策略,即SCHED_OTHER,则无法保证被唤醒的顺序
如果没有任何线程基于条件变量阻塞,则调用 pthread_cond_signal不起作用。

5、指定时间内解除阻塞

pthread_cond_broadcast解除所有基于该条件变量阻塞的线程的阻塞。应在互斥锁的保护下修改相关条件,该互斥锁应该是与该条件变量相关联的那个互斥锁(即调用wati时指定的那个互斥锁)。否则,可能在条件变量的测试和pthread_cond_wait阻塞之间修改该变量,这会导致无限期等待。

由于pthread_cond_broadcast会导致所有基于该条件阻塞的线程再次争用互斥锁,因此即便使用了pthread_cond_broadcast实际上最终也只有一个线程可以获得锁并开始运行。虽然都是只有一个线程可以运行,但是这种情形与pthread_cond_signal是有所区别的:

  • 如果有多个线程阻塞在条件变量上,并且pthread_cond_signal唤醒了其中一个线程,则其它线程仍然在等待被唤醒然后再尝试获取相应的互斥锁,它们阻塞在条件变量上
  • 如果有多个线程阻塞在条件变量上,并且pthread_cond_broadcast唤醒它们,则所有线程都开始竞争互斥锁,胜利者开始执行,失败者阻塞在互斥锁上
如果没有任何线程基于条件变量阻塞,则调用pthread_cond_broadcast不起作用。
自旋锁是SMP架构中的一种low-level的同步机制。

当线程A想要获取一把自选锁而该锁又被其它线程锁持有时,线程A会在一个循环中自选以检测锁是不是已经可用了。对于自选锁需要注意:

  • 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
  • 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。(在内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起,最近刚解决了一个内核中的问题就是由于持有自旋锁时sleep了,然后导致所有的核全部挂起(是一个8核的CPU))
使用任何锁需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:
  1. 当线程被阻塞时锁所需要的资源
对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。

因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。

pthread_spin_init用来申请使用自旋锁所需要的资源并且将它初始化为非锁定状态。pshared的取值及其含义:
  • PTHREAD_PROCESS_PRIVATE:仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。
pthread_spin_destroy用来销毁指定的自旋锁并释放所有相关联的资源(所谓的所有指的是由pthread_spin_init自动申请的资源)在调用该函数之后如果没有调用pthread_spin_init重新初始化自旋锁,则任何尝试使用该锁的调用的结果都是未定义的。如果调用该函数时自旋锁正在被使用或者自旋锁未被初始化则结果是未定义的。

共享锁(S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁,直到已释放所有共享锁。获准共享锁的事务只能读数据,不能修改数据。
排他锁(X锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的锁,直到在事务的末尾将资源上的锁释放为止。获准排他锁的事务既能读数据,又能修改数据。

当进程进入CPU运行时,就会给它的代码上锁,以免别的CPU中的进程修改里面的代码(不排除CPU给别的CPU上锁这样的情况,以后会讨论到。)。所谓子旋锁就是这样的一把锁:进程A进入CPU,锁上门运行,进程B来到CPU前,发现门被锁上了,于是等待进程A出来交出开锁钥匙。

正如每次我们谈到“锁”这个概念时,总会谈到“死锁”——是的,我们用锁,就必须防止死锁,死锁是这样产生的:进程A进入CPU运行,上锁,进程B 来到CPU门前等待进程A出来,可是糟糕的情况出现了:进程A要想出来就必须获取进程B的帮助,于是进程A开始等待进程B的帮助,可是进程B却又一直等待进程A出来!这样的等待无法终止,最终成为死锁。

再比如,进程A要锁上甲代码段,然后想再去锁乙代码段,进程B要锁上乙代码段,然后想再去锁甲代码段。第一步大家都没问题,可是两个进程都要进行下一步时,发现无法完成任务了:进程A已经锁上甲代码段,进程B没法再去操作它,同理进程B已经锁上乙代码段,进程A也没办法操作它,于是两个进程等待对方释放锁,当然,这样的等待也是无止无休的。这就好象两辆汽车在一座很榨的桥上相向行驶,两车碰头谁也不让谁,都在等待对方让路。

避免死锁,必须使每次上锁操作都是有顺序的、原子的操作。有顺序的,也就是说每次都按照可执行队列地址从低向高的顺序上锁——我们以后会很好的讨论这个。

原子的,就是说每次上锁必须执行到底,否则不予执行

我要回帖

更多关于 自旋锁 的文章

 

随机推荐