你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> iOS 10 Day by Day 2:線程競態檢測工具 Thread Sanitizer

iOS 10 Day by Day 2:線程競態檢測工具 Thread Sanitizer

編輯:IOS開發基礎

本文介紹了 Xcode 8 的新出的多線程調試工具 Thread Sanitizer,可以在 app 運行時發現線程競態。

《iOS 10 day by day》是 shinobicontrols 公司編寫的系列博客,介紹開發者需要了解的 iOS 10 新特性,每周更新。本系列翻譯(文集地址)已取得官方授權。倉薯翻譯,歡迎指正:)

Shinobicontrols 為 iOS 和 Android 開發者提供高性能、響應式的 UI 控件 SDK,尤其是圖表方面的控件。 官網 : shinobicontrols.com  twitter : @shinobicontrols

想想一下,你的 app 已經近乎大功告成:它經過精良的打磨,單元測試全覆蓋。只剩下一個問題:有一個很嚴重的 bug,但是是偶發的,你已經花了好幾個小時嘗試修復它卻一無所獲。問題到底出在哪裡呀?

這種情況經常是多個線程訪問同一塊內存造成的。我可以大膽猜測,多線程的 bug 是許多程序員的夢魇。這類 bug 非常難定位,而且只有特定條件下才能重現:所以找出問題的原因確實困難重重。

而問題的原因常常是所謂的『線程競態』。對這個名詞我們不再多費筆墨去解釋了,以下摘自 Google 的 ThreadSanitizer 文檔:

兩個線程同時訪問同一個變量,而且其中至少一個線程要做的是寫操作,這種情況就叫競態。

調試競態問題曾經讓程序員們大為頭疼;不過值得慶幸的是,Xcode 發布了一個新的線程調試工具叫做 Thread Sanitizer 可以檢測出這類問題,甚至比你發現得還早。

建工程

我們做了一個簡單的應用,能讓用戶存錢、取錢,每次 $100。跟之前一樣,最終版的工程放在 Github 上了。

銀行賬戶

銀行賬戶的數據模型很簡單,名為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
        }

        // 模仿銀行的防偽驗證過程
        sleep(2)

        self.balance = newBalance

        completed()
    }

    func deposit(amount: Int, completed: () -> ()) {
        let newBalance = self.balance + amount
        self.balance = newBalance

        completed()
    }
}

裡面只包含了這麼幾個方法,能讓我們給賬戶存錢、取錢。存取的金額寫死為 $100。

其中,deposit方法是立即返回的,而withdraw方法要花一點時間才能執行完。我們名義上說是因為銀行要執行防偽驗證,背後其實就是讓線程 sleep 了 2 秒。這在後面能給我們一個使用多線程的借口。

另外一點要注意的是 completed block,在存取成功之後執行。

View Controller

View Controller 裡有兩個 button ——一個存錢、一個取錢——還有一個 label,顯示當前賬戶余額。Storyboard 中的布局是這樣的:

227290-1e49672eb7c048a2.png

Storyboard的界面

從 Storyboard 中引出顯示余額 label 的 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)"
    }
}

來試一下吧:

227290-0072970edd4351b3.gif

有延遲地存取

嗯……取錢的過程有點慢呀。這是因為我們所寫的withdraw方法裡有嚴格的『防偽驗證』機制,在方法結束前會一直 block 主線程。而我們希望的是用戶能快速重復存錢、取錢,把延遲降到最低。

Dispatch Queue 登場了

如果要是能把withdraw方法從主線程移出來,就解決這問題了。我們可以用上新出的『Swift 化』的 GCD 庫:

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
        }

        // 模仿銀行的防偽驗證過程
        sleep(2)

        self.balance = newBalance

        DispatchQueue.main.async {
            onSuccess()
        }
    }
}

再跑一次:

227290-92b025979e9f2643.gif

無延遲地存取

等一下,我們的錢呢?一開始賬戶余額是 $100,我們先取了 $100,然後存了 $100,怎麼賬戶余額只剩下 0 了呢?

存取方法肯定是沒問題的(剛才都分別測過了),看起來問題就出在把 withdraw 的任務放到單獨線程這一步。

Thread Sanitizer 來解救我們啦!

開啟 Thread Sanitizer 很簡單,只需點擊 target 的 Edit Scheme...,然後在 Diagnostics tab 下勾選 Thread Sanitizer。可以選擇 Pause on issues,這樣比較方便一步步調試問題。我們把它勾上。

227290-74bbbdb64832f3c2.png

Edit scheme

227290-08340b87870f35aa.png

勾選 Thread Sanitizer

因為 thread sanitizer 只在運行時工作,我們需要把工程重新編譯、重新跑一下。來試試吧。

在 WWDC 演講中,蘋果推薦在所有的單元測試裡都打開 thread sanitizer。Sanitizer 只在運行時有效,而且必須要代碼運行到那兒才能檢測出線程競態。如果你的代碼單元測試覆蓋率很高,那麼 Thread Sanitizer 能找出工程裡絕大部分的線程競態(可以參考下我們在 iOS 9 Day by Day 裡寫過的 Xcode 7 的測試覆蓋工具)。

.

還要注意的一點是,對於 Swift 這個工具只對 Swift 3 的代碼有效(Objective-C 也兼容),而且只能用 64 位的模擬器來跑。

現在我們再把之前的操作重復一遍,先取錢,再馬上存錢。這時候 thread sanitizer 把 app 暫停了,因為它發現了線程競態。它清晰地展現出了沖突發生時的調用棧。

227290-e19e48a069420aef.png

調用棧

而且,它在控制台裡打印出了相關信息。

通過調用棧和打印出的信息,Thread Analyzer 給力地幫我們定位了問題所在: Account.deposit 方法與 Account.widthdraw 方法會訪問同一個屬性 Account.balance,從而出現了競態。哎呀,看樣子我們應該把存錢和取錢放在同一個線程裡進行。

我們修改一下 Account 類的代碼,用一個公共的 queue:

class Account {
    var balance: Int = 0
    private let queue = DispatchQueue(label: "com.shinobicontrols.balance-moderator")

    func withdraw(amount: Int, onSuccess: () -> ()) {
        queue.async {
            // 跟之前一樣...
        }
    }

    func deposit(amount: Int, onSuccess: () -> ()) {
        queue.async {
            let newBalance = self.balance + amount
            self.balance = newBalance

            DispatchQueue.main.async {
                onSuccess()
            }
        }
    }
}

再跑一遍代碼,發現還是有競態;只不過這次不是在 Account 類裡,而是由 ViewController 類在主線程訪問 balance 造成的。

227290-7aec6f70c3c9b401.png

調用棧

為解決這個問題,我們可以把 balance 屬性改成 private 保護起來,只能在 Account 類內部訪問它,然後改用 queue 來返回結果。

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

之前所有對 balance 屬性的寫操作都要改成私有的 _balance

現在再跑一遍,再怎麼重復點擊 "withdraw" 和 "deposit" 都不會驚動 Thread Sanitizer 了。太棒啦——我們用這個工具修好了多線程的 bug。

擴展閱讀

盡管看著不起眼,Thread Sanitizer 還是很有可能會成為 iOS 開發者的一個重要工具。它能在程序運行沒出錯的情況下就找到線程競態,可以為你省下大把時間 debug 間歇出現的多線程問題。

一如既往,蘋果的 WWDC 演講 信息量很大,值得一看。Sanitizer 是 Clang 編譯器的一部分,更詳細的信息可以參考 LLVM 的官網,還有 Google 開發 sanitizer 的團隊編寫了許多有趣的 wiki,其中包括對檢測多線程問題算法的簡單介紹。

我們用到了一點 Swift 3 新出的 GCD 語法。Apple 在Swift 3 的 GCD 並發編程的演講中對此有所介紹,可以看一看。另外,Roy Marmelstein 也有一篇短小精悍的博客介紹其中的變化。

如果有任何問題和評論,我們都很歡迎你的反饋。可以發我 tweet @sam_burnstone,也可以關注 @shinobicontrols 關注最新動態以及 iOS 10 Day by Day 系列的更新。感謝閱讀!

原文地址:iOS 10 Day by Day :: Day 2 :: Thread Sanitizer

原作者:Sam Burnstone @sam_burnstone

ShinobiControls 官網:ShinobiControls.com twitter : @shinobicontrols

文集地址:iOS 10 day by day 倉薯翻譯

本文地址:http://www.jianshu.com/p/358535119e9b

譯者:戴倉薯



文章轉自 戴倉薯的簡書

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved