为了保证线程安全操作,一般语言都提供了锁Lock来保证临界区安全。所以我们可以通过NSLock来实现对变量的读和写都加锁来实现线程安全。
class Sample {
var intValue = 0
let lock:NSLock = NSLock
func getValue()->Int {
lock.lock()
let value = intValue
lock.unlock()
return value
}
func setValue(newValue:Int) {
lock.lock()
intValue = newValue
lock.unlock()
}
}
这样会有一个问题,如果需要线程安全保证的变量特别多,或者针对该变量的操作次数比较多,那么这种代码就需要写得比较多了,虽然可以定义一个函数把操作简洁点,但是还是不够优雅。
其实Swift里面还可以通过串行队列来保证线程安全,把针对变量的操作放到同一个队列中,并且以同步的方式来执行。
class QueueSample {
let queue = DispatchQueue("com.liudong.demo")
var intValue = 0
func getValue()->Int {
return queue.sync{
intValue
}
}
func setValue(newValue:Int) {
queue.sync {
intValue = newValue
}
}
}
这样写也能达到目标,但是还是有跟用lock相同的问题,就是模板代码多,某些情况下,可能还得建立不止一个队列。这里还存在一个问题,如果需要同时取值并设置值,例如自增+=1操作,就需要先get再set,代码比较难看。其实swift还有一个语法糖,PropertyWrapper,看下其漂亮的使用。
import Foundation
@propertyWrapper
class Atomic<Value> {
var projectedValue:Atomic<Value> {
return self
}
private let queue = DispatchQueue(label: "com.liudong.demo")
private var value: Value
init(wrappedValue: Value) {
self.value = wrappedValue
}
var wrappedValue: Value {
get {
return queue.sync { value }
}
set {
queue.sync { value = newValue }
}
}
func mutate<R>(_ mutation: (inout Value) -> R) -> R {
return queue.sync {
return mutation(&value)
}
}
}
//使用如下
class MagicSample {
@Atomic var intValue:Int = 0
func test() {
DispatchQueque.global().async { [weak self] in
guard let self = self else {return}
//注意:如果实现intValue+=1来实现自增1的话,是不能直接写intValue+=1的,必须要使用mutate来保证原子操作。
let value = self.$atomicValue.mutate { (v) -> Int in
v += 1
return v
}
if intValue == 1 {
print("测试get")
}
}
}
}
后续如果有其他的Property需要保证原子操作,只需要加上@Atomic注解即可,减少了模板代码的编写。
从代码编写的简洁度来说,DispatchQueue的方式有优势,下面来探究下两种方式性能上的差异。
性能对比方案:
让一个整数自增到给定值,比较两种方式的执行时长。
class ViewController: UIViewController {
private var atomicBool = false
@Atomic private var atomicValue = 0
@Atomic private var atomicArray = [Int]()
private let intLock = NSLock()
private var lockInt = 0
private let INCRETE_COUNT = 50000
var start:Int? = nil
var end:Int? = nil
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.view.addSubview(lockBtn)
lockBtn.backgroundColor = UIColor.red
lockBtn.frame = CGRect.init(x: 100, y: 100, width: 100, height: 100)
self.view.addSubview(queueBtn )
queueBtn.frame = CGRect.init(x: 100, y: 200, width: 100, height: 100)
queueBtn.backgroundColor = UIColor.blue
let lockGesture = UITapGestureRecognizer.init(target: self, action: #selector(testLock))
lockBtn.addGestureRecognizer(lockGesture)
let queueGesture = UITapGestureRecognizer.init(target: self, action: #selector(testQueue))
queueBtn.addGestureRecognizer(queueGesture)
}
@objc private func testLock() {
self.lockInt = 0
start = Int(Date().timeIntervalSince1970 * 1000)
for index in 0...INCRETE_COUNT {
DispatchQueue.global().async { [weak self] in
guard let self = self else {return}
self.intLock.lock()
self.lockInt+=1
do {
// print("int value : \(self.lockInt)")
if self.lockInt == self.INCRETE_COUNT {
let end = Int(Date().timeIntervalSince1970 * 1000)
print("lock执行时间:\(end - self.start!)")
}
self.intLock.unlock()
}
}
}
}
@objc private func testQueue() {
self.atomicValue = 0
start = Int(Date().timeIntervalSince1970 * 1000)
for index in 0...INCRETE_COUNT {
DispatchQueue.global().async { [weak self] in
guard let self = self else {return}
let value = self.$atomicValue.mutate { (v) -> Int in
v += 1
return v
}
if value == self.INCRETE_COUNT {
let end = Int(Date().timeIntervalSince1970 * 1000)
print("queue:执行时间:\(end - self.start!)")
}
}
}
}
let lockBtn:UIButton = {
let btn = UIButton.init()
btn.setTitle("Lock", for: .normal)
return btn
}()
let queueBtn:UIButton = {
let btn = UIButton.init()
btn.frame = CGRect.init(x: 100, y: 100, width: 100, height: 100)
btn.setTitle("Queue", for: .normal)
return btn
}()
}
界面如下,红色按钮点击启动lock的方案,蓝色按钮点击启动queque方案
测试结果:
结论:lock性能更好,并且随着循环次数的增加,lock和queue的性能差距越来越大,循环次数达到5000的话,queue的耗时是lock的2倍。平时开发中,如果针对变量的多进程赋值取值到了1W加,可以考虑用锁来提升性能,操作次数1w一下的话,可以用队列,方便使用。