彩票走势图

logo 移动开发学习指南
文档彩票走势图>>移动开发学习指南>>iOS开发文集iOS 10 Day By Day: Thread Sanitizer线程检查工具

iOS开发文集iOS 10 Day By Day: Thread Sanitizer线程检查工具


通常,这些是由于多个线程同时访问相同的一些内存而造成的。我猜想,线程问题是许多开发人员做噩梦的原因。他们是出了名的难以追踪,错误只发生在特定条件下:所以确定问题的根源是非常复杂的。

通常导致线程问题的原因是所谓的“竞争条件”。我们不会去关注太多的细节,像是这意味着什么,而是从谷歌引用ThreadSanitizer手册:


数据竞争发生在当两个线程同时访问同一变量,并且至少有一个访问是编写状态时。


这些用来追踪的是一个绝对的噩梦,但值得庆幸的是Xcode附带一个新的调试工具叫做Thread Sanitizer,甚至可以在你注意到他们之前帮助识别这些问题。

The Project

我们将创建一个简单的应用程序,使我们能够存款和取款100美元面额。像往常一样,项目的完成版本已在GitHub上(为了方便各位读者,小编已经为大家整理了,请点击这里下载)。

The Account

我们的Account模式非常简单:

import Foundation
class Account {
    var balance: Int = 0
    func withdraw(amount: Int, completed: () -> ()) {
        let newBalance = self.balance - amount
        if newBalance < 0 {
            print("You don't have enough money to withdraw \(amount)")
            return
        }
        // Simulate processing of fraud checks
        sleep(2)
        self.balance = newBalance
        completed()
    }
    func deposit(amount: Int, completed: () -> ()) {
        let newBalance = self.balance + amount
        self.balance = newBalance
        completed()
    }
}

它包含几个方法使我们能够取款和存款到我们的账户。存款和取款金额是硬编码$100。

deposit方法已经几乎立即执行,然而,withdraw还需要一段时间才能完成。我们会说这是因为我们需要为取款执行一些欺诈检查,但实际上我们只发送当前线程睡眠2秒。这将给我们后面使用一些多线程提供借口。

唯一需要注意的另一件事是完成模块,这是当存款和取款都成功完成时才执行。

视图控制器

我们的视图控制器由两个按钮——存款和取款,以及一个显示当前余额的标签组成。故事板的布局:

iOS开发文集iOS 10 Day By Day: Thread Sanitizer线程检查工具

为连接我们的UI元素,我们有一个IBOutlet,引用平衡标签和以用户当前的平衡更新标签的方法。

import UIKit
class ViewController: UIViewController {
    @IBOutlet var balanceLabel: UILabel!
    let account = Account()
    override func viewDidLoad() {
        super.viewDidLoad()
        updateBalanceLabel()
    }
    @IBAction func withdraw(_ sender: UIButton) {
        self.account.withdraw(amount: 100, onSuccess: updateBalanceLabel)
    }
    @IBAction func deposit(_ sender: UIButton) {
        self.account.deposit(amount: 100, onSuccess: updateBalanceLabel)
    }
    func updateBalanceLabel() {
        balanceLabel.text = "Balance: $\(account.balance)"
    }
}

让我们给它一个旋转:

iOS开发文集iOS 10 Day By Day: Thread Sanitizer线程检查工具

嗯……当我们试着取回钱时有点慢!这是由于我们Account 的withdraw方法及其严格的“欺诈检查”,导致主线程阻塞,直到该方法已经完成。我们希望用户能够以最小的延迟反复点击“Deposit”和“Withdraw”。

救援调度队列

如果我们可以从主线程删除阻塞的withdraw方法,这就太棒了。我们将使用新“Swiftified”中央调度库:

func withdraw(amount: Int, onSuccess: () -> ()) {
    DispatchQueue(label: "com.shinobicontrols.balance-moderator").async {
        let newBalance = self.balance - amount
        if newBalance < 0 {
            print("You don't have enough money to withdraw \(amount)")
            return
        }
        // Simulate processing of fraud checks
        sleep(2)
        self.balance = newBalance
        DispatchQueue.main.async {
            onSuccess()
        }
    }
}

让我们再次运行它:

iOS开发文集iOS 10 Day By Day: Thread Sanitizer线程检查工具

等一等!我们的钱哪里去了?我们存入100美元,取回了100美元然后,剩下0,尽管开始时是100美元!

我们有信心我们的方法按预期的运行(因为他们是单元测试),它看起来就像我们的withdraw任务调度到背景队列引发了一个问题。

Thread Sanitizer线程检查工具来拯救我们的理智!

打开检查工具非常简单,只需将你的目标的计划设置和在Diagnostics标签中检查Thread Sanitizer箱。我们可以选择在遇到的问题上暂停,这使得它能够容易地在个案基础上评估每一个问题。我们会这样。

iOS开发文集iOS 10 Day By Day: Thread Sanitizer线程检查工具
iOS开发文集iOS 10 Day By Day: Thread Sanitizer线程检查工具

由于线程检查工具只在运行时起作用,我们需要重新编译和重新运行应用程序。让我们开始吧。


在WWDC上,苹果建议在你所有的单元测试开启线程检查工具。检查工具在运行时操作,如果代码执行,只能够确定数据竞争。如果你的代码完全得以单元测试,那么你可能会发现线程检查工具发现了大多数问题,如果不是全部测试,发现的是你的项目的竞态条件 (你会发现我们博客的iOS 9 Day by Day中一个有用的阅读,Xcode 7的代码覆盖工具)。

其他值得注意的是,它只能运行在语言版本3编写的Swift代码上(Objective-C也可兼容),并且只能使用64位模拟器运行。


当我们重复我们之前取款的过程,然后立即存款,线程检查工具会暂停我们的应用程序的执行,因为它发现了竞态条件。这给了我们一个很好的冲突访问发生的地方的堆栈跟踪。

iOS开发文集iOS 10 Day By Day: Thread Sanitizer线程检查工具

它还将结果输出到控制台,所以你没有必要从Xcode运行检查工具。

通过堆栈跟踪和提供的信息,线程分析仪有助于表明,当访问Account.balance属性时在我们的Account.deposit和Account.withdraw方法中有一个数据竞争。哦,看来我们需要在withdraw和deposit方法中使用相同的串行调度队列:

我们将修改我们的Account类来使用共享队列:

class Account {
    var balance: Int = 0
    private let queue = DispatchQueue(label: "com.shinobicontrols.balance-moderator")
    func withdraw(amount: Int, onSuccess: () -> ()) {
        queue.async {
            // Same as earlier...
        }
    }
    func deposit(amount: Int, onSuccess: () -> ()) {
        queue.async {
            let newBalance = self.balance + amount
            self.balance = newBalance
            DispatchQueue.main.async {
                onSuccess()
            }
        }
    }
}

再次运行应用程序显示了我们仍然有数据竞争,但是它不再是在我们的Account类中,而是由于我们的ViewController从主线程访问balance。

iOS开发文集iOS 10 Day By Day: Thread Sanitizer线程检查工具

我们可以通过转换到一个只有访问Account的私有变量来保护我们的balance属性,而不是用我们的队列返回balance。

private var _balance: Int = 0
var balance: Int {
    return queue.sync {
        return _balance
    }
}

我们需要转换任何书面到平衡变量以使用私有_balance属性。

现在当我们运行我们的应用程序,我们应该能够多次点击“withdraw”和“deposit”而无需令人不安的线程检查工具。太好了,我们刚刚使用这个新工具来修正了我们的错误代码。

进一步的阅读

虽然看起来似乎不像起初那样,线程检查工具可能会成为开发人员工具箱中一个非常重要的iOS工具。它发现数据竞争的能力,即使在程序的运行期间没有发生,也可能会拯救无数小时调试断断续续的线程问题的时间。

像往常一样,苹果的WWDC大会很丰富,值得一看。sanitizer是Clang编译器的一部分,在LLVM网站上可以找到更详细的信息,在谷歌建立了sanitizer的团队有许多有趣的wiki页面,其中包括了算法用于检测线程问题的高层次的演练。


我们使用Swift 3中提供给我们的一个小的新面貌GCD。苹果也在“Concurrent Programming With GCD in Swift 3”谈话中谈到了这个,你可能会发现它的用处。此外,Roy Marmelstein写了一篇很好且简洁的帖子阐述这一变化。


本文翻译自:

 

PS: 关于移动开发,这些产品你可以关注>>
关于移动开发的最新资讯和产品推荐,请<>!
扫码咨询


添加微信 立即咨询

电话咨询

客服热线
023-68661681

TOP