文章

Swift线程安全问题讨论

一直扑在业务开发上耗电,最终只会电量用尽,花点时间充电必不可少

在阅读AFNetworkingSwift版本的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()
            }
        }
    }
}

image-20231102171110081

奔溃原因分析可以见头条的文章分析:头条稳定性治理: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

Swift线程安全变量实现

头条稳定性治理:ARC 环境中对 Objective-C 对象赋值的 Crash 隐患

How to use Dynamic Member Lookup in Swift

本文由作者按照 CC BY 4.0 进行授权