使用 Swift 设计多线程应用程序

作为汽车行业的 iOS 开发人员,我花了大量时间处理实时数据。在当今的许多应用中,有效处理连续数据流的诉求是非常重要的。为确保不卡住用户界面,你很可能需要使用多线程。

处理实时流式传输的信息是件很意思的事情,因为你将不断收到可用于更新 UI 的新数据。但由于 iOS 设备在硬件方面的局限性,这也可能是最困难和最令人沮丧的事情。幸运的是,苹果已经通过一个非常易用的 GCD(Grand Central Dispatch) 接口提供了多线程能力。你可能对这样的代码比较熟悉:

DispatchQueue.main.async {
  // Place a work item in the GCD main queue and then
  // move on to the next statement in your code.
}

如果你没有显式指定放在某个队列的话,大多数程序代码都在主队列 (main queue) 里运行。它是一个串行队列,意味着它会选取第一个任务项、执行代码、等待完成、释放任务,然后再选择下一个任务项,依此类推。

多线程与并发

主队列不是通过 GCD 能使用的唯一队列,GCD 有许多预定义且具有不同优先级的队列。同时也有一些方法创建你自己的专用队列,如下所示:

let myConcurrentQueue = DispatchQueue(label: "ConcurrentQueue",
                                      qos: .background,
                                      attributes: .concurrent,
                                      autoreleaseFrequency: .workItem,
                                      target: nil)

请注意,我们刚刚创建了具有 .concurrent 属性的队列,这意味着在此队列中执行下个任务项之前不会等待前一个完成,它简单地将前一个任务项放在一个线程里启动,然后转去处理下个任务项,不管第一个任务是否已经完成。

现在,来点技术性的……

设想你正在处理采样率为 20Hz 的数据流,也就是说大约有 50 毫秒的时间来解析和解释数据,将结果添加到数据结构并通知 view 展示。如果 iOS 设备尝试在主线程里执行这些,那么只剩非常少的时间检查用户是否有尝试与应用发生交互,从而导致应用失去响应,这就是我们要改用多线程的地方。
假设我们使用一个非常简单的数据结构(整数数组)来存储接收到的数据样本,我们可能会创建一个队列并这样使用:

// Here's our data queue from before, but with a
// higher priority Quality of Service flag
let myDataQueue = DispatchQueue(label: "DataQueue",
                                qos: .userInitiated,
                                attributes: .concurrent,
                                autoreleaseFrequency: .workItem,
                                target: nil)

// Our data structure, probably initialized in 
// a data manager somewhere
var dataArray = [Int]()

// When we receive our data, we call our
// parsing/storing/updating code like this
myDataQueue.async {
  let parsedData = parseData(data)
  dataArray.append(parsedData)
  DispatchQueue.main.async {
    updateViews()
  }
}

这样可以工作吗??

上面的代码看起来不错吧?现在我们在后台线程上进行所有数据的处理,主线程仅用于更新视觉效果。但是,这几乎肯定会导致程序崩溃。为什么呢?答案有点技术性,但理解这个很重要。

由于我们的队列是并发的,因此它会向工作线程抛出要并行执行的任务项。我们还使用一个数据作为数据存储。Swift 数组是一种结构类型 (struct type),这也意味着它是一种值类型。当你尝试将值追加到这样的数组上时,将会:

  1. 分配一个新数组并拷贝旧数组里的数值;
  2. 追加新数据;
  3. 将新引用写回数组变量;
  4. 系统继续释放旧数组占用的内存。

想想看如果两个线程复制了同一个数组,它们将自己的数据追加到副本,然后先后或同时将新引用写回变量,会发生什么情况。
第一种情况会给我们不正确的数据,因为先写入线程中的数据将被后写入线程的数据覆盖。第二种情况会导致程序崩溃,因为两个线程无法同时获得对已分配内存的写权限。
考虑到这一点,我们可以使用 DispatchQueue 类附带的非常智能的方式,即 flags 参数。现在可以这样修改代码:

let myDataQueue = DispatchQueue(label: "DataQueue",
                                qos: .userInitiated,
                                attributes: .concurrent,
                                autoreleaseFrequency: .workItem,
                                target: nil)

var dataArray = [Int]()

// The .barrier flag tells the queue that this particular
// work item will need to be executed without any other 
// work item running in parallel
myDataQueue.async(flags: .barrier) {
  let parsedData = parseData(data)
  dataArray.append(parsedData)
  DispatchQueue.main.async {
    // This method will most likely need to access our data
    // structure at some point, and it will need to do so
    // in a specific manner. Check the implementation below.
    updateViews()
  }
}

func updateViews() {
  let dataForViews = return myDataQueue.sync { return dataArray }
  // Do any updates using the dataForViews variable,
  // since that will remain intact even if the data

这可能看起来让人头大,但我会详细解释:
每当添加一个将会修改数据结构的任务项时,通过使用 .barrier 标志,来告诉队列这个特定的任务需要单独执行,这意味着先等待正在运行的任务全部完成,然后单独执行该任务,直到完后才开始并发执行后续任务(这就保证了不会同时读写)。
当主线程要访问数据以更新视图时,它需要使用同步 (sync) 调用来遍历数据,如果不这样就会导致风险:另一个写线程可能随时会损坏它正在读取的数据。

结语

希望你已经顺利读写并获得了一些新知识。在几天内复习一下可能会更有帮助,让自己有机会反复思考。


原文: https://medium.com/@JimmyMAndersson/designing-multi-threaded-applications-using-swift-bab16e64dbb4
作者:Jimmy M Andersson
编译:码王爷

你可能感兴趣的:(使用 Swift 设计多线程应用程序)