iOS 多线程开发之系列文章
多线程开发是日常开发任务中不可缺少的一部分,在 iOS 开发中常用到的多线程开发技术有 GCD、Operation、Thread,本文主要讲解多线系列文章中关于 线程安全 的相关知识。
简介
多个线程访问同一块资源进行读写,如果不加控制随意访问容易产生数据错乱从而引发数据安全问题。为了解决这一问题,就有了加锁的概念。加锁的原理就是当有一个线程正在访问资源进行写的时候,不允许其他线程再访问该资源,只有当该线程访问结束后,其他线程才能按顺序进行访问。对于读取数据,有些程序设计是允许多线程同时读的,有些不允许。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,就不得不说一下NSConditionLock,NSConditionLock对NSCondition又做了一层封装,自带条件探测,能够更简单灵活的使用。
使用 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_enter 和 objc_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_enter 和 objc_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
性能对比
引用一张被广泛引用在此类文章中的图片来说明