何时应该使用自旋锁而不是互斥锁?

15 浏览
0 Comments

何时应该使用自旋锁而不是互斥锁?

我认为两者都在做相同的工作,你如何决定使用哪一个进行同步?

0
0 Comments

当应该使用自旋锁而不是互斥锁的时候,可以参考以下内容:

在Alexander Sandler的博客文章《pthread mutex vs pthread spinlock》中,作者展示了如何使用#ifdef来实现测试行为的自旋锁和互斥锁。但是,确保根据观察和理解作出最终的决策,因为给出的示例是一个孤立的案例,你的项目需求和环境可能完全不同。

自旋锁和互斥锁是用于多线程编程中的同步机制。它们的目的是确保在多个线程访问共享资源时,只有一个线程能够访问该资源。然而,它们的实现方式有所不同。

互斥锁是一种阻塞锁。当一个线程尝试获取互斥锁时,如果锁已经被其他线程持有,则该线程会被阻塞,直到锁被释放。这种阻塞机制会导致线程的上下文切换,从而带来一定的开销。

自旋锁是一种忙等待锁。当一个线程尝试获取自旋锁时,如果锁已经被其他线程持有,则该线程会一直循环检查锁是否释放,而不会被阻塞。这种忙等待的方式可以避免线程的上下文切换,从而减少开销。然而,如果自旋锁一直无法获取,则会造成浪费CPU资源。

因此,当应该使用自旋锁而不是互斥锁的情况是当对共享资源的争用情况较短,并且期望等待时间也较短的情况下。在这种情况下,使用自旋锁可以避免线程的上下文切换,从而提高性能。

然而,应该注意的是,自旋锁不适用于长时间等待的情况,因为这会导致浪费CPU资源。在长时间等待的情况下,应该使用互斥锁来避免浪费资源。

总之,根据对共享资源的争用情况和期望等待时间的评估,可以选择使用自旋锁或互斥锁来实现线程同步。

0
0 Comments

在单处理器上,当任务等待由中断服务例程(ISR)提供的锁时,使用自旋锁可能是有意义的。中断会将控制权转移到ISR,ISR会准备好资源供等待的任务使用。在将控制权返回给被中断的任务之前,它会释放锁。自旋任务会发现自旋锁可用并继续执行。

然而,对于这个答案我不完全同意。在单处理器上,如果一个任务持有一个资源的锁,那么ISR不能安全地进行并且不能等待任务解锁资源(因为任务被中断了)。在这种情况下,任务应该简单地禁用中断,以强制在自身和ISR之间进行排斥。当然,这必须在非常短的时间间隔内完成。

"等待锁被中断服务例程提供",他的意思是ISR将解锁资源。

在这种情况下,使用自旋锁而不是互斥锁的原因是因为在单处理器上,任务可以通过等待ISR提供的锁来避免阻塞。如果使用互斥锁,任务将被阻塞直到锁可用。但是,需要注意的是,这种情况适用于单处理器系统,多处理器系统可能不适用。

解决方法是在任务等待ISR提供的锁时使用自旋锁。自旋锁是一种忙等锁,它会循环检查锁是否可用,而不会将任务置于休眠状态。这样,任务可以立即获得锁并继续执行,而不必等待锁的释放。但是,需要注意的是,自旋锁会消耗CPU资源,因此仅在短时间内等待锁时使用自旋锁是合适的。

下面是使用C++语言实现自旋锁的示例代码:

#include 
class SpinLock {
private:
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
    void lock() {
        while (flag.test_and_set(std::memory_order_acquire)) {
            // Do nothing, just keep spinning
        }
    }
    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

在上述代码中,`lock`函数通过循环检查`flag`的状态来获取自旋锁。如果`flag`已被设置,说明锁已被其他任务持有,任务将继续循环等待。当`flag`未被设置时,任务获得锁并继续执行。

`unlock`函数用于释放自旋锁,它会清除`flag`的状态,表示锁已被释放。

总之,在单处理器上,当任务需要等待中断服务例程提供的锁时,使用自旋锁可以避免阻塞任务,并且在短时间内等待锁时使用自旋锁是合适的。通过使用自旋锁,任务可以立即获得锁并继续执行,而不必等待锁的释放。但是需要注意,自旋锁会消耗CPU资源,因此只在短时间内等待锁时使用自旋锁是合适的。

0
0 Comments

在理论上,当一个线程尝试锁定一个互斥锁并且失败时,因为互斥锁已经被锁定,它会进入睡眠状态,立即允许另一个线程运行。它将继续睡眠,直到被唤醒,这将在之前持有锁的任何线程解锁时发生。当一个线程尝试锁定一个自旋锁并且失败时,它将不断重新尝试锁定它,直到最终成功; 因此,它不会允许另一个线程占据其位置(当然,一旦当前线程的CPU运行时间量超过,操作系统将强制切换到另一个线程)。

互斥锁的问题在于,将线程置于睡眠状态和再次唤醒它们都是非常昂贵的操作,它们将需要相当多的CPU指令,因此也需要一些时间。如果现在互斥锁仅被锁定了很短的时间,将线程置于睡眠状态和再次唤醒它们的时间可能远远超过线程实际睡眠的时间,甚至可能超过线程通过不断轮询自旋锁所浪费的时间。另一方面,对自旋锁进行轮询将不断浪费CPU时间,如果锁定时间较长,这将浪费更多的CPU时间,如果线程睡眠而不是自旋锁,效果会更好。

在单核/单CPU系统上使用自旋锁通常没有意义,因为只要自旋锁轮询阻塞唯一可用的CPU核心,就没有其他线程可以运行,而且由于没有其他线程可以运行,锁定也不会解锁。即只在这些系统上,自旋锁只会浪费CPU时间而没有真正的好处。如果线程被置于睡眠状态,另一个线程可以立即运行,可能会解锁锁定,然后在它再次唤醒后允许第一个线程继续处理。

在多核/多CPU系统上,如果只持有锁定的时间非常短,不断将线程置于睡眠状态和再次唤醒它们的时间可能会明显降低运行时性能。当使用自旋锁时,线程有机会充分利用其完整的运行时量(始终仅阻塞很短的时间,但随后立即继续工作),从而提高处理吞吐量。

由于很多时候程序员无法事先知道互斥锁还是自旋锁更好(例如,因为目标架构的CPU核心数量未知),操作系统也无法知道某段代码是否针对单核还是多核环境进行了优化,大多数系统不严格区分互斥锁和自旋锁。事实上,大多数现代操作系统都有混合互斥锁和混合自旋锁。那实际上是什么意思?

混合互斥锁在多核系统上首先表现得像自旋锁。如果线程无法锁定互斥锁,由于互斥锁可能很快解锁,因此线程不会立即被置于睡眠状态,而是首先与自旋锁完全相同。只有在一定时间(或重试或任何其他测量因素)后仍未获得锁定时,线程才会真正进入睡眠状态。如果在只有一个核心的系统上运行相同的代码,互斥锁将不会自旋,因为如上所述,这没有好处。

混合自旋锁一开始就表现得像普通自旋锁,但为了避免浪费太多CPU时间,它可能有一个退避策略。它通常不会将线程置于睡眠状态(因为使用自旋锁时不希望发生这种情况),但它可能决定停止线程(立即停止或经过一定时间后停止;这称为“让步”)并允许另一个线程运行,从而增加自旋锁被解锁的机会(您仍然需要线程切换的成本,但不需要置线程于睡眠状态和再次唤醒的成本)。

如果有疑问,使用互斥锁,它们通常是更好的选择,并且大多数现代系统如果看起来有利于自旋锁,将允许它们自旋锁一小段时间。使用自旋锁有时可以提高性能,但仅在特定条件下,而您对此有疑问实际上告诉我,您目前没有在任何可能受益于自旋锁的项目上工作。您可以考虑使用自己的“锁对象”,它可以在内部使用自旋锁或互斥锁(例如,在创建此类对象时可以配置此行为),最初在所有地方使用互斥锁,如果您认为在某个地方使用自旋锁可能真正有帮助,请尝试并比较结果(例如,使用分析器),但一定要在得出结论之前测试单核和多核系统的两种情况(以及可能的不同操作系统,如果您的代码将跨平台)。

更新:iOS的警告

实际上不仅限于iOS,但iOS是开发者最容易遇到这个问题的平台:如果您的系统具有不保证任何线程,无论其优先级如何低,最终都能获得运行机会的线程调度程序,那么自旋锁可能会导致永久死锁。iOS调度程序区分不同类别的线程,只有在没有较高类别的线程想要运行时,较低类别的线程才会运行。这个问题的关键在于:您的代码在低优先级类别的线程中获取了一个自旋锁,而在获取锁的过程中,时间量已经超过,并且线程停止运行。这个自旋锁能够再次释放的唯一方式是低优先级类别的线程再次获得CPU时间,但这不保证一定会发生。您可能有几个始终要运行的高优先级类别的线程,任务调度程序将始终优先考虑它们。其中一个线程可能会遇到自旋锁并尝试获取它,这当然是不可能的,并且系统将使其让步。问题是:让步的线程立即可以再次运行!由于优先级较高,线程持有锁的线程没有获得CPU运行时间的机会。要么其他某个线程将获得运行时间,要么刚刚让步的线程将获得运行时间。为什么互斥锁不会出现这个问题?当高优先级线程无法获取互斥锁时,它不会让步,它可能会自旋一会儿,但最终会被置于睡眠状态。睡眠线程在没有事件唤醒它之前不可用于运行,例如等待的互斥锁被解锁。苹果已经意识到了这个问题,并由此弃用了OSSpinLock。新的锁称为os_unfair_lock。这个锁避免了上述情况,因为它了解不同的线程优先级类别。如果您确定在iOS项目中使用自旋锁是个好主意,请使用它。远离OSSpinLock!在iOS中绝对不要实现自己的自旋锁!如果有疑问,请使用互斥锁。macOS不受此问题影响,因为它具有不同的线程调度程序,不会允许任何线程(即使是低优先级线程)在CPU时间上“耗尽”,但在那里也可能出现相同的情况,并且会导致非常差的性能,因此在macOS上也已弃用OSSpinLock

如果您想了解更多有关在Linux内核中实现的自旋锁和互斥锁的知识,我强烈推荐阅读伟大的Linux设备驱动程序第三版(LDD3)的第5章(互斥锁:第109页;自旋锁:第116页)。

使用C#的异步代码时,我目前发现唯一可靠的锁是SemaphoreSlim。旧的基于线程的lock(object)类型的锁甚至不会通过编译器,而其他类型的锁在释放锁时可能会给您带来SynchronizationLockException,并显示“调用线程未持有锁定”消息。特别是在持有锁定时调用其他异步方法时。等待异步概念确实破坏了明确的“每个线程一个堆栈”的思想,并且在调试中很难跟踪堆栈树以及异常日志。

0