背景
随着业务的复杂,发现 Event Report 的公共数据存取发生一切线程问题,所以再复习一下相关的知识,同时修一下陈旧的 issue…
锁
锁(lock
)或者互斥锁(mutex
)是一种结构,用来保证一段代码在同一时刻只有一个线程执行。它们通常被用来保证多线程访问同一可变数据结构时的数据一致性。主要有下面几种锁:
- 阻塞锁(
Blocking locks
):常见的表现形式是当前线程会进入休眠,直到被其他线程释放。 - 自旋锁(
Spinlocks
):使用一个循环不断地检查锁是否被释放。如果等待情况很少话这种锁是非常高效的,相反,等待情况非常多的情况下会浪费 CPU 时间。 - 读写锁(
Reader/writer locks
):允许多个读线程同时进入一段代码,但当写线程获取锁时,其他线程(包括读取器)只能等待。这是非常有用的,因为大多数数据结构读取时是线程安全的,但当其他线程边读边写时就不安全了。 - 递归锁(
Recursive locks
):允许单个线程多次获取相同的锁。非递归锁被同一线程重复获取时可能会导致死锁、崩溃或其他错误行为。
Swift 可用的锁的 Api
pthread_mutex_t
是一个可选择性地配置为递归锁的阻塞锁;pthread_rwlock_t
是一个阻塞读写锁;DispatchQueue
~dispatch_queue_t~ 可以用作阻塞锁,也可以通过使用 barrier block 配置一个同步队列作为读写锁,还支持异步执行加锁代码;OperationQueue
可以用作阻塞锁。与 dispatch_queue_t 一样,支持异步执行加锁代码。NSLock
是 Objective-C 类的阻塞锁,它的同伴类 NSRecursiveLock 是递归锁。OSSpinLock
顾名思义,是一个自旋锁。
最后,Objective-C 中的 @synchronized
是一个阻塞递归锁。
因为 pthread
API 在 Swift 中不太好用,而且功能并不比其它 API 多。因此可能更多的在 C 和 Objective-C 中使用,因为它们又好用又高效。所以本文就不讨论前两个了。
NSLock
是一个简单的锁定类,易于使用且效率很高。如果需要显式锁定和解锁,那可以用它替代 DispatchQueue
。但在大多数情况下不需要使用它。
OSSpinLock
对于经常使用锁定、竞争较少且锁定代码运行速度快的用户来说,是一个很好的选择。它的开销非常少,有助于提升性能。如果代码可能会在很长一段时间内保持锁定或竞争很多,那最好不要用这个 API,因为这会浪费 CPU 时间。通常来说,你可以先使用 DispatchQueue
,如果这块出现了性能问题,再考虑换成 OSSpinLock
。
DispatchQueue
~dispatch_queue_t~已经重新更名为 DispatchQueue
其受欢迎程度可见一斑。
除了常用的 DispatchQueue.main.async {}
它能有各种各样的使用:
- 默认提供了
sync{}
async{}
这些api,所以在操作属性时候不需要显式调用lock
unlock
的函数。 - 提供了许多其他有用的 API,如使用单个
dispatch_async
在后台执行被锁定的代码 - 设置
定时器
或其他作用于queue
的事件源,以便它们自动执行锁定 - 作为
NotificationCenter
观察者,因为对Observer
的要求是Any
1
2
3
NotificationCenter.default.addObserver(self, selector: #selector(oA), name: NSNotification.Name.init("haha"), object: nil)
NotificationCenter.default.post(name: NSNotification.Name.init("haha"), object: nil)
- 使用
OperationQueue
的属性underlyingQueue
作为NSURLSession
代理。
developer.apple.com/documentation/foundation/nsoperationqueue/underlyingqueue
Event 的实践
关于本文章的背景,对于 Event 的线程冲突,最小模型和解决方法应该是下边这样:
采用并发队列解决多个读操作的性能问题,然后共享互斥锁来解决写操作的数据竞争问题。对于 iOS 来说它就是 GCD 中的写栏栅 barrier 机制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Dic {
let serialQueue = DispatchQueue(label: "DicSyncQueue", attributes: .concurrent)
private var _data = [String: String]()
var data: [String: String] {
get {
return serialQueue.sync {
return _data
}
}
set {
serialQueue.async(flags: .barrier) {
self._data = newValue
}
}
}
func data(key: String) -> String? {
serialQueue.sync {
return _data[key]
}
}
func set(key: String, vaule newValue: String) {
serialQueue.async(flags: .barrier) {
self._data[key] = newValue
}
}
}
let dic = Dic()
DispatchQueue.concurrentPerform(iterations: 100) { index in
dic.set(key: "\(index % 100)", vaule: "\(arc4random() % 10)")
}
print("🧩🧩--\(dic.data.count)--🧩--\(Date())🧩")
print("🧩🧩--\(dic.data)--🧩--\(Date())🧩")
上面的代码将打印出正确的数据:
1
2
🧩🧩--100--🧩--2020-03-05 09:20:09 +0000--74🧩
🧩🧩--["87": "7", "56": "5", "7": "8", "70": "9", "60": "6", "62": "8", "47": "9", "76": "2", "4": "3", "3": "9", "63": "0", "27": "3", "72": "7", "9": "1", "11": "5", "32": "9", "22": "7", "96": "5", "44": "2", "36": "2", "29": "3", "93": "1", "19": "0", "0": "0", "85": "5", "90": "1", "38": "4", "42": "9", "33": "1", "18": "5", "16": "8", "12": "5", "69": "5", "86": "7", "78": "9", "82": "8", "84": "2", "57": "4", "53": "4", "14": "6", "8": "7", "13": "9", "46": "0", "1": "0", "54": "5", "6": "4", "40": "7", "41": "3", "66": "1", "92": "9", "15": "6", "98": "0", "31": "1", "28": "4", "64": "1", "94": "0", "67": "3", "35": "7", "99": "4", "26": "1", "83": "3", "45": "2", "30": "8", "49": "2", "37": "0", "97": "3", "51": "5", "68": "7", "21": "5", "95": "4", "55": "4", "24": "1", "10": "0", "73": "5", "65": "4", "52": "6", "81": "3", "61": "3", "77": "7", "50": "2", "48": "8", "25": "5", "75": "3", "39": "9", "17": "3", "59": "7", "43": "3", "91": "8", "5": "8", "20": "1", "23": "1", "88": "3", "79": "8", "71": "4", "80": "7", "2": "3", "34": "9", "89": "3", "74": "2", "58": "8"]--🧩--2021-03-05 09:20:09 +0000🧩
DispatchSemaphore
DispatchSemaphore
是一个使用线程信号量进行线程操作的类。
例如初始化信号量为 0,然后添操作(wait,信号量-1当前小于零,被冻结)。当发出信号(single,信号量加+),即执行操作。
例如登陆的情况。
- 登陆api得到用户信息
- 登陆之后使用用户请求其他多个api,当多个API都成功返回则进入页面
实际的业务可能不是这样,但是类似的…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import UIKit
class A {
var userID = String()
var amount = Double()
var address = String()
var cardNumber = String()
func test() {
let semaphore = DispatchSemaphore(value: 0)
print("🧩🧩--\("login")--🧩--\(Date())--\(#function)--\(#line)🧩")
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
// api call complete
self.userID = "user"
semaphore.signal()
}
semaphore.wait()
print("🧩🧩--\("login success")--🧩--\(Date())--\(#function)--\(#line)🧩")
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
// api call complete
self.amount = 100
semaphore.signal()
}
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
// api call complete
self.address = "Xxx xx Ste 100"
semaphore.signal()
}
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
// api call complete
self.cardNumber = "1000 1000 1000 1000"
semaphore.signal()
}
semaphore.wait()
semaphore.wait()
semaphore.wait()
print("🧩🧩--\("data request success")--🧩--\(Date())--\(#function)--\(#line)🧩")
print(self.userID)
print(self.amount)
print(self.address)
print(self.cardNumber)
}
}
A().test()
上述代码将打印如下结果。
1
2
3
4
5
6
7
🧩🧩--login--🧩--2021-03-11 07:27:57 +0000--test()--13🧩
🧩🧩--login success--🧩--2021-03-11 07:27:59 +0000--test()--22🧩
🧩🧩--data request success--🧩--2021-03-11 07:28:01 +0000--test()--43🧩
user
100.0
Xxx xx Ste 100
1000 1000 1000 1000
永远不要在主线程上运行 wait()
函数,因为它将冻结您的应用程序。
wait()
函数使我们可以指定超时。一旦达到超时,无论信号量计数值如何,等待都会结束。
OperationQueue
这个跟 DispatchQueue
虽然是哥俩,但是地位应该比大哥差很多,因为他的 API 复杂,用起来不方便。
奇效在使用继承和自定义上,通过自定义 cancel
complete
可以方便的控制每个节点的结束和取消,在管理依赖的方面非常方便。
虽然 abandon 的 用处非常少,但是还是写一下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
fileprivate protocol WaitToFinishOperationControlProtocol {
func complete()
func abandon()
}
fileprivate final class WaitToFinishOperation: Operation, WaitToFinishOperationControlProtocol {
func complete() { self.done = true }
func abandon() { self.abandoned = true; self.done = true }
private var block: ((WaitToFinishOperationControlProtocol) -> Void) = { _ in }
private var done = false {
willSet {
self.didChangeValue(forKey: "isExecuting")
self.willChangeValue(forKey: "isFinished")
}
didSet {
self.didChangeValue(forKey: "isExecuting")
self.didChangeValue(forKey: "isFinished")
}
}
private var abandoned = false {
willSet {
self.didChangeValue(forKey: "isCancelled")
}
didSet {
self.didChangeValue(forKey: "isCancelled")
}
}
convenience init(_ block: @escaping (WaitToFinishOperationControlProtocol) -> Void) {
self.init()
self.block = block
self.done = false
}
override func main() { block(self) }
override var isFinished: Bool { done }
override var isExecuting: Bool { !done }
override var isCancelled: Bool { abandoned }
override var isAsynchronous: Bool { true }
}
最后
借着解决问题复习了一下理解。借用 swift.gg/locks-thread-safety-and-swift/ 的结尾吧:
Swift 语言层面并不支持线程同步,但是 Apple 的系统框架有很多好用的 API。GCD
和 DispatchQueue
非常好用,并且Swift 中的 API 也是如此。虽然 Swift 里没有 @synchronized
和原子属性,但我们有其他更好的选择。