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

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

关于 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://www.easck.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 对象,所以这里我们需要使用上面这段代码将这种情况排除掉。