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

2020-01-21 01:44:55丽君

前言

在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 协议中通过同一个链接处理多个请求的特性,而且线性处理也意味着我们没有好好利用处理器的性能。