Swift线程安全问题讨论
一直扑在业务开发上耗电,最终只会电量用尽,花点时间充电必不可少
在阅读AFNetworking
的Swift
版本的Alomofire
的时候,看到有@Protected
修饰词
1
2
3
/// Protected `MutableState` value that provides thread-safe access to state values.
@Protected
fileprivate var mutableState = MutableState()
Property Wrappers
- Proposal: SE-0258
- Status: Implemented (Swift 5.1)
常见属性赋值的非原子性问题
在这里,我们可以尝试实现一个OC里面的atomic
功能,如下:保证了get和set的原子性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@propertyWrapper
final class Atomic<T> {
private let queue = DispatchQueue(label: "com.rambotest.atomic")
private var value: T
init(wrappedValue: T) {
self.value = wrappedValue
}
var wrappedValue: T {
get { queue.sync { value } }
set { queue.sync { value = newValue } }
}
}
我们以下面的例子为项目示例,进行全局的问题演示和解决
异步执行全局变量的赋值操作崩溃
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class RAMTest {}
class ViewController: UIViewController {
var objc = RAMTest()
var x = 0
private let queue = DispatchQueue(label: "com.concurrent1.atomic", attributes: .concurrent)
func requestTest(_ sender: Any) {
for _ in 0...1000 {
queue.async {
self.objc = RAMTest()
}
}
}
}
奔溃原因分析可以见头条的文章分析:头条稳定性治理:ARC 环境中对 Objective-C 对象赋值的 Crash 隐患
简单讲就是ARC下赋值操作是会保留旧值再赋值新值最后释放旧值的多步奏进行,如果多线程操作对象赋值,就会导致旧值出现多次过度release的情况而崩溃。
我们可以用我们新建的Atomic属性包装器来对objc进行封装,如下:
1
@Atomic var objc = RAMTest()
再次运行测试代码奔溃消失。
多线程下赋值再取值操作的不可控问题
1
2
3
4
5
6
7
8
9
10
x = 0
for _ in 0..<1000 {
queue.async {
self.x += 1
}
}
// 延迟执行,等for中的异步线程全部执行结束
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
print(self.x)
}
预期值应该是1000,但实际上,最后的值并不可控,因为递增x
不是原子的,因为它首先调用get
然后set
。
我们可以通过在属性包装器中封装方法
1
2
3
4
5
func mutate(_ mutation: (inout Value) -> Void) {
return queue.sync {
mutation(&value)
}
}
然后将方法改下
1
2
3
4
5
6
7
8
9
10
x = 0
for _ in 0..<1000 {
queue.async {
self._x.mutate { $0 += 1 }
}
}
// 延迟执行,等for中的异步线程全部执行结束
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
print(self.x)
}
此问题在OC中也常见
优化一下属性包装器
细心的同学肯定发现,属性包装器中封装的方法mutate
,需要使用_x
才能访问的到。这种方式其实不是很好的帮我进行额外的需要的扩展能力。
这里我们可以利用属性包装器的projectedValue
来投影属性,使用美元符号$
的语法糖来调用projectedValue
1
2
3
self._x.mutate { $0 += 1 }
// 可以改成如下,使用美元符号
self.$x.mutate { $0 += 1 }
学习下Protected
defer
defer
这篇文章介绍挺好的,defer
所声明的 block 会在当前代码(当前大括号内)行退出后被调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
extension Lock {
// 线程安全的执行闭包, 并把闭包的返回值返回给调用者
func around<T>(_ closure: () -> T) -> T {
lock(); defer { unlock() }
return closure()
}
// 线程安全的执行闭包
func around(_ closure: () -> Void) {
lock(); defer { unlock() }
closure()
}
}
Lock
Alamofire
主要是使用自旋锁os_unfair_lock_t
实现的锁的能力封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// An `os_unfair_lock` wrapper.
final class UnfairLock: Lock {
private let unfairLock: os_unfair_lock_t
init() {
unfairLock = .allocate(capacity: 1)
unfairLock.initialize(to: os_unfair_lock())
}
deinit {
unfairLock.deinitialize(count: 1)
unfairLock.deallocate()
}
fileprivate func lock() {
os_unfair_lock_lock(unfairLock)
}
fileprivate func unlock() {
os_unfair_lock_unlock(unfairLock)
}
}
@dynamicMemberLookup
动态属性查找,和OC的runtime有点相似。python、js其实就是这种方式的。
@dynamicMemberLookup
,它指示 Swift 在访问属性时调用下标方法。此下标方法subscript(dynamicMember:)
是必需的,您将获得所请求属性的字符串名称,并且可以返回您喜欢的任何值。
1
2
3
4
5
6
7
8
9
10
11
12
@dynamicMemberLookup
struct Person {
subscript(dynamicMember member: String) -> String {
let properties = ["name": "Taylor Swift", "city": "Nashville"]
return properties[member, default: ""]
}
}
let taylor = Person()
print(taylor.name) // Taylor Swift
print(taylor.city) // Nashville
print(taylor.favoriteIceCream) //
没有@dynamicMemberLookup
是没法实现这种下标语法的。
参考文章
Swift - 属性包装器(@propertyWrapper)的使用
Swift Atomic Properties with Property Wrappers
【Alamofire】【Swift】属性包装器注解@propertyWrapper