iOS App 程式開發

設計多執行緒 (Multi-Threaded) App 處理連續的資料流量更有效率!

設計多執行緒 (Multi-Threaded) App 處理連續的資料流量更有效率!
設計多執行緒 (Multi-Threaded) App 處理連續的資料流量更有效率!
In: iOS App 程式開發, Swift 程式語言
本篇原文(標題:Designing Multi-Threaded Applications Using Swift)刊登於作者 Medium,由 Jimmy M Andersson 所著,並授權翻譯及轉載。

multi-threaded

身為一名在汽車產業裡的 iOS 開發者,我花了不少時間處理即時資料。現今許多 App 都需要有效率地處理連續的資料流量,為了確保不會卡住使用者介面,你很可能需要使用多執行緒來開發。

處理即時的資料流量非常有趣,因為你會不斷收到可以用來更新視覺畫面的新資料。這也是開發時最困難、最令人沮喪的事情,因為 iOS 裝置有一些硬體上的限制。幸運的是,Apple 透過非常容易使用的 GCD (Grand Central Dispatch) 介面,提供多執行緒 (Multi-Thread) 功能。你可能會對下面的程式碼感到熟悉:

DispatchQueue.main.async {
  // Place a work item in the GCD main queue and then
  // move on to the next statement in your code.
}

如果沒有明確地將程式碼放入到其他佇列 (Queue) 的話,你大部分的程式碼都會在主佇列 (Main Queue) 執行。主佇列是一個有次序的佇列,也就是說它會選擇這行佇列的第一個項目來執行,然後等到執行結束後再將其釋放,接著再選擇佇列裡的下一個項目,如此類推。

多執行緒 (Multi-threaded) 與並行 (Concurrency)

然而,主佇列並不是 GCD 內唯一可以使用的佇列。當中有一些預先定義好的佇列,而它們有著不同的優先層級。你也可以創建自己特定的佇列,像是這樣:

let myConcurrentQueue = DispatchQueue(label: "ConcurrentQueue",
                                      qos: .background,
                                      attributes: .concurrent,
                                      autoreleaseFrequency: .workItem,
                                      target: nil)

你會注意到我們創建的佇列中有個 .concurrent 屬性,這表示佇列不會等待一個項目執行結束後才執行下一個項目。它會把項目放入一個執行緒並開始執行,接著不論第一個項目是否已經完成,都會移動到下一個項目。

讓我們說點技術性的東西 ⋯⋯

比方說,你正在處理採樣率為 20Hz 左右的資料流,這意味著你會有大約 50 毫秒的時間來解析及解譯資料、將資料加進資料結構、並通知視圖來呈現。如果你的 iOS 裝置嘗試在主執行緒上執行此操作,它就只有很少的時間去確認使用者是否正試著跟 App 互動,而且 App 也會變得反應遲鈍。所以,我們就要在這裡轉用多執行緒了。

假設我們正在用一個簡單的資料結構,來儲存收到的資料樣本,像是一個普通的整數陣列。然後,我們可能會想創建一個佇列並使用它,就像這樣:

// 這是先前的資料佇列,不過它有一個更高優先權的標記
let myDataQueue = DispatchQueue(label: "DataQueue",
                                qos: .userInitiated,
                                attributes: .concurrent,
                                autoreleaseFrequency: .workItem,
                                target: nil)                              
// 我們的資料結構,可能已在某處的 Data Manager 預先初始化了
var dataArray = [Int] ()

// 收到資料後,我們呼叫解析、儲存、更新的程式碼
myDataQueue.async {
  let parsedData = parseData(data)
  dataArray.append(parsedData)
  DispatchQueue.main.async {
    updateViews()
  }
}

這有用嗎?

這看起來不錯,對吧?現在我們正讓所有資料在背景執行緒上處理,而主執行緒只是用來更新視覺畫面。然而,這樣鐵定會造成閃退,為甚麼呢?這答案有點技術性,但這一點非常重要。

因為佇列是同步執行的,所以它會把工作項目丟到執行緒上,以作平行執行。我們正使用一個陣列來儲存資料,而 Swift 陣列是屬於結構型別 (Struct Type),也就是一種數值型別 (Value Type)。當你試著加入數值到陣列時,執行緒就會:

  1. 分配新的陣列,並從舊陣列中複製數值;
  2. 加入新資料;
  3. 將新的參照寫回至變數;
  4. 系統釋放舊陣列所使用的記憶體。

試想一下,如果兩個執行緒將相同的陣列複製過去,會發生甚麼事?它們會分別加入自己的資料到複製的陣列上,然後寫回新的參照到變數裡,可能一個執行緒先加入資料然後到另一個、又或是兩個執行緒同時加入資料。前者會讓我們的資料出錯,因為第一個執行緒寫入的資料並不會出現於第二個執行緒的陣列;而後者則會導致 App 閃退,因為兩個執行緒無法同時獲得對已分配記憶體的寫入權限。

考慮到這一點,我們可以使用 DispatchQuere 類別中一個相當聰明的架構 ── flag。現在,我們可以將程式碼改寫為:

let myDataQueue = DispatchQueue(label: "DataQueue",
                                qos: .userInitiated,
                                attributes: .concurrent,
                                autoreleaseFrequency: .workItem,
                                target: nil)

var dataArray = [Int] ()

// .barrier flag 告訴佇列,這個特定工作項目需要在沒有其他平行執行的項目時執行
myDataQueue.async(flags: .barrier) {
  let parsedData = parseData(data)
  dataArray.append(parsedData)
  DispatchQueue.main.async {
    // 這個方法可能會需要在某個時刻讀取資料,並且需要特定的方式來執行// 
    // 看看下面的實作
    updateViews()
  }
}

func updateViews() {
  let dataForViews = return myDataQueue.sync { return dataArray }
  // 使用 dataForViews 變數來進行更新  
  // 因為即使在更新期間更改了資料陣列
  // 它也能保持不變
}

這可能看起來有點複雜,讓我解釋一下。

使用了 .barrier flag 後,每當我們添加一個透過寫入來更改資料結構的項目時,我們都會告訴佇列這個特定的工作項目需要單獨執行。這表示佇列需要等到所有正在運作的執行緒結束後,才執行這個項目,然後等這個項目執行完畢,再開始平行執行程式碼。

當主執行緒需要讀取資料來更新視圖時,它需要透過一個同步呼叫來完成資料佇列。不然,一個正在寫入的執行緒就有可能破壞主執行緒正在讀取的資料。

總結

希望你明白本篇文章,並獲得一些新知識。在幾天後再閱讀一次本篇文章能夠幫助你理解當中的概念,也讓你有機會去反思一下。

如果你有任何疑問,歡迎在下面留言。

想學習更多關於 iOS 開發相關的知識,你可以看看我之前的文章 “iOS Development and the Wrong Kind of MVC”

本篇原文(標題:Designing Multi-Threaded Applications Using Swift)刊登於作者 Medium,由 Jimmy M Andersson 所著,並授權翻譯及轉載。
作者簡介:Jimmy M Andersson 是一名軟件開發人員,在活躍於汽車行業的 NIRA Dynamics 中負責數據採集。他開發了監控和日誌記錄 App,來演示及可視化公司的產品組合和其功能。他目前正在進修資訊科技碩士學位,目標是以數據科學專業畢業。他每週都會在 Medium 發表軟件開發文章。你也可以在 Twitter Email 聯絡他。
譯者簡介:楊敦凱-目前於科技公司擔任 iOS Developer,工作之餘開發自有 iOS App同時關注網路上有趣的新玩意、話題及科技資訊。平時的興趣則是與自身專業無關的歷史、地理、棒球。來信請寄到:[email protected]
作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
更多來自 AppCoda 中文版
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。