2017-08-16 140 views
4

我在斯威夫特3中创建一个简单单的单身getter和setter:线程安全的

class MySingleton { 
    private var myName: String 
    private init() {} 
    static let shared = MySingleton() 

    func setName(_ name: String) { 
     myName = name 
    } 

    func getName() -> String { 
     return myName 
    } 
} 

因为我做了init()私有的,也宣告shared实例是static let,我想初始化是线程安全的。但是myName的getter和setter函数又是如何线程安全的?

+0

我认为它不是线程安全的,即使它是单例或不是 –

+0

如何使线程安全? –

+0

添加串行调度队列作为该类的成员,并对其执行get/set操作 – Alexander

回答

6

你说得对,你写的那些getter不是线程安全的。在Swift中,目前最简单(最安全)的方法是使用Grand Central Dispatch队列作为锁定机制。最简单(也是最容易推理的)方法是使用基本的串行队列。

class MySingleton { 

    static let shared = MySingleton() 

    // Serial dispatch queue 
    private let lockQueue = DispatchQueue(label: "MySingleton.lockQueue") 

    private var _myName: String 
    var myName: String { 
     get { 
      var result: String! 

      lockQueue.sync { 
       result = self._myName 
      } 

      return result 
     } 

     set { 
      lockQueue.sync { 
       self._myName = newValue 
      } 
     } 
    } 

    private init() { 
     _myName = "initial name" 
    } 
} 

使用串行调度队列将保证先进先出执行以及实现对数据的“锁定”。也就是说,数据在更改时无法读取。在这种方法中,我们使用sync来执行数据的实际读取和写入,这意味着调用者将总是被迫等待其轮到,类似于其他锁定基元。

注意:这不是most performant的方法,但它很容易阅读和理解。这是一个很好的通用目的解决方案,以避免竞争条件,但并不意味着为并行算法开发提供同步。

来源: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html What is the Swift equivalent to Objective-C's "@synchronized"?

+0

值得一提的是,没有什么比“快速实现这一点的最佳方式”存在。线程安全或并发编程与使用的编程语言无关,并且与单例构造完全无关。第一个要回答的问题不应该是如何,而是为什么你需要同步你的数据。如果没有优势,最好避免任何并行或并发。你的回答几乎是正确的,问题很模糊...... – user3441734

+0

@ user3441734由于GCD(或其他技术)在所有环境中都不可用,我会争辩说存在连接。这个问题使用单例作为前提,但最终这个问题与单身人士无关。说“最好避免任何并行或并发”是非常糟糕的建议。事实上,为了避免iOS中的并发比简单地接受和理解它更具挑战性。 –

+0

我写道:“如果没有一些优势,最好避免......”我是GCD的一大乐趣,我几乎在所有项目中都使用它。 – user3441734

5

稍微不同的方式来做到这一点(这是从一个Xcode 9游乐场)是使用并发队列,而不是一个串行队列。

final class MySingleton { 
    static let shared = MySingleton() 

    private let nameQueue = DispatchQueue(label: "name.accessor", qos: .default, attributes: .concurrent) 
    private var _name = "Initial name" 

    private init() {} 

    var name: String { 
     get { 
      var name = "" 
      nameQueue.sync { 
       name = _name 
      } 

      return name 
     } 
     set { 
      nameQueue.async(flags: .barrier) { 
       self._name = newValue 
      } 
     } 
    } 
} 
  • 使用并发队列意味着多个读取来自多个线程未阻止彼此。由于在获取时没有突变,因此可以同时读取该值,因为...
  • 我们正在使用.barrier异步分派来设置新值。该块可以异步执行,因为调用者不需要等待值的设置。该块将不会运行,直到其前面的并发队列中的所有其他块完成。所以,当这个setter等待运行时,现有的挂起读操作不会受到影响。障碍意味着当它开始运行时,不会有其他块运行。有效地,在setter的持续时间内将队列变成一个串行队列。在此模块完成之前不能进一步读取。当程序块完成后,新的值已经被设置,在这个setter之后添加的任何获得者现在可以同时运行。
+2

我非常喜欢这种变化。据我了解,就数据一致性而言,它将是“正确的”。唯一不同的是,对于一个初学者来说,对setter的调用语义会有些违反直觉,他们可能会在“获取锁定”时期待延迟。基本上,在执行器执行后“有效设置”和“实际设置”,但这并不重要。 –

+0

@AllenHumphreys,使用“你的”(连续)或“Abizern”(障碍)的方法取决于很多因素。在并发队列上使用屏障并不是违反直觉的,它是不同的。它有一些优势,也有一些劣势。有时他们两人都可能失败。 – user3441734