iOS 多线程开发之线程安全

1,050 阅读7分钟

iOS 多线程开发之系列文章

iOS 多线程开发之概念

iOS 多线程开发之 Thread

iOS 多线程开发之 GCD

iOS 多线程开发之 Operation

iOS 多线程开发之线程安全

多线程开发是日常开发任务中不可缺少的一部分,在 iOS 开发中常用到的多线程开发技术有 GCD、OperationThread,本文主要讲解多线系列文章中关于 线程安全 的相关知识。

简介

多个线程访问同一块资源进行读写,如果不加控制随意访问容易产生数据错乱从而引发数据安全问题。为了解决这一问题,就有了加锁的概念。加锁的原理就是当有一个线程正在访问资源进行写的时候,不允许其他线程再访问该资源,只有当该线程访问结束后,其他线程才能按顺序进行访问。对于读取数据,有些程序设计是允许多线程同时读的,有些不允许。UIKit 中几乎所有控件都不是线程安全的,因此需要在主线程上更新 UI。

Swift 中用 var 声明的变量默认是 非原子性 的,如果要保证线程安全,我们就需要引入锁的感念。

解决多线程安全问题的方法

互拆锁

NSLock

NSLock 是在同一应用程序中协调多个执行线程的操作的对象。

NSLock 对象可以用来作为对应用程序全局数据的中间访问,或者用来保护代码的关键部分,允许它自动运行。

注意点:

  • 加锁解锁必须在同一线程.
  • 不应该使用这个类来实现递归锁。在同一个线程上调用两次锁方法将会永久锁定线程。
  • 解锁未锁定的锁被认为是程序员的错误,应该在代码中修复。当错误发生时,NSLock类通过向控制台打印错误消息来报告这些错误。

NSLock 作为最常用的锁,使用起来非常简单。在需要加锁的地方 lock(),然后在解锁的地方 unlock() 即可。

private let lock = NSLock()

private var _finished = false

public override var isFinished: Bool {
    get {
        lock.lock()
        let v = _finished
        lock.unlock()
        return v
    }
    set {
        guard isFinished != newValue else {
            return
        }
        willChangeValue(forKey: "isFinished")
        lock.lock()
        _finished = newValue
        lock.unlock()
        didChangeValue(forKey: "isFinished")
    }
}

这样会有一个问题,如果需要线程安全保证的变量特别多,或者针对该变量的操作次数比较多,那么这种代码就需要写得比较多了,虽然可以定义一个函数把操作简洁点,但是还是不够优雅。 其实 Swift 里面还可以通过串行队列来保证线程安全,把针对变量的操作放到同一个队列中,并且以同步的方式来执行。

条件锁

NSCondition

条件锁,顾名思义,就是满足某些条件才会开锁。NSCondition,可以确保线程仅在满足特定条件时才能获取锁。一旦获得了锁并执行了代码的关键部分,线程就可以放弃该锁并将关联条件设置为新的条件。条件本身是任意的:可以根据应用程序的需要定义它们。

NSCondition 对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。通俗的说,也就是条件成立,才会执行锁住的代码。条件不成立时,线程就会阻塞,直到另一个线程向条件对象发出信号解锁为止。

1.锁住关键代码,通过条件控制线程的进行.

let condition =  NSCondition()

var products: [NSObject] = []

DispatchQueue.global().async {
    while(true){
        condition.lock()
        if products.count == 0{
            print(Date(),"消费1缺货等待")
            condition.wait()
        }

        print(Date(),"消费1执行")
        products.removeFirst()
        condition.unlock()
    }
}

DispatchQueue.global().async {
    while(true){
        condition.lock()
        print(Date(),"生产线程执行")
        products.append(NSObject())
        condition.signal()
        condition.unlock()
        sleep(1)
    }
}

2.唤醒其它一个或多个线程

let condition =  NSCondition()

DispatchQueue.global().async {
    condition.lock()
    print("线程1加锁成功", Thread.current)
    condition.wait()
    print("线程1")
    condition.unlock()
    print("线程1解锁成功", Thread.current)
}

DispatchQueue.global().async {
    condition.lock()
    print("线程2加锁成功", Thread.current)
    condition.wait()
    print("线程2")
    condition.unlock()
    print("线程2解锁成功", Thread.current)
}

DispatchQueue.global().async {
    sleep(2)
    //唤醒一个等待的线程
    //            print("唤醒一个等待的线程")
    //            condition.signal()
    //唤醒所有等待的线程
    print("唤醒所有等待的线程")
    condition.broadcast()
}

NSConditionLock

说到NSCondition,就不得不说一下NSConditionLockNSConditionLockNSCondition又做了一层封装,自带条件探测,能够更简单灵活的使用。

使用 NSConditionLock,可以确保线程仅在 condition 符合情况时上锁,并且执行相应的代码,然后分配新的状态。状态值需要自己定义。

示例:

class ViewController: UIViewController {

    let lock = NSConditionLock(condition: 10)

    var person = Person(name: "Leo", age: 23)

    override func viewDidLoad() {

        super.viewDidLoad()

        let queue1 = dispatch_queue_create("com.test.queue1", DISPATCH_QUEUE_SERIAL)

        let queue2 = dispatch_queue_create("com.test.queue1", DISPATCH_QUEUE_SERIAL)

        dispatch_async(queue1) { () -> Void in
            self.lock.lockWhenCondition(10)
            self.person.update("Jack", delay: 2, age: 25)
            self.lock.unlockWithCondition(10)
        }

        dispatch_async(queue2) { () -> Void in
            self.lock.lockWhenCondition(10)
            self.person.update("Lucy", delay: 1, age: 24)
            self.lock.unlockWithCondition(10)
        }
        self.performSelector("logPerson", withObject: nil, afterDelay: 4)

        // Do any additional setup after loading the view, typically from a nib.
    }

    func logPerson(){
        NSLog("%@ %d", person.name,person.age)
    }
}

对比 NSCondition 和 NSConditionLock

相同点:

  • 都是互斥锁
  • 通过条件变量来控制加锁、释放锁,从而达到阻塞线程、唤醒线程的目的

不同点:

  • NSCondition是基于对pthread_mutex的封装,而NSConditionLock是对NSCondition做了一层封装
  • NSCondition是需要手动让线程进入等待状态阻塞线程、释放信号唤醒线程,NSConditionLock则只需要外部传入一个值,就会依据这个值进行自动判断是阻塞线程还是唤醒线程

递归锁 NSRecursiveLock

private let lock = NSRecursiveLock()

private var _finished = false

public override var isFinished: Bool {
    get {
        lock.lock()
        let v = _finished
        lock.unlock()
        return v
    }
    set {
        guard isFinished != newValue else {
            return
        }
        willChangeValue(forKey: "isFinished")
        lock.lock()
        _finished = newValue
        lock.unlock()
        didChangeValue(forKey: "isFinished")
    }
}

objc_sync

互斥锁(同步锁)@synchronized

虽然 @synchronized这个方法很简单好用,但是很不幸的是在 Swift 中它已经不存在了。其实 @synchronized 在幕后做的事情是调用了 objc_sync 中的 objc_sync_enterobjc_sync_exit 方法,并且加入了一些异常判断。

objc_sync_enter(object)方法会在 object 上开启同步(synchronize),如果成功返回OBJC_SYNC_SUCCESS, 否则返回OBJC_SYNC_NOT_OWNING_THREAD_ERROR ,直到objc_sync_exit(object)

因此,在 Swift 中,如果我们忽略掉那些异常的话,我们想要 lock 一个变量的话,可以这样写:

func myMethod(anObj: Any) {
    objc_sync_enter(anObj)
    // 在 enter 和 exit 之间 anObj 不会被其他线程改变
    objc_sync_exit(anObj)
}

更进一步,如果我们喜欢以前的那种形式,甚至可以写一个全局的方法,并接受一个闭包,来将 objc_sync_enterobjc_sync_exit 封装起来:

func synchronized(_ lock: Any, closure: () -> Void) {
    objc_sync_enter(lock)
    closure()
    objc_sync_exit(lock)
}

再结合Swift 的尾随闭包的语言特性,这样,使用起来就和Objective-C 中很像了:

func myMethod(anObj: Any) {
    synchronized(anObj) {
        // 在括号内 anObj 不会被其他线程改变
    }
}

PropertyWrapper

使用 PropertyWrapper 属性包装器实现 @Atomic

import Foundation

@propertyWrapper
public struct Atomic<Value> {

  private let queue = DispatchQueue(label: "com.safe.queue")
  private var value: Value


  public init(wrappedValue: Value) {
    self.value = wrappedValue
  }

  public var wrappedValue: Value {
    get {
      return queue.sync { value }
    }

    set {
      queue.sync { value = newValue }
    }
  }
}

使用 @Atomic 属性包装器

class ViewController: UIViewController {

    var start:Int? = nil
    var end:Int? = nil
    
    private let INCRETE_COUNT = 50000
    
    @Atomic var atomicValue:Int = 0


    override func viewDidLoad() {
        super.viewDidLoad()
        testQueue()
    }

    private func testQueue() {
        self.atomicValue = 0
        start = Int(Date().timeIntervalSince1970 * 1000)
        for _ in 0..<INCRETE_COUNT {
            DispatchQueue.global().async { [weak self] in
                guard let self = self else {return}
                //注意:如果实现 atomicValue += 1 来实现自增 1 的话,是不能直接写 atomicValue += 1 的,必须要使用 mutate 来保证原子操作。
                let value = self.$atomicValue.mutate { (v) -> Int in
                    v += 1
                    return v
                }

                if value == self.INCRETE_COUNT {
                    let end = Int(Date().timeIntervalSince1970 * 1000)
                    print("atomicValue:\(self.atomicValue)")
                    print("queue:执行时间:\(end - self.start!)")
                }
            }
        }
    }
}

注意: @Atomic 属性包装器不适用于集合类型, 解决方法根据需求实现集合类型。

线程安全的词典

public class AtomicDict<Key: Hashable, Value>: CustomDebugStringConvertible {

    private var dictStorage = [Key: Value]()

    private let queue = DispatchQueue(label: "com.safe.queue)",

                                      qos: .utility,

                                      attributes: .concurrent,

                                      autoreleaseFrequency: .inherit,

                                      target: .global())

    public subscript(key: Key) -> Value? {
        get {
            queue.sync { dictStorage[key] }
        }

        set {
            queue.async(flags: .barrier) { [weak self] in
                self?.dictStorage[key] = newValue
            }
        }
    }

    public var debugDescription: String {
        return dictStorage.debugDescription
    }
    
    
    public init() {}
}

自旋锁

OSSpinLock

由于存在因为低优先级争夺资源导致的死锁,在iOS10.0之后已废弃,并引入下面的新方法。

os_unfair_lock

替代 OSSpinLock 的自旋锁方案。需要导入 os

性能对比

引用一张被广泛引用在此类文章中的图片来说明

image.png