您好,登錄后才能下訂單哦!
小編給大家分享一下iOS中如何實(shí)現(xiàn)多網(wǎng)絡(luò)請(qǐng)求的線程安全,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
在iOS 網(wǎng)絡(luò)編程有一種常見(jiàn)的場(chǎng)景是:我們需要并行處理二個(gè)請(qǐng)求并且在都成功后才能進(jìn)行下一步處理。下面是部分常見(jiàn)的處理方式,但是在使用過(guò)程中也很容易出錯(cuò):
DispatchGroup:通過(guò) GCD 機(jī)制將多個(gè)請(qǐng)求放到一個(gè)組內(nèi),然后通過(guò) DispatchGroup.wait()
和 DispatchGroup.notify()
進(jìn)行成功后的處理。
OperationQueue:為每一個(gè)請(qǐng)求實(shí)例化一個(gè) Operation 對(duì)象,然后將這些對(duì)象添加到 OperationQueue ,并且根據(jù)它們之間的依賴關(guān)系決定執(zhí)行順序。
同步 DispatchQueue:通過(guò)同步隊(duì)列和 NSLock 機(jī)制避免數(shù)據(jù)競(jìng)爭(zhēng),實(shí)現(xiàn)異步多線程中同步安全訪問(wèn)。
第三方類庫(kù):Futures/Promises 以及響應(yīng)式編程提供了更高層級(jí)的并發(fā)抽象。
在多年的實(shí)踐過(guò)程中,我意識(shí)到上面這些方法這些方法都存在一定的缺陷。另外,要想完全正確的使用這些類庫(kù)還是有些困難。
并發(fā)編程中的挑戰(zhàn)
使用并發(fā)的思維思考問(wèn)題很困難:大多數(shù)時(shí)候,我們會(huì)按照讀故事的方式來(lái)閱讀代碼:從第一行到最后一行。如果代碼的邏輯不是線性的話,可能會(huì)給我們?cè)斐梢欢ǖ睦斫怆y度。在單線程環(huán)境下,調(diào)試和跟蹤多個(gè)類和框架的程序執(zhí)行已經(jīng)是非常頭疼的一件事了,多線程環(huán)境下這種情況簡(jiǎn)直不敢想象。
數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題:在多線程并發(fā)環(huán)境下,數(shù)據(jù)讀取操作是線程安全的而寫(xiě)操作則是非線程安全。如果發(fā)生了多個(gè)線程同時(shí)對(duì)某個(gè)內(nèi)存進(jìn)行寫(xiě)操作的話,則會(huì)發(fā)生數(shù)據(jù)競(jìng)爭(zhēng)導(dǎo)致潛在數(shù)據(jù)錯(cuò)誤。
理解多線程環(huán)境下的動(dòng)態(tài)行為本身就不是一件容易的事,找出導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)的線程就更為麻煩。雖然我們可以通過(guò)互斥鎖機(jī)制解決數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題,但是對(duì)于可能修改的代碼來(lái)說(shuō)互斥鎖機(jī)制的維護(hù)會(huì)是一件非常困難的事。
難以測(cè)試:并發(fā)環(huán)境下很多問(wèn)題并不會(huì)在開(kāi)發(fā)過(guò)程中顯現(xiàn)出來(lái)。雖然 Xcode 和 LLVM 提供了Thread Sanitizer 這類工具用于檢查這些問(wèn)題,但是這些問(wèn)題的調(diào)試和跟蹤依然存在很大的難度。因?yàn)椴l(fā)環(huán)境下除了代碼本身的影響外,應(yīng)用也會(huì)受到系統(tǒng)的影響。
處理并發(fā)情形的簡(jiǎn)單方法
考慮到并發(fā)編程的復(fù)雜性,我們應(yīng)該如何解決并行的多個(gè)請(qǐng)求?
最簡(jiǎn)單的方式就是避免編寫(xiě)并行代碼而是講多個(gè)請(qǐng)求線性的串聯(lián)在一起:
let session = URLSession.shared session.dataTask(with: request1) { data, response, error in // check for errors // parse the response data session.dataTask(with: request2) { data, response error in // check for errors // parse the response data // if everything succeeded... callbackQueue.async { completionHandler(result1, result2) } }.resume() }.resume()
為了保持代碼的簡(jiǎn)潔,這里忽略了很多的細(xì)節(jié)處理,例如:錯(cuò)誤處理以及請(qǐng)求取消操作。但是這樣將并無(wú)關(guān)聯(lián)的請(qǐng)求線性排序其實(shí)暗藏著一些問(wèn)題。例如,如果服務(wù)端支持 HTTP/2 協(xié)議的話,我們就沒(méi)發(fā)利用 HTTP/2 協(xié)議中通過(guò)同一個(gè)鏈接處理多個(gè)請(qǐng)求的特性,而且線性處理也意味著我們沒(méi)有好好利用處理器的性能。
關(guān)于 URLSession 的錯(cuò)誤認(rèn)知
為了避免可能的數(shù)據(jù)競(jìng)爭(zhēng)和線程安全問(wèn)題,我將上面的代碼改寫(xiě)為了嵌套請(qǐng)求。也就是說(shuō)如果將其改為并發(fā)請(qǐng)求的話:請(qǐng)求將不能進(jìn)行嵌套,兩個(gè)請(qǐng)求可能會(huì)對(duì)同一塊內(nèi)存進(jìn)行寫(xiě)操作而數(shù)據(jù)競(jìng)爭(zhēng)非常難以重現(xiàn)和調(diào)試。
解決改問(wèn)題的一個(gè)可行辦法是通過(guò)鎖機(jī)制:在一段時(shí)間內(nèi)只允許一個(gè)線程對(duì)共享內(nèi)存進(jìn)行寫(xiě)操作。鎖機(jī)制的執(zhí)行過(guò)程也非常簡(jiǎn)單:請(qǐng)求鎖、執(zhí)行代碼、釋放鎖。當(dāng)然要想完全正確使用鎖機(jī)制還是有一些技巧的。
但是根據(jù) URLSession 的文檔描述,這里有一個(gè)并發(fā)請(qǐng)求的更簡(jiǎn)單解決方案。
init(configuration: URLSessionConfiguration, delegate: URLSessionDelegate?, delegateQueue queue: OperationQueue?)
[…]
queue : An operation queue for scheduling the delegate calls and completion handlers. The queue should be a serial queue, in order to ensure the correct ordering of callbacks. If nil, the session creates a serial operation queue for performing all delegate method calls and completion handler calls.
這意味所有 URLSession 的實(shí)例對(duì)象包括 URLSession.shared 單例的回調(diào)并不會(huì)并發(fā)執(zhí)行,除非你明確的傳人了一個(gè)并發(fā)隊(duì)列給參數(shù) queue 。
URLSession 拓展并發(fā)支持
基于上面對(duì) URLSession 的新認(rèn)知,下面我們對(duì)其進(jìn)行拓展讓它支持線程安全的并發(fā)請(qǐng)求(完成代碼地址)。
enum URLResult { case response(Data, URLResponse) case error(Error, Data?, URLResponse?) } extension URLSession { @discardableResult func get(_ url: URL, completionHandler: @escaping (URLResult) -> Void) -> URLSessionDataTask } // Example let zen = URL(string: "https://api.github.com/zen")! session.get(zen) { result in // process the result }
首先,我們使用了一個(gè)簡(jiǎn)單的 URLResult 枚舉來(lái)模擬我們可以在 URLSessionDataTask 回調(diào)中獲得的不同結(jié)果。該枚舉類型有利于我們簡(jiǎn)化多個(gè)并發(fā)請(qǐng)求結(jié)果的處理。這里為了文章的簡(jiǎn)潔并沒(méi)有貼出 URLSession.get(_:completionHandler:)
方法的完整實(shí)現(xiàn),該方法就是使用 GET 方法請(qǐng)求對(duì)應(yīng)的 URL 并自動(dòng)執(zhí)行 resume()
最后將執(zhí)行結(jié)果封裝成 URLResult 對(duì)象。
@discardableResult func get(_ left: URL, _ right: URL, completionHandler: @escaping (URLResult, URLResult) -> Void) -> (URLSessionDataTask, URLSessionDataTask) { }
該段 API 代碼接受兩個(gè) URL 參數(shù)并返回兩個(gè) URLSessionDataTask 實(shí)例。下面代碼是函數(shù)實(shí)現(xiàn)的第一段:
precondition(delegateQueue.maxConcurrentOperationCount == 1, "URLSession's delegateQueue must be configured with a maxConcurrentOperationCount of 1.")
因?yàn)樵趯?shí)例化 URLSession 對(duì)象時(shí)依舊可以傳入并發(fā)的 OperationQueue 對(duì)象,所以這里我們需要使用上面這段代碼將這種情況排除掉。
var results: (left: URLResult?, right: URLResult?) = (nil, nil) func continuation() { guard case let (left?, right?) = results else { return } completionHandler(left, right) }
將這段代碼繼續(xù)添加到實(shí)現(xiàn)中,其中定義了一個(gè)表示返回結(jié)果的元組變量 results 。另外,我們還在函數(shù)內(nèi)部定義了另一個(gè)工具函數(shù)用于檢查是否兩個(gè)請(qǐng)求都已經(jīng)完成結(jié)果處理。
let left = get(left) { result in results.left = result continuation() } let right = get(right) { result in results.right = result continuation() } return (left, right)
最后將這段代碼追加到實(shí)現(xiàn)中,其中我們分別對(duì)兩個(gè) URL 進(jìn)行了請(qǐng)求并在請(qǐng)求都完成后一次返回了結(jié)果。值得注意的是這里我們通過(guò)兩次執(zhí)行 continuation()
來(lái)判斷請(qǐng)求是否全部完成:
第一次執(zhí)行 continuation()
時(shí)因?yàn)槠渲幸粋€(gè)請(qǐng)求并未完成結(jié)果為 nil 所以回調(diào)函數(shù)并不會(huì)執(zhí)行。
第二次執(zhí)行的時(shí)候兩個(gè)請(qǐng)求全部完成,執(zhí)行回調(diào)處理。
接下來(lái)我們可以通過(guò)簡(jiǎn)單的請(qǐng)求來(lái)測(cè)試下這段代碼:
extension URLResult { var string: String? { guard case let .response(data, _) = self, let string = String(data: data, encoding: .utf8) else { return nil } return string } } URLSession.shared.get(zen, zen) { left, right in guard case let (quote1?, quote2?) = (left.string, right.string) else { return } print(quote1, quote2, separator: "\n") // Approachable is better than simple. // Practicality beats purity. }
并行悖論
我發(fā)現(xiàn)解決并行問(wèn)題最簡(jiǎn)單最優(yōu)雅的方法就是盡可能的少使用并發(fā)編程,而且我們的處理器非常適合執(zhí)行那些線性代碼。但是如果將大的代碼塊或任務(wù)拆分為多個(gè)并行執(zhí)行的小代碼塊和任務(wù)將會(huì)讓代碼變得更加易讀和易維護(hù)。
以上是“iOS中如何實(shí)現(xiàn)多網(wǎng)絡(luò)請(qǐng)求的線程安全”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。