Swift 单例中的线程安全问题
单例是常见的一种设计模式。最近在编写单例代码的时候,发现公司很多同事的 Swift 单例写法都是这样的,
extension NSObject {
@discardableResult
static func kep_synchronized<T>(_ lock: AnyObject, closure: () -> T) -> T {
objc_sync_enter(lock)
defer { objc_sync_exit(lock) }
return closure()
}
}
class DefaultDict: NSObject {
private static var manager: DefaultDict?
static var sharedManager: DefaultDict {
get {
var newShared = manager
kep_synchronized(self) {
if newShared == nil {
newShared = DefaultDict()
manager = newShared
}
}
return newShared!
}
}
}
先是对 NSObject 进行了拓展,加了个锁方法(应该是互斥锁)。随后在单例类里面加了两个属性,一个是实际上的单例属性(初始是nil),另一个是只提供 get 方法的只读属性,并且在这个只读属性里面加上了锁。
一开始我以为这个锁是为了线程安全加上的,但后来研究了一下发现这个锁其实是没有必要的。
下面是实验代码(注意不要在 Playground 里面试验,直接新建工程,把代码写在 viewDidAppear里面):
extension NSObject {
@discardableResult
static func kep_synchronized<T>(_ lock: AnyObject, closure: () -> T) -> T {
objc_sync_enter(lock)
defer { objc_sync_exit(lock) }
return closure()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let count = 10000
for index in 0..<count {
DefaultDict.sharedManager.set(value: index, key: String(index))
}
DispatchQueue.concurrentPerform(iterations: count) { index in
print("value will read with index \(String(index))")
if let n = DefaultDict.sharedManager.object(key: String(index)) as? Int {
print("value read with index \(String(n))")
}
}
DefaultDict.sharedManager.reset()
DispatchQueue.concurrentPerform(iterations: count) { index in
print("value will set with index \(String(index))")
DefaultDict.sharedManager.set(value: index,key: String(index))
print("value set with index \(String(index))")
}
}
}
对类 DefaultDict 的写法分为以下几种:
- 直接声明一个静态常量属性存储单例(
static let
),对单例属性 dict 的处理不放在串行队列里面
class DefaultDict {
private var dict:[String: Any] = [:]
public static let sharedManager = DefaultDict()
private init() {
}
public func set(value: Any, key: String) {
dict[key] = value
}
public func object(key: String) -> Any? {
dict[key]
}
public func reset() {
print("reset")
dict.removeAll()
}
}
- 使用我司常见的单例加锁初始化方式,但同样不把对属性 dict 的处理放串行队列里面
class DefaultDict: NSObject {
private var dict:[String: Any] = [:]
private static var manager: DefaultDict?
static var sharedManager: DefaultDict {
get {
var newShared = manager
kep_synchronized(self) {
if newShared == nil {
newShared = DefaultDict()
manager = newShared
}
}
return newShared!
}
}
override init() {
super.init()
}
public func set(value: Any, key: String) {
dict[key] = value
}
public func object(key: String) -> Any? {
dict[key]
}
public func reset() {
print("reset")
dict.removeAll()
}
}
- 使用我司常见的单例加锁初始化方式,但把对属性 dict 的处理放互斥锁里面
class DefaultDict: NSObject {
private var dict:[String: Any] = [:]
private static var manager: DefaultDict?
static var sharedManager: DefaultDict {
get {
var newShared = manager
kep_synchronized(self) {
if newShared == nil {
newShared = DefaultDict()
manager = newShared
}
}
return newShared!
}
}
override init() {
super.init()
}
public func set(value: Any, key: String) {
Self.kep_synchronized(self) {
self.dict[key] = value
}
}
public func object(key: String) -> Any? {
var result: Any?
Self.kep_synchronized(self) {
result = self.dict[key]
}
return result
}
public func reset() {
print("reset")
Self.kep_synchronized(self) {
self.dict.removeAll()
}
}
}
- 直接声明一个静态常量属性存储单例(
static let
),把对单例属性 dict 的处理放在串行队列里面
class DefaultDict: NSObject {
private var dict:[String: Any] = [:]
private let serialQueue = DispatchQueue(label: "serialQueue")
static var sharedManager = DefaultDict()
override init() {
super.init()
}
public func set(value: Any, key: String) {
serialQueue.sync {
self.dict[key] = value
}
}
public func object(key: String) -> Any? {
var result: Any?
serialQueue.sync {
result = self.dict[key]
}
return result
}
public func reset() {
print("reset")
serialQueue.sync {
self.dict.removeAll()
}
}
}
上面几种不同写法的运行结果是:
- 互斥锁和串行队列都不用。崩溃
- 用互斥锁,单例写法为我司的 get 和 optional 结合的方式
- 只在获取单例全局属性的地方,也就是 sharedManager 加上互斥锁。会出现崩溃
- 在改变属性值的地方也加上互斥锁。不会崩溃
- 用串行队列同步任务,单例写法为直接赋值。不会崩溃
- 单例获取的地方用互斥锁(get 和 optional 结合),改变值的地方用串行队列。不会崩溃
所以,在获取单例静态变量的地方加锁并不能保证他是线程安全的,只能保证多个线程获取到的单例静态变量是同一个。但是这种写法应该是多余的。Swift 中 let 声明的变量都是线程安全的,所以直接用static let sharedManager = DefaultDict()
这种方式声明单例就可以了。
如果是为了懒加载,而使用我司那种 get 和 optional 结合的方式声明单例的话,那更没必要了。因为 Swift 中的全局常量和变量都是懒加载的
Global constants and variables are always computed lazily, in a similar manner to Lazy Stored Properties. Unlike lazy stored properties, global constants and variables don’t need to be marked with the lazy modifier.
想要保证对某个属性值的操作是线程安全的,就只能对这些操作加锁或者放到串行队列同步任务里面