使用 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
编译:码王爷

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • iOS多线程编程 基本知识 1. 进程(process) 进程是指在系统中正在运行的一个应用程序,就是一段程序的执...
    陵无山阅读 6,028评论 1 14
  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,740评论 1 17
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,093评论 1 32
  • 本文用来介绍 iOS 多线程中 GCD 的相关知识以及使用方法。这大概是史上最详细、清晰的关于 GCD 的详细讲...
    花花世界的孤独行者阅读 497评论 0 1
  • 大多数的教程都太老,或者也不知道从哪里抄写的文章。导致我使用ButterKnife时,每次都不其作用,最后才发现配...
    小酷哥阅读 291评论 0 0