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 的写法分为以下几种:

  1. 直接声明一个静态常量属性存储单例(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()
    }
}
  1. 使用我司常见的单例加锁初始化方式,但同样不把对属性 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()
    }
}
  1. 使用我司常见的单例加锁初始化方式,但把对属性 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()
        }
    }
}
  1. 直接声明一个静态常量属性存储单例(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()
        }
    }
}

上面几种不同写法的运行结果是:

  1. 互斥锁和串行队列都不用。崩溃
  2. 用互斥锁,单例写法为我司的 get 和 optional 结合的方式
    1. 只在获取单例全局属性的地方,也就是 sharedManager 加上互斥锁。会出现崩溃
    2. 在改变属性值的地方也加上互斥锁。不会崩溃
  3. 用串行队列同步任务,单例写法为直接赋值。不会崩溃
  4. 单例获取的地方用互斥锁(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.

想要保证对某个属性值的操作是线程安全的,就只能对这些操作加锁或者放到串行队列同步任务里面

Read more

联通 FTTR 宽带从路由器设置自动重启和穿墙功率

联通 FTTR 宽带从路由器设置自动重启和穿墙功率

几个月前把家里宽带换成了联通的千兆 FTTR 宽带,包含一主一从两个点位。配套光猫设备是华为的星光 F50 尊享版。 主点位放置在客厅茶几上,方便连接电视。从点位放在卧室门口,那里恰好有一个不耽误过路的小拐角可以放路由器。平常我们基本不在客厅活动,其他区域最近的 Wi-Fi 信号源是从路由器,因此我们大多数的设备连接的都是从路由器。从路由器的工作负荷很大。 从路由器个头小主路由器很多,散热不咋地。工作时间久了发热就容易发生数据包堵塞,丢包延迟高。需要把它电源拔掉重启。从宽带开通到现在,数据包堵塞影响网络的情况每个月会发生一次。有一次还影响了居家办公的视频会议。宽带维修师傅也给不出有效的法子,建议就是定期插拔从路由器电源。 从路由器和书房之间隔了两堵墙。信号到我书桌那个位置时,千兆网速已经衰减到只有 400-500Mbps 了,折损将近一半。叠加路由器发热的 debuff,书桌位置的网速最差的时候几乎和百兆宽带差不多。 我尝试过在光猫后台管理将路由器功率设置到「穿墙」模式,但没有任何作用。今天在后台研究了一番发现,原来我之前设置的功率是仅对主路由器生效,从路由器还是标准功率。要修

By Gray
《漫步华尔街(第12版)》读书笔记

《漫步华尔街(第12版)》读书笔记

股票分析 基本面分析 * 基本面分析的四个基本决定因素 * 预期增长率 * 复合增长(复利)对投资决策有很重要的意义。 * 一只股票的股利增长和盈利增长率越高,理性投资者应愿意为其支付越高的价格。 * 推论:一只股票的超常增长率持续时间越长,理性投资者应愿意为其支付越高的价格。 * 预期股利支付率 * 对于预期增长率相同的两只股票来说,持有股利支付率越高的股票,较之股利支付率低的股票,会使你的财务状况更好。 * 在其他条件相同的情况下,一家公司发放的现金股利占其盈利的比例越高,理性投资者应愿意为其股票支付越高的价格。 * 特例,很多处于强劲增长阶段的公司,往往不支付任何股利。这时候不满足「在其他条件相同的情况下」。 * 风险程度 * 在其他条件相同的情况下,一家公司的股票风险越低,理性投资者(以及厌恶风险的投资者)应愿意为其股票支付越高的价格。 * 市场利率水平 * 在其他条件相同的情况下,市场利率越低,理性投资者应愿意为股票支付越高的价格。 * 举例,银行存款利率

By Gray