【Swift】iOS 线程锁

Swift 中var生命的变量默认是非原子性的,若是要保证线程安全,咱们就须要引入锁的感念。html

注意:谨慎直接在Demo中用for+print()等来证实是否线程安全。由于print()方法自己是线程安全的,它可能会拯救你的不安全代码。第3节objc_sync部分的例子有print()和NSLog()的比较,结果仅做参考。编程

  1. 本文将着重介绍NSCondition以及DispatchSemaphore
  2. 本文介绍的内容Demo代码都是基于Swift4.0

一 互斥锁

iOS里的线程互斥锁主要有如下几种安全

  1. 遵循NSLocking协议. 包括NSLockNSConditionNSConditionLockNSRecursiveLock
  2. GCD. DispatchSemaphore, DispatchWorkItemFlags.barrier
  3. objc_sync. 包括 @synchronized
  4. pthread. 包括POSIX。POSIX比较底层,可是通常不多用了。在此对POSIX也不详述。文末有相关讨论的资源。

1. NSLocking协议

NSlocking协议自己仅仅定义了lock()和unlock()bash

public protocol NSLocking {

    
    public func lock() 

    public func unlock()
}
复制代码

除了NSCondition,其它三种锁都是有如下两个方法markdown

  1. open func `try`() -> Bool
    尝试锁,若是成功,返回true。这里须要注意的是,若是对一个已经调用lock()加锁的程序再次加锁会产生死锁,此时不会有返回值。(参考下面注意点4.1)多线程

  2. open func lock(before limit: Date) -> Bool
    加锁,而且给这个锁一个过时时间,时间到了自动解锁。并发

1.1 NSLock

open class NSLock : NSObject, NSLocking {

    
    open func `try`() -> Bool 

    open func lock(before limit: Date) -> Bool

    
    @available(iOS 2.0, *)
    open var name: String?
}

复制代码

最经常使用的锁。在须要加锁的地方lock(),而后在解锁的地方unlock()便可。app

1.2 NSCondition

@available(iOS 2.0, *)

open class NSCondition : NSObject, NSLocking {

    /// 阻塞(休眠)线程。收到信号唤醒
    open func wait()
    /// 休眠当前线程,并设置一个自动唤醒的时间
    open func wait(until limit: Date) -> Bool 
    /// 发出信号 唤醒一个线程
    open func signal() 
     /// 发出信号,所用运用当前NSConditionh实例的wait()线程都会唤醒
    open func broadcast()

    
    @available(iOS 2.0, *)
    open var name: String?
}
复制代码

下面详细介绍一下NSCondition异步

1.2.1. 执行步骤伪代码

lock the condition // 锁住condition
while (!(boolean_predicate)) { // 一个做为判断用的Bool值
    wait on condition // Wait()
}
do protected work // 执行任务代码
(optionally, signal or broadcast the condition again or change a predicate value) // 经过signal或者baroadcast来改变状态
unlock the condition // 解锁condition
复制代码

1.2.2. 简介

wait()其实并不能直接用于锁住线程,做用原理以下。
调用condition wait()以后,condition 实例会解锁它已有的锁(保证同时只有一个锁)而后使当前调用的线程休眠。当 condition 被signal()通知后,系统会唤起线程。而后 condition 实例会在wait()或者wait(until:)方法结束的位置再次给线程加锁。所以,从线程的角度来看,就好像wait()一直在保有这个锁。
虽然wait()会给线程加锁,在测试的时候也确实能够按照指望运行,可是根据苹果官方文档, 单只使用wait()加锁并不能确保安全。因此,不管什么状况使用Condition的时候,第一步老是加锁。锁住当前condition能够确保判断和任务代码不会受其它使用相同condition的线程影响。async

基于condition发信号的原理,使用Bool值来判断是很是重要的。给condition发射信号并不能保证condition自己为true。因为发信号的时间问题可能会致使假信号的出现。使用Bool值来判断能够确保以前的信号不会形成代码在还不能安全运行的时候执行。这个判断值就是一个很简单Bool标签,仅仅是用来判断信号是否发射完成。 这部分的内容其实和用 POSIX Thread Locks中的情形同样。 wait()函数的内部伪代码以下

unlock the thread
wait for the signal
lock the thread
复制代码

在使用的时候应当以下(不包含Bool判断)

self.lock.lock()
self.lock.wait()
self.lock.unlock()
复制代码

1.3 NSConditionLock

使用NSConditionLock,能够确保线程仅在condition符合状况时上锁,而且执行相应的代码,而后分配新的状态。状态值须要本身定义。

1.4 NSRecurisiveLock

NSRecursiveLock 定义了一种能够屡次给相同线程上锁并不会形成死锁的锁。

2. GCD

GCD里的DispatchSemaphore,和DispatchWorkItemFlagBarrier也是能够达到线程锁的目的

2.1 DispatchSemaphore

open class DispatchSemaphore : DispatchObject {
}
extension DispatchSemaphore {

    public func signal() -> Int // 信号量增长1

    public func wait() // 信号量减小1

    public func wait(timeout: DispatchTime) -> DispatchTimeoutResult // 信号量减小1 并设置在timeout时间后加回这个减小的信号量1

    public func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult
}
extension DispatchSemaphore {
    @available(iOS 4.0, *)
    public /*not inherited*/ init(value: Int)
}
复制代码

2.1.1 初始化

DispatchSemaphore(value: value)
value对应着最大信号值,因此信号值能够对应到以下应用场景

  1. 当初始化的value值等于0,适用于两个线程之间协调任务。
  2. 当初始化的value值小于0,会形成返回Null,初始化失败。
  3. 当初始化的value值大于0,适合于管理一个有限的资源池,资源池的大小等于value值。
    而对于某个使用DispatchSemaphore来加锁的线程来讲,仅当当前信号量大于0时任务才会执行(能够理解成资源池有空闲资源)。

2.1.2 Demo

class GCDLockTest {
    let semaphore = DispatchSemaphore(value: 1) // 资源池1资源
    func test() {
        
        queueA.async {
            //直到经过signal()增长信号量
            self.semaphore.wait()  // 信号值 -1;资源池剩余资源0
            print("QueueA Gonna Sleep")
            sleep(3)
            print("QueueA Woke up")
            self.semaphore.signal()  // 信号值 +1;资源池剩余资源1
            
        }
        queueB.async {
            self.semaphore.wait()  // 信号值 -1 资源池剩余资源0
            print("QueueB Gonna Sleep")
            sleep(3)
            print("QueueB Work up")
            self.semaphore.signal()  // 信号值 +1 资源池剩余资源1

        }
        queueC.async {
            self.semaphore.wait()  // 信号值 -1 资源池剩余资源0
            print("QueueC Gonna Sleep")
            sleep(3)
            print("QueueC Wake up")
            self.semaphore.signal()  // 信号值 +1 资源池剩余资源1
            
        }
    }
}  
复制代码

上述代码的输出将会是

QueueA Gonna Sleepd // 信号量-1 (当前任务正在进行,无空闲资源)
// 3秒 期间queueB, queueC 并无执行,由于信号量初始化的值1,也就是最大容许1,能够理解为资源池只有一个资源
QueueA Woke up // QeueuA 完成 信号量+1  当前信号量1,资源释放,空闲资源有了。因而下一个线程开始执行
QueueB Gonna Sleep // QueueB执行 信号量-1
// 3秒
QueueB Work up // QueueB结束 信号量+1
QueueC Gonna Sleep // QueueC执行 信号量-1
// 3秒
QueueC Wake up // QueueC 结束 信号量+1

复制代码

同理,若是初始化值为2,最大能够同时两个线程执行。
若是初始值是3的话咱们的Demo中三个线程就均可以同时执行。
那若是初始化0呢?
显然,本例中的三个线程都将不能执行,由于信号量一直高于初始值。如今回看咱们在2.1中提到的应用场景,是否是就很好理解。

咱们将QueueAQueueB稍做改变

queueA.async {
            //直到经过signal()增长信号量
            self.semaphore.wait()  // 信号值 +1
            print("QueueA Gonna Sleep")
            sleep(3)
            print("QueueA Woke up")
            self.semaphore.signal()  // 信号值 -1
            
        }
        queueB.async {
            self.semaphore.signal()  // 信号值 +1
            print("QueueB Gonna Sleep")
            sleep(3)
            print("QueueB Work up")
        }
  
复制代码

这时的输出会是什么?

QueueB Gonna Sleep // 由于QueueA在执行的时候信号值+1,超过了0,因此只能等待
QueueA Gonna Sleep // 当QueueB执行的时候,信号值-1,没有超过0,因此QueueA就能执行了
/// 3秒
QueueB Work up
QueueA Woke up
复制代码

因此,当初始化值为0时,就能够达到两个线程其中一个再另外一个以后结束等功能。

注意: 若是在主线程中wait()会阻塞UI刷新

2.3 DispatchGroup

enter()是明确告诉GCD你要开始
leave()是明确标明任务结束
通常状况下不须要明确使用enter()/leave() 。 只有好比说,你的任务中包含其它异步任务,而你想要在这个子异步任务开始前就结束等待,那就可使用leave()了。

3. objc_sync

3.1 objc_sync_enter/objc_sync_exit

class SyncTest {
    var count = 0
    func test() {
        count = 0
        
        let queueA = DispatchQueue(label: "Q1")
        let queueB = DispatchQueue(label: "Q2")
        let queueC = DispatchQueue(label: "Q3")
        
        queueA.async {
            for _ in 1...100 {
                NSLog("%i", self.increased())
//                print(self.increased())
            }
        }
        queueB.async {
            for _ in 1...100 {
                NSLog("%i", self.increased())
//                print(self.increased())
            }
        }
        queueC.async {
            for _ in 1...100 {
                NSLog("%i", self.increased())
//                print(self.increased())
            }
            
        }
      
        
    }
    func increased() -> Int {
        objc_sync_enter(count)
        count += 1
        objc_sync_exit(count)
        return count
    }
}
复制代码

3.1.1

objc_sync_enter(object)方法会在object上开启同步(synchronize),若是成功返回OBJC_SYNC_SUCCESS, 不然返回OBJC_SYNC_NOT_OWNING_THREAD_ERROR ,直到objc_sync_exit(object)

objectAny类型。在本Demo中甚至能够直接传入self。可是它会锁住整个

二 自旋锁

主要介绍两种

  1. OSSpinLock。因为存在由于低优先级争夺资源致使的死锁,在iOS10.0以后已废弃,并引入下面的新方法。
  2. os_unfair_lock。替代OSSpinLock的自旋锁方案。须要导入os

三 性能比较

引用一张被普遍引用在此类文章中的图片来讲明

根据我后来本身作的测试,OSSpinLock和os_unfair_lock以及dispatch_semaphore三者的性能是最优且接近的。

四 注意点

1. 串行队列即便异步执行也不会从新开新线程。参考第二点后面的例子。
2. 主线程队列是单一线程串行队列的。不要在主线程加锁,会致使UI刷新被阻塞。

for i in 1...10 {
            DispatchQueue.global().async {
                print("\(i)---\(Thread.current)")
            }
        }

      
复制代码
for i in 1...10 {
            DispatchQueue.main.async {
                print("\(i)---\(Thread.current)")
            }
        }

      
复制代码

输出

5---<NSThread: 0x60c000074280>{number = 11, name = (null)}
2---<NSThread: 0x60800006e500>{number = 5, name = (null)}
6---<NSThread: 0x60400007a0c0>{number = 6, name = (null)}
3---<NSThread: 0x60400007a140>{number = 10, name = (null)}
9---<NSThread: 0x60800006e6c0>{number = 12, name = (null)}
4---<NSThread: 0x600000069b40>{number = 4, name = (null)}
8---<NSThread: 0x60400007a080>{number = 3, name = (null)}
1---<NSThread: 0x60800006e600>{number = 9, name = (null)}
7---<NSThread: 0x60400007a100>{number = 8, name = (null)}
10---<NSThread: 0x60000046c6c0>{number = 7, name = (null)}

复制代码
1---<NSThread: 0x60c000077d40>{number = 1, name = main}
2---<NSThread: 0x60c000077d40>{number = 1, name = main}
3---<NSThread: 0x60c000077d40>{number = 1, name = main}
4---<NSThread: 0x60c000077d40>{number = 1, name = main}
5---<NSThread: 0x60c000077d40>{number = 1, name = main}
6---<NSThread: 0x60c000077d40>{number = 1, name = main}
7---<NSThread: 0x60c000077d40>{number = 1, name = main}
8---<NSThread: 0x60c000077d40>{number = 1, name = main}
9---<NSThread: 0x60c000077d40>{number = 1, name = main}
10---<NSThread: 0x60c000077d40>{number = 1, name = main}

复制代码

3. 并发和并行: 并行是线程被多个CPU内核执行,并发是线程轮流交替被单个CPU内核执行。
4. 上锁的英文 acquire a lock
5. 对一个已经lock()的锁再次调用lock()将会产生死锁,这也是递归锁引入的缘由。递归锁实现的就是能够屡次加锁也不会产生死锁。

BTW: 一样的死锁会产生在在同一个同步线程中调用这个线程的同步队列

五 资源

本例代码后续会上传到Github
苹果官方多线程编程指南
POSIX博客

相关文章
相关标签/搜索