Swift 3 中的 dispatch_once 在哪里?

Posted

技术标签:

【中文标题】Swift 3 中的 dispatch_once 在哪里?【英文标题】:Whither dispatch_once in Swift 3? 【发布时间】:2016-06-14 01:02:49 【问题描述】:

好的,所以我发现了 Xcode 8 中的新 Swifty Dispatch API。我很喜欢使用 DispatchQueue.main.async,并且我一直在浏览 Xcode 中的 Dispatch 模块以查找所有新 API。

但我也使用dispatch_once 来确保不会多次执行诸如单例创建和一次性设置之类的事情(即使在多线程环境中)...并且找不到dispatch_once在新的 Dispatch 模块中?

static var token: dispatch_once_t = 0
func whatDoYouHear() 
    print("All of this has happened before, and all of it will happen again.")
    dispatch_once(&token) 
        print("Except this part.")
    

【问题讨论】:

【参考方案1】:

从 Swift 1.x 开始,Swift 一直使用 dispatch_once behind the scenes 来执行全局变量和静态属性的线程安全延迟初始化。

所以上面的static var 已经在使用dispatch_once,这使得它有点奇怪(并且可能再次将它用作另一个dispatch_once 的标记。事实上,使用@987654328 确实没有安全的方法@ 没有这种递归,所以他们把它去掉了。相反,只使用它构建的语言特性:

// global constant: SomeClass initializer gets called lazily, only on first use
let foo = SomeClass()

// global var, same thing happens here
// even though the "initializer" is an immediately invoked closure
var bar: SomeClass = 
    let b = SomeClass()
    b.someProperty = "whatever"
    b.doSomeStuff()
    return b
()

// ditto for static properties in classes/structures/enums
class MyClass 
    static let singleton = MyClass()
    init() 
        print("foo")
    

所以,如果您一直使用dispatch_once 进行一次性初始化 并产生一些值,那就太好了——您可以将该值设置为全局变量或静态属性正在初始化。

但是,如果您使用dispatch_once 做不一定有结果的工作怎么办?您仍然可以使用全局变量或静态属性来做到这一点:只需将该变量的类型设为 Void

let justAOneTimeThing: () = 
    print("Not coming back here.")
()

如果访问全局变量或静态属性来执行一次性工作对您来说是不合适的——比如说,您希望您的客户在使用您的库之前调用“初始化我”函数——只需将该访问权限包装在一个函数中即可:

func doTheOneTimeThing() 
    justAOneTimeThing

请参阅migration guide 了解更多信息。

【讨论】:

考虑到单例“是一种将类的实例化限制为一个对象的设计模式”,将private 添加到类的构造函数中不是很好吗?请注意,如果在此类中有一个变量,您可能有多个实例为该变量保存不同的值。一旦构造函数是private,只有static let singleton 行可以初始化它。 如果我必须在 dispatch_once 块中使用实例变量怎么办? @InderKumarRathore :该评论实际上比表面上看到的要多得多。为什么不将其作为一个单独的问题发布? @rickster 在搜索 SO 后,我在 Apple Developer Fourm 上发帖。我会尽快发布解决方案。这是帖子:forums.developer.apple.com/thread/69433?sr=stream【参考方案2】:

虽然“lazy var”模式让我不再关心调度令牌,并且通常比 dispatch_once() 更方便,但我不喜欢它在调用站点上的样子:

_ = doSomethingOnce

我希望这个语句看起来更像是一个函数调用(因为它暗示了动作),但它看起来根本不是这样。此外,必须写 _ = 来明确丢弃结果很烦人。

有更好的办法:

lazy var doSomethingOnce: () -> Void = 
  print("executed once")
  return 
()

这使得以下事情成为可能:

doSomethingOnce()

这可能效率较低(因为它调用空闭包而不是仅仅丢弃 Void),但提高清晰度对我来说完全值得。

【讨论】:

与简单地使用Void 相同的小警告:如果有人在第一次调用之前写doSomethingOnce = ,它甚至不会调用它一次。 如果它包含的代码可以抛出和异常,请告知是否有一种方法可以删除在“doSomethinOnce”声明的站点生成的编译器错误。 "lazy var doSomethingOnce: () throws -> Void" @user2196409 函数抛出的错误可以在调用站点通过使用try 关键字标记调用来处理,就像普通的抛出函数一样(我在 Swift 4.2 中检查了这一点)。 【参考方案3】:

这里和互联网上的其他答案非常好,但我觉得这个小花絮也应该提到:

dispatch_once 的优点在于它的优化程度,基本上是在第一次运行后以一种我几乎不理解的方式修改代码,但我有理由保证这比设置和检查(真实的)要快得多全局令牌。

虽然令牌可以在 Swift 中合理地实现,但必须声明另一个存储的布尔值并不是那么好。更不用说线程不安全了。正如doc 所说,您应该使用“延迟初始化的全局”。是的,但为什么要把全局范围弄得乱七八糟,对吧?

在有人说服我有更好的方法之前,我倾向于在我将使用它的范围内或合理接近的范围内声明我的 do-once 闭包,如下所示:

private lazy var foo: Void = 
    // Do this once
()

基本上我的意思是“当我读到这个时,foo 应该是运行这个块的结果。”它的行为方式与全局 let 常量完全相同,只是在正确的范围内。而且更漂亮。然后我会在任何我喜欢的地方调用它,通过将它读入其他永远不会使用的东西。为此,我喜欢 Swift 的_。像这样:

_ = foo

这个非常酷的怪癖实际上已经存在了一段时间,但还没有看到太多的爱。它基本上在运行时将变量单独保留,作为未调用的闭包,直到有东西想看到它的Void 结果。在读取时,它调用闭包,将其丢弃并将其结果保存在foo 中。 Void 几乎不使用内存,因此后续读取(即_ = foo)在 CPU 上什么也不做。 (不要引用我的话,请有人检查一下程序集以确保!)拥有尽可能多的数量,并且 Swift 在第一次运行后基本上不再关心它!丢掉那个旧的dispatch_once_t,让你的很多代码都像圣诞节那天第一次打开它时一样漂亮!

我的一个问题是您可以在第一次读取之前将foo 设置为其他值,然后您的代码将永远被调用!因此,全局 let 常量可以防止这种情况发生。问题是,类范围内的常量不能很好地与self 一起使用,所以不要使用实例变量......但是说真的,你什么时候将 anything 设置为Void?? p>

那,你需要将返回类型指定为Void(),否则它仍然会抱怨self。谁来敲门?

lazy 只是为了让变量尽可能地懒惰,所以 Swift 不会直接在 init() 上运行它。

很时髦,只要你记得不要给它写信! :P

【讨论】:

您的方法和@rickster 方法之间存在细微差别。您的 "lazily initialized locals" 一次操作完成一次 per instance,而 "lazily initialized globals" 一旦操作完成一次 per输入。 See this gist【参考方案4】:

Swift 3.0 中的“dispatch_once”示例

第 1 步:只需将以下代码替换为您的 Singleton.swift(Singleton 类)

// Singleton Class
class Singleton: NSObject  
var strSample = NSString()

static let sharedInstance:Singleton = 
    let instance = Singleton ()
    return instance
 ()

// MARK: Init
 override init() 
    print("My Class Initialized")
    // initialized with variable or property
    strSample = "My String"


Singleton Sample Image

第 2 步:从 ViewController.swift 调用 Singleton

// ViewController.swift
    override func viewDidLoad() 
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        let mySingleton = Singleton.sharedInstance
        print(mySingleton.strSample)

        mySingleton.strSample = "New String"
        
        print(mySingleton.strSample)
        
        let mySingleton1 = Singleton.sharedInstance
        print(mySingleton1.strSample)

    

ViewController Sample Image

这样的输出

My Class Initialized
My String
New String
New String

【讨论】:

您能解释一下为什么这只发送一次吗?为什么访问 sharedInstance 的 2 个线程之间的竞争不会发生冲突?此外,作为“静态”,它在某个未知时间初始化 - 无论我们是否需要它。 dispatch_once 的好处是我可以控制它是否/何时执行。【参考方案5】:

在 Xcode 8 GA Swift 3 下编译

创建 dispatch_once 单例类实例的推荐且优雅的方式:

final class TheRoot 
static let shared = TheRoot()
var appState : AppState = .normal
...

使用它:

if TheRoot.shared.appState == .normal 
...

这些行有什么作用?

final - 所以类不能被覆盖、扩展,它也使代码运行得更快,间接更少。

static let shared = TheRoot() - 这一行执行惰性初始化,并且只运行一次。

这个解决方案是线程安全的。

【讨论】:

你能解释一下为什么这是“惰性初始化”吗?我在其他语言中使用静态变量的经验是,编译器会在加载附近的某个任意时间为它们插入初始化代码。据我所知,从不偷懒。 swift的定义在哪里?【参考方案6】:

根据Migration Guide:

免费函数 dispatch_once 在 Swift 中不再可用。在 Swift,您可以使用延迟初始化的全局变量或静态属性 获得与 dispatch_once 相同的线程安全和调用一次保证 提供。

例子:

  let myGlobal =  … global contains initialization in a call to a closure … ()

  // using myGlobal will invoke the initialization code only the first time it is used.
  _ = myGlobal  

【讨论】:

【参考方案7】:

线程安全的 dispatch_once:

private lazy var foo: Void = 
    objc_sync_enter(self)
    defer  objc_sync_exit(self) 

    // Do this once
()

【讨论】:

现有的惰性机制已经是线程安全的(在它最多执行一次的意义上)。这种额外的“线程安全”没有帮助,而且可能会造成伤害。 @polytopia 对于以下变量private lazy var foo: Void = print("hello") (),此代码将多次打印“hello”(仅在模拟器上重现):for _ in 0 ... 1000 DispatchQueue.global(qos: .default).async _ = self.foo

以上是关于Swift 3 中的 dispatch_once 在哪里?的主要内容,如果未能解决你的问题,请参考以下文章

Swift Facebook 登录在 dispatch_once 中被破坏

如何在 Swift 中只执行一次代码?

如何在 Swift 中实现单例类

模拟dispatch_once

目标c单例dispatch_once实现更好?

iOS 的单例模式 dispatch_once