OSSpinLock

文章:iOS 笔记 - 锁iOS 开发中的八种锁(LockiOS 常见知识点(三):Lock不再安全的 OSSpinLock

 

一、OSSpinLock

OSSpinLock 是一种自旋锁,属于 busy-waiting 类型的锁。与互斥锁不同,当 SpinLock 被其它线程持有,spinLock 不会被阻塞,而会一直的请求获取 lock,从而消耗大量 cpu 资源。所以当临界区任务时间较长时,并不适合用 SpinLock,但当任务时间较短,其效率很高。

只有加锁、解锁、尝试加锁三个方法。

#import <libkern/OSAtomic.h>

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    __block OSSpinLock oslock = OS_SPINLOCK_INIT;

    // 线程 1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"线程1 准备上锁");
        OSSpinLockLock(&oslock);
        sleep(4);
        NSLog(@"线程1");
        OSSpinLockUnlock(&oslock);
        NSLog(@"线程1 解锁成功");
        NSLog(@"--------------------------------------------------------");
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"线程2 准备上锁");
        OSSpinLockLock(&oslock);
        NSLog(@"线程2");
        OSSpinLockUnlock(&oslock);
        NSLog(@"线程2 解锁成功");
    });   
}

2019-03-01 16:17:03.812 Demo[589:1531210] 线程2 准备上锁
2019-03-01 16:17:03.812 Demo[589:1531209] 线程1 准备上锁
2019-03-01 16:17:03.812 Demo[589:1531210] 线程2
2019-03-01 16:17:03.812 Demo[589:1531210] 线程2 解锁成功
2019-03-01 16:17:07.814 Demo[589:1531209] 线程1
2019-03-01 16:17:07.815 Demo[589:1531209] 线程1 解锁成功
2019-03-01 16:17:07.816 Demo[599:1532045] --------------------------------------------------------


2019-03-01 16:18:33.605 Demo[599:1532045] 线程1 准备上锁
2019-03-01 16:18:33.605 Demo[599:1532044] 线程2 准备上锁
2019-03-01 16:18:37.610 Demo[599:1532045] 线程1
2019-03-01 16:18:37.610 Demo[599:1532045] 线程1 解锁成功
2019-03-01 16:18:37.611 Demo[599:1532045] --------------------------------------------------------
2019-03-01 16:18:37.668 Demo[599:1532044] 线程2
2019-03-01 16:18:37.668 Demo[599:1532044] 线程2 解锁成功

先锁住线程 1 时,异步执行锁住线程 2,线程 2 会一直等待(自旋锁不会让等待的进入睡眠状态),直到线程 1 的任务执行完且解锁完毕,线程 2 会立即执行;同理,先锁住线程 2,那么线程 1 就需要等待线程 2 解锁成功才能继续执行。

修改一下代码:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&oslock);
//        OSSpinLockUnlock(&oslock);
    });

当注释掉先锁住的线程的解锁代码,后锁住的线程将一直等待。

常用的方法:

OS_SPINLOCK_INIT       默认值为 0。在 locked 状态时就会大于 0unlocked 状态下为 0
OSSpinLockLock(&oslock)  上锁。参数为 &OSSpinLock
OSSpinLockUnlock(&oslock)  解锁。参数为 &OSSpinLock
OSSpinLockTry(&oslock)  尝试加锁,可以加锁则立即加锁并返回  YES,反之返回 NO

trylocklock 使用场景:

当前线程锁失败,也可以继续其它任务,用 trylock 合适;
当前线程只有锁成功后,才会做一些有意义的工作,那就 lock,没必要轮询 trylock

OSSpinLock 与 NSLock 的区别:

NSLock 请求加锁失败的话,会先轮询,但一秒过后便会使线程进入 waiting 状态,等待唤醒;而 OSSpinLock一直轮询,等待时会消耗大量 CPU 资源,不适用于较长时间的任务。

 

二、不再安全的 OSSpinLock

2015-12-14 苹果工程师透露了自旋锁的 bug,对话内容大致如下:

新版 iOS 中,系统维护了 5 个不同的线程优先级/QoSbackgroundutilitydefaultuser-initiateduser-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock

具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock,这就是优先级反转。这并不只是理论上的问题,开发者已经遇到很多次这个问题,于是苹果工程师停用了 OSSpinLock

苹果工程师 Greg Parker 提到,对于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但如果系统负载高时,它仍有可能将高优先级的线程阻塞数十秒之久;另一种方案是使用 handoff lock 算法,这也是 libobjc 目前正在使用的。锁的持有者会把线程 ID 保存到锁内部,锁的等待者会临时贡献出它的优先级来避免优先级反转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上目前还一切都好。

libobjc 里用的是 Mach 内核的 thread_switch() 然后传递了一个 mach thread port 来避免优先级反转,另外它还用了一个私有的参数选项,所以开发者无法自己实现这个锁。另一方面,由于二进制兼容问题,OSSpinLock 也不能有改动。

最终的结论就是,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。

 

三、OSSpinLock 的替代方案

除了 OSSpinLock 外,dispatch_semaphore  pthread_mutex 性能是最高的。有消息称,苹果在新系统中已经优化了 pthread_mutex 的性能,所以它看上去和 OSSpinLock 差距并没有那么大了。

 

四、替换更新

查看 CoreFoundation 的源码能够发现,苹果至少在 2014 年就发现了这个问题,并把 CoreFoundation 中的 spinlock 替换成了 pthread_mutex,具体变化可以查看这两个文件:CFInternal.h(855.17)CFInternal.h(1151.16)苹果自己发现问题后,并没有及时更新 OSSpinLock 的文档,也没有告知开发者。

iOS 10/macOS 10.12 发布时,苹果提供了新的 os_unfair_lock 作为 OSSpinLock 的替代,并且将 OSSpinLock 标记为了 Deprecated

google/protobuf 内部的 spinlock 被全部替换为 dispatch_semaphore,详情可以看这个提交:https://github.com/google/protobuf/pull/1060。用 dispatch_semaphore 而不用 pthread_mutex 应该是出于性能考虑。

You may also like...