Swift 并发初步

前言

学如逆水行舟,不进则退。共勉!!今天主要是分享一篇关于Swift并发初步的文章。

iOS资料|地址

一些基本概念

同步和异步

在我们说到线程的执行方式时,同步 (synchronous) 和异步 (asynchronous) 是这个话题中最基本的一组概念。同步操作意味着在操作完成之前,运行这个操作的线程都将被占用,直到函数最终被抛出或者返回。Swift 5.5 之前,所有的函数都是同步函数,我们简单地使用 func 关键字来声明这样一个同步函数:

var results: [String] = []
func addAppending(_ value: String, to string: String) {
    results.append(value.appending(string))
}

addAppending 是一个同步函数,在它返回之前,运行它的线程将无法执行其他操作,或者说它不能被用来运行其他函数,必须等待当前函数执行完成后这个线程才能做其他事情。
在这里插入图片描述
在 iOS 开发中,我们使用的 UI 开发框架,也就是 UIKit 或者 SwiftUI,不是线程安全的:对用户输入的处理和 UI 的绘制,必须在与主线程绑定的 main runloop 中进行。假设我们希望用户界面以每秒 60 帧的速率运行,那么主线程中每两次绘制之间,所能允许的处理时间最多只有 16 毫秒 (1 / 60s)。当主线程中要同步处理的其他操作耗时很少时 (比如我们的 addAppending,可能耗时只有几十纳秒),这不会造成什么问题。但是,如果这个同步操作耗时过长的话,主线程将被阻塞。它不能接受用户输入,也无法向 GPU 提交请求去绘制新的 UI,这将导致用户界面掉帧甚至卡死。这种“长耗时”的操作,其实是很常见的:比如从网络请求中获取数据,从磁盘加载一个大文件,或者进行某些非常复杂的加解密运算等。

下面的 loadSignature 从某个网络 URL 读取字符串:如果这个操作发生在主线程,且耗时超过 16ms (这是很可能发生的,因为通过握手协议建立网络连接,以及接收数据,都是一系列复杂操作),那么主线程将无法处理其他任何操作,UI 将不会刷新。

// 从网络读取一个字符串
func loadSignature() throws -> String? {
  // someURL 是远程 URL,比如 https://example.com
  let data = try Data(contentsOf: someURL)
  return String(data: data, encoding: .utf8)
}

在这里插入图片描述
loadSignature 最终的耗时超过 16 ms,对 UI 的刷新或操作的处理不得不被延后。在用户观感上,将表现为掉帧或者整个界面卡住。这是客户端开发中绝对需要避免的问题之一。

Swift 5.5 之前,要解决这个问题,最常见的做法是将耗时的同步操作转换为异步操作:把实际长时间执行的任务放到另外的线程 (或者叫做后台线程) 运行,然后在操作结束时提供运行在主线程的回调,以供 UI 操作之用:

func loadSignature(
  _ completion: @escaping (String?, Error?) -> Void
)
{
  DispatchQueue.global().async {
    do {
      let d = try Data(contentsOf: someURL)
      DispatchQueue.main.async {
        completion(String(data: d, encoding: .utf8), nil)
      }
    } catch {
      DispatchQueue.main.async {
        completion(nil, error)
      }
    }
  }
}

在这里插入图片描述
DispatchQueue.global 负责将任务添加到全局后台派发队列。在底层,GCD 库 (Grand Central Dispatch) 会进行线程调度,为实际耗时繁重的 Data.init(contentsOf:) 分配合适的线程。耗时任务在主线程外进行处理,完成后再由 DispatchQueue.main 派发回主线程,并按照结果调用 completion 回调方法。这样一来,主线程不再承担耗时任务,UI 刷新和用户事件处理可以得到保障。

异步操作虽然可以避免卡顿,但是使用起来存在不少问题,最主要包括:

  • 错误处理隐藏在回调函数的参数中,无法用 throw 的方式明确地告知并强制调用侧去进行错误处理。
  • 对回调函数的调用没有编译器保证,开发者可能会忘记调用 completion,或者多次调用 completion。
  • 通过 DispatchQueue 进行线程调度很快会使代码复杂化。特别是如果线程调度的操作被隐藏在被调用的方法中的时候,不查看源码的话,在 (调用侧的) 回调函数中,几乎无法确定代码当前运行的线程状态。
  • 对于正在执行的任务,没有很好的取消机制。

除此之外,还有其他一些没有列举的问题。它们都可能成为我们程序中潜在 bug 的温床,在之后关于异步函数的章节里,我们会再回顾这个例子,并仔细探讨这些问题的细节。

需要进行说明的是,虽然我们将运行在后台线程加载数据的行为称为异步操作,但是接受回调函数作为参数的 loadSignature(_😃 方法,其本身依然是一个同步函数。这个方法在返回前仍旧会占据主线程,只不过它现在的执行时间非常短,UI 相关的操作不再受影响。

Swift 5.5 之前,Swift 语言中并没有真正异步函数的概念,我们稍后会看到使用 async 修饰的异步函数是如何简化上面的代码的。

串行和并行

另外一组重要的概念是串行和并行。对于通过同步方法执行的同步操作来说,这些操作一定是以串行方式在同一线程中发生的。“做完一件事,然后再进行下一件事”,是最常见的、也是我们人类最容易理解的代码执行方式:

if let signature = try loadSignature() {
  addAppending(signature, to: "some data")
}
print(results)

loadSignature,addAppending 和 print 被顺次调用,它们在同一线程中按严格的先后顺序发生。这种执行方式,我们将它称为串行 (serial) 。
在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值