iOS中多网络请求的线程安全详解

前言

在iOS 网络编程有一种常见的场景是:我们需要并行处理二个请求并且在都成功后才能进行下一步处理。下面是部分常见的处理方式,但是在使用过程中也很容易出错:

  • DispatchGroup:通过 GCD 机制将多个请求放到一个组内,然后通过 DispatchGroup.wait() DispatchGroup.notify() 进行成功后的处理。
  • OperationQueue:为每一个请求实例化一个 Operation 对象,然后将这些对象添加到 OperationQueue ,并且根据它们之间的依赖关系决定执行顺序。
  • 同步 DispatchQueue:通过同步队列和 NSLock 机制避免数据竞争,实现异步多线程中同步安全访问。
  • 第三方类库:Futures/Promises 以及响应式编程提供了更高层级的并发抽象。

在多年的实践过程中,我意识到上面这些方法这些方法都存在一定的缺陷。另外,要想完全正确的使用这些类库还是有些困难。

并发编程中的挑战

使用并发的思维思考问题很困难:大多数时候,我们会按照读故事的方式来阅读代码:从第一行到最后一行。如果代码的逻辑不是线性的话,可能会给我们造成一定的理解难度。在单线程环境下,调试和跟踪多个类和框架的程序执行已经是非常头疼的一件事了,多线程环境下这种情况简直不敢想象。

数据竞争问题:在多线程并发环境下,数据读取操作是线程安全的而写操作则是非线程安全。如果发生了多个线程同时对某个内存进行写操作的话,则会发生数据竞争导致潜在数据错误。

理解多线程环境下的动态行为本身就不是一件容易的事,找出导致数据竞争的线程就更为麻烦。虽然我们可以通过互斥锁机制解决数据竞争问题,但是对于可能修改的代码来说互斥锁机制的维护会是一件非常困难的事。

难以测试:并发环境下很多问题并不会在开发过程中显现出来。虽然 Xcode 和 LLVM 提供了Thread Sanitizer这类工具用于检查这些问题,但是这些问题的调试和跟踪依然存在很大的难度。因为并发环境下除了代码本身的影响外,应用也会受到系统的影响。

处理并发情形的简单方法

考虑到并发编程的复杂性,我们应该如何解决并行的多个请求?

最简单的方式就是避免编写并行代码而是讲多个请求线性的串联在一起:

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()

为了保持代码的简洁,这里忽略了很多的细节处理,例如:错误处理以及请求取消操作。但是这样将并无关联的请求线性排序其实暗藏着一些问题。例如,如果服务端支持 HTTP/2 协议的话,我们就没发利用 HTTP/2 协议中通过同一个链接处理多个请求的特性,而且线性处理也意味着我们没有好好利用处理器的性能。

关于 URLSession 的错误认知

为了避免可能的数据竞争和线程安全问题,我将上面的代码改写为了嵌套请求。也就是说如果将其改为并发请求的话:请求将不能进行嵌套,两个请求可能会对同一块内存进行写操作而数据竞争非常难以重现和调试。

解决改问题的一个可行办法是通过锁机制:在一段时间内只允许一个线程对共享内存进行写操作。锁机制的执行过程也非常简单:请求锁、执行代码、释放锁。当然要想完全正确使用锁机制还是有一些技巧的。

但是根据 URLSession 的文档描述,这里有一个并发请求的更简单解决方案。

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 的实例对象包括 URLSession.shared 单例的回调并不会并发执行,除非你明确的传人了一个并发队列给参数 queue 。

URLSession 拓展并发支持

基于上面对 URLSession 的新认知,下面我们对其进行拓展让它支持线程安全的并发请求(完成代码地址)。

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
}

首先,我们使用了一个简单的 URLResult 枚举来模拟我们可以在 URLSessionDataTask 回调中获得的不同结果。该枚举类型有利于我们简化多个并发请求结果的处理。这里为了文章的简洁并没有贴出 URLSession.get(_:completionHandler:) 方法的完整实现,该方法就是使用 GET 方法请求对应的 URL 并自动执行 resume() 最后将执行结果封装成 URLResult 对象。

@discardableResult
func get(_ left: URL, _ right: URL, completionHandler: @escaping (URLResult, URLResult) -> Void) -> (URLSessionDataTask, URLSessionDataTask) {

}

该段 API 代码接受两个 URL 参数并返回两个 URLSessionDataTask 实例。下面代码是函数实现的第一段:

 precondition(delegateQueue.maxConcurrentOperationCount == 1,
  "URLSession's delegateQueue must be configured with a maxConcurrentOperationCount of 1.")

因为在实例化 URLSession 对象时依旧可以传入并发的 OperationQueue 对象,所以这里我们需要使用上面这段代码将这种情况排除掉。

var results: (left: URLResult?, right: URLResult?) = (nil, nil)

func continuation() {
 guard case let (left?, right?) = results else { return }
 completionHandler(left, right)
}

将这段代码继续添加到实现中,其中定义了一个表示返回结果的元组变量 results 。另外,我们还在函数内部定义了另一个工具函数用于检查是否两个请求都已经完成结果处理。

let left = get(left) { result in
 results.left = result
 continuation()
}

let right = get(right) { result in
 results.right = result
 continuation()
}

return (left, right)

最后将这段代码追加到实现中,其中我们分别对两个 URL 进行了请求并在请求都完成后一次返回了结果。值得注意的是这里我们通过两次执行 continuation() 来判断请求是否全部完成:

  • 第一次执行 continuation() 时因为其中一个请求并未完成结果为 nil 所以回调函数并不会执行。
  • 第二次执行的时候两个请求全部完成,执行回调处理。

接下来我们可以通过简单的请求来测试下这段代码:

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.
}

并行悖论

我发现解决并行问题最简单最优雅的方法就是尽可能的少使用并发编程,而且我们的处理器非常适合执行那些线性代码。但是如果将大的代码块或任务拆分为多个并行执行的小代码块和任务将会让代码变得更加易读和易维护。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

作者:Adam Sharp,时间:2017/9/21

翻译:BigNerdCoding, 如有错误欢迎指出。原文链接

(0)

相关推荐

  • IOS 网络请求中设置cookie

    IOS 网络请求中设置cookie 1. ASIHTTPRequest ASIHTTPRequest 是一款极其强劲的 HTTP 访问开源项目.让简单的 API 完成复杂的功能,如:异步请求,队列请求,GZIP 压缩,缓存,断点续传,进度跟踪,上传文件,HTTP 认证. cookie的支持 如果 Cookie 存在的话,会把这些信息放在 NSHTTPCookieStorage 容器中共享,并供下次使用.你可以用 [ ASIHTTPRequest setSessionCookies:nil ] ;

  • 详解iOS中多个网络请求的同步问题总结

    场景描述:我们同时发出了a.b.c 3个网络请求,我们希望在a.b.c 3个网络请求都结束的时候获得一个通知. 常见解决方法:通过度娘目前找到两种做法:1.通过添加标识来判断请求是否全部结束 2.dispatch_group + 信号量 本篇文章demo 1.添加标识的解决方法 在遇到这个问题时首先想到了唐巧大大的猿题库团队开源的网络框架YTKNetwork,然后阅读源码发现YTKNetwork是通过添加标识来实现网络请求的批量请求处理: 话不多说直接上代码在YTKNetwork里负责进行网络批

  • iOS中多网络请求的线程安全详解

    前言 在iOS 网络编程有一种常见的场景是:我们需要并行处理二个请求并且在都成功后才能进行下一步处理.下面是部分常见的处理方式,但是在使用过程中也很容易出错: DispatchGroup:通过 GCD 机制将多个请求放到一个组内,然后通过 DispatchGroup.wait() 和 DispatchGroup.notify() 进行成功后的处理. OperationQueue:为每一个请求实例化一个 Operation 对象,然后将这些对象添加到 OperationQueue ,并且根据它们之

  • Android 网络请求框架Volley实例详解

    Android 网络请求框架Volley实例详解 首先上效果图 Logcat日志信息on Reponse Volley特别适合数据量不大但是通信频繁的场景,像文件上传下载不适合! 首先第一步 用到的RequetQueue RequestQueue.Java RequestQueue请求队列首先得先说一下,ReuqestQueue是如何对请求进行管理的...RequestQueue是对所有的请求进行保存...然后通过自身的start()方法开启一个CacheDispatcher线程用于缓存调度,开

  • 微信小程序之网络请求简单封装实例详解

    微信小程序之网络请求简单封装实例详解 在微信小程序中实现网络请求相对于Android来说感觉简单很多,我们只需要使用其提供的API就可以解决网络请求问题. 普通HTTPS请求(wx.request) 上传文件(wx.uploadFile) 下载文件(wx.downloadFile) WebSocket通信(wx.connectSocket) 为了数据安全,微信小程序网络请求只支持https,当然各个参数的含义就不在细说,不熟悉的话可以:可以去阅读官方文档的网络请求api,当我们使用request

  • http proxy 对网络请求进行代理使用详解

    目录 正文 命令行启动服务器 详细的调用栈 捕捉错误 正文 使用下面这段简单的代码对网络请求进行代理: const http = require('http'); const httpProxy = require('http-proxy'); const targetUrl = 'https://www.sap.cn/index.html'; const proxy = httpProxy.createProxyServer({ target: targetUrl, }); http.crea

  • IOS中计算缓存文件的大小判断实例详解

    IOS中计算缓存文件的大小判断实例详解 IOS中计算缓存文件的大小判断,在这里分享一下自己的心得,希望和大家一起分享技术,如果有什么不足,还请大家指正.写出这篇目的,就是希望大家一起成长,我也相信技术之间没有高低,只有互补,只有分享,才能使彼此更加成长. 实例代码: //获取缓存文件路径 -(NSString *)getCachesPath{ // 获取Caches目录路径 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCaches

  • 浅谈IOS中AFNetworking网络请求的get和post步骤

    1.首先通过第三方:CocoaPods下载AFNetworking 1.1.先找到要查找的三方库:pod search + AFNetworking 1.2.出来一堆列表页面,选择三方库最新版本命令,例如: pod 'MBProgressHUD','~>0.8'  (:q 返回) 1.3.创建工程,进入工程: cd + 工程路径 1.4.编辑工程的Podfile文件: vim Podfile 1.5.(platform :iOS, '8.0'
target "工程名" do
po

  • IOS网络请求之NSURLSession使用详解

    前言: 无论是Android还是ios都离不开与服务器交互,这就必须用到网络请求,记得在2013年做iOS的时候那时候用的ASIHTTPRequest框架,现在重新捡起iOS的时候ASIHTTPRequest已经停止维护,大家都在用AFNetWorking作为首选网络请求框架,之前的ASIHTTPRequest是基于NSURLConnection类实现的,早期的AFNetWorking也是基于NSURLConnection实现,后来iOS9 之后已经放弃了NSURLConnection,开始使用

  • Tomcat处理请求的线程模型详解

    目录 一.前言 二.tomcat结构 三.探讨tomcat是如何处理请求 1.初始化 2.如何处理客户端请求 总结 一.前言 JAVA后端项目,运行在容器tomcat中,由于现在springboot的内置tomcat容器,其默认配置屏蔽了很多对tomcat的认知,但是对tomcat的学习和认识是比较重要的,所以专门查资料加深了理解,本文主要讨论在springboot集成下的tomcat9的请求过程,线程模型为NIO. 二.tomcat结构 找了张结构图,每个模块的意思和作用就不详解了,可以搜其他

  • Swift网络请求库Alamofire使用详解

    前言 Alamofire是一个使用Swift开发的网络请求库,其开发团队是AFNetworking的原团队.它语法简洁,采用链式编程的思想,使用起来是相当的舒服.本质是基于NSURLSession进行封装.接下开我们就进入实战,开始学习Alamofire的使用. GET请求 常用的get请求示例以及请求结果 Alamofire.request("https://httpbin.org/get", method: .get, parameters: nil, encoding: URLE

  • android 网络请求库volley方法详解

    使用volley进行网络请求:需先将volley包导入androidstudio中 File下的Project Structrue,点加号导包 volley网络请求步骤: 1. 创建请求队列       RequestQueue queue = Volley.newRequestQueue(this); 2.创建请求对象(3种) StringRequest request = new StringRequest("请求方法","请求的网络地址","成功的网

随机推荐