iOS/Swift多线程之---如何避免数据竞争(Data race)

2018 年 4 月 20 日 CocoaChina crafttang

多线程编程中, 常见的问题有


  • 死锁Deadlock


死锁指的是由于两个或多个执行单元之间相互等待对方结束而引起阻塞的情况。每个线程都拥有其他线程所需要的资源,同时又等待其他线程已经拥有的资源,并且每个线程在获取所有需要资源之前都不会释放自己已经拥有的资源。


  • 优先级翻转/倒置/逆转 Priority inversion


当一个高优先级任务通过信号量机制访问共享资源时,该信号量已被一低优先级任务占有,而这个低优先级任务在访问共享资源时可能又被其它一些中等优先级任务抢先,因此造成高优先级任务被许多具有较低优先级任务阻塞,实时性难以得到保证。


  • 数据竞争Race condition


Data Race是指多个线程在没有正确加锁的情况下,同时访问同一块数据,并且至少有一个线程是写操作,对数据的读取和修改产生了竞争,从而导致各种不可预计的问题。


这里我们重点讲讲iOS中的数据竞争问题以及如何解决/避免这类问题.


本文所用到的示例代码均可以在Github下载: https://github.com/zhihuitang/GCDExample


数据竞争 Data race



Data Race的问题非常难查,Data Race一旦发生,结果是不可预期的,也许直接就Crash了,也许导致执行流程错乱了,也许把内存破坏导致之后某个时刻突然Crash了。


在我们的产品中,经常会碰见这样的情况, 代码在我们开发测试阶段完美运行, 没有任何问题. 但产品上线后, 时不时有用户抱怨各种奇怪的问题, 后台日志跟踪(例如Fabric)也会出现一些异常的exception, 根据这些Exception的日志打印的堆栈也很难发现根本问题.



通常这些问题极有可能是多个线程同时访问内存中的同一段地址造成的。多线程问题是许多开发人员的噩梦, 它们难以跟踪重现,因为错误只发生在某些条件下,时间随机. 所以确定问题的根本原因可能是非常棘手的, 这就是我们所说的的“race condition”。


跟踪数据竞争在过去是一个绝对的噩梦,但幸运的是从Xcode8.0已经发布了一个新的调试工具,称为Thread Sanitizer(又叫TSan),可以帮助在运行时检测多线程中的数据竞争问题. 没了解过的小朋友可以参看官方视频 WWDC 2016 Session 412


例子


假设在你的App内, 有个联系人, 他是Person对象, 包含name和email, 定义如下:


class Person: NSObject {
   var name: String
   var email: String
   init(name: String, email: String)
{
       self.name = name
       self.email = email
   }
   func setProperty(name: String, email: String) {
       self.name = name
       randomDelay(maxDuration:  0.5)
       randomDelay(maxDuration:  0.5)
       self.email
= email
   }
   override var description: String {
       return "[\(name)] \(email)"
   }
}


由于业务需要, 这个Person可能会被多个线程访问(read & write), Person依次被修改为 Leijun, Luoyonghao, Yuchengdong, Goodguy.


let contacts = [("Leijun", "leijun@mi.com"), ("Luoyonghao", "luoyonghao@smartisan.com"), ("Yuchengdong", "yuchengdong@huawei.com"), ("Goodguy", "crafttang@gmail.com")]
private func updateContact(person: Person, contacts: [(String, String)]){
   for (name, email) in contacts {
       dispatchQueue.async(group: dispatchGroup) {
           person.setProperty(name: name, email: email)
           print("Current person: \(person)")
       }
   }
   dispatchGroup.notify(queue: DispatchQueue.global()) {
       print("==> Final person: \(person)")
   }
}


Person对象在被修改的过程中,我们同时打印出修改后的结果, 以及最终的结果. 如果一切运行正常的情况下, Person的最终结果应该为 Goodguy.


运行 GCDExample App, 点击 Test1 - Non-Thread-Safe 按钮, 对应代码如下:


let person = Person(name: "unknown", email: "unknown")
updateContact(person: person, contacts: contacts)
点击按钮后, 我们可以从Xcode的output窗口可以看见类似输出:
Current person: [Goodguy] crafttang@gmail.com
Current person: [Goodguy] yuchengdong@huawei.com
Current person: [Goodguy] luoyonghao@smartisan.com
Current person: [Goodguy] leijun@mi.com
==> Final person: [Goodguy] leijun@mi.com


上面的日志并不是我们预期的结果:


  1. 最终的Person, name倒是对的, 但Email错误;

  2. 中间结果每次输出的Person的name与email除第一个外, 均不对应.



造成上面name和email不对应的原因就是因为数据竞争(Data race), 多个线程试图修改同一块内存(person), 会导致修改数据的混乱, 严重的可能导致App崩溃.


利用Xcode的TSan,我们可以检测到这个问题, 在 Diagnostics页面, 选中Thread Sanitizer:



重新运行App, 点击Test1 - Non-Thread-Safe, 你会发现Xcode的output多了很多乱七八糟的输出:




同时, 在Xcode navigator面板, 你会发现检测到了Threading issues, thread9在read, thread10在write同一个变量,导致 Data race.


如何解决这种Data race问题呢? 将共享变量的 read和write放在同一个DispatchQueue中. 采用什么样的DispatchQueue, 这里有2种方法:


  1. 采用串行的DispatchQueue, 所有的read/write都是串行的, 所以不会出现Data race的问题; 但是效率比较低,即使所有的操作都是read, 也必须排队一个一个的读.

  2. 采用并行的DispatchQueue, 所有的read都可以并行进行, 所有的write都必须"独占"(barrier)的进行: 我write的时候, 任何人不允许read或者write.如下图:



对于第1点, 由于效率较低, 也比较简单,这里我就不介绍.下面重点介绍如何采用barrier DispatchQueue才避免Data race的问题.

声明一个ThreadSafePerson的类, 继承于Person:


class ThreadSafePerson: Person {
   let isolationQueue = DispatchQueue(label: "com.crafttang.isolationQueue", attributes: .concurrent)
   override func setProperty(name: String, email: String) {
       isolationQueue.async(flags: .barrier) {
           super.setProperty(name: name, email: email)
       }
   }
   override var description: String {
       return isolationQueue.sync { super.description }
   }
}


在这个类中, 声明了一个并行.concurrent的DispatchQueue:


let isolationQueue = DispatchQueue(label: "com.crafttang.isolationQueue", attributes: .concurrent)


将所有的read和write操作都放在这个isolationQueue中.


读取person对象时:


override var description: String {
   return isolationQueue.sync { super.description }
}


修改person对象时, 将操作放在barrier的isolationQueue中, 这样就保证了这个写操作是独占的:


override func setProperty(name: String, email: String) {
   isolationQueue.async(flags: .barrier) {
       super.setProperty(name: name, email: email)
   }
}




在ViewController添加第二个按钮Test2 - Thread-Safe, 对应代码如下:


@IBAction func button2Tapped(_ sender: UIButton) {
   let person = ThreadSafePerson(name: "unknown", email: "unknown")
   updateContact(person: person, contacts: contacts)
}


运行App, 点击第二个按钮Test2 - Thread-Safe, 在Xcode的output中你可能得到的输出如下:


Current person: [Luoyonghao] luoyonghao@smartisan.com
Current person: [Luoyonghao] luoyonghao@smartisan.com
Current person: [Yuchengdong] yuchengdong@huawei.com
Current person: [Goodguy] crafttang@gmail.com
==> Final person: [Goodguy] crafttang@gmail.com


所有的name和email均对应正确, 并且最后的 Final person也是正确的. 至此,完美解决Data race问题.



相关推荐:


登录查看更多
0

相关内容

苹果公司在 WWDC 2014 开幕 Keynote 上发布的全新编程语言,具有更多现代化特性,同时容易使用,定位是补充 Objective-C. > Swift is an innovative new programming language for Cocoa and Cocoa Touch. Writing code is interactive and fun, the syntax is concise yet expressive, and apps run lightning-fast. Swift is ready for your next iOS and OS X project — or for addition into your current app — because Swift code works side-by-side with Objective-C.

Swift - Apple Developer

【2020新书】从Excel中学习数据挖掘,223页pdf
专知会员服务
91+阅读 · 2020年6月28日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
58+阅读 · 2020年6月26日
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
7 款实用到哭的App,只说一遍
高效率工具搜罗
84+阅读 · 2019年4月30日
“黑客”入门学习之“windows系统漏洞详解”
安全优佳
8+阅读 · 2019年4月17日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
占坑!利用 JenKins 持续集成 iOS 项目时遇到的问题
Python 杠上 Java、C/C++,赢面有几成?
CSDN
6+阅读 · 2018年4月12日
已删除
生物探索
3+阅读 · 2018年2月10日
Neo4j 和图数据库起步
Linux中国
8+阅读 · 2017年12月20日
Arxiv
38+阅读 · 2020年3月10日
On Feature Normalization and Data Augmentation
Arxiv
15+阅读 · 2020年2月25日
Arxiv
6+阅读 · 2020年2月15日
Arxiv
7+阅读 · 2017年12月28日
Arxiv
6+阅读 · 2016年1月15日
VIP会员
相关资讯
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
7 款实用到哭的App,只说一遍
高效率工具搜罗
84+阅读 · 2019年4月30日
“黑客”入门学习之“windows系统漏洞详解”
安全优佳
8+阅读 · 2019年4月17日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
占坑!利用 JenKins 持续集成 iOS 项目时遇到的问题
Python 杠上 Java、C/C++,赢面有几成?
CSDN
6+阅读 · 2018年4月12日
已删除
生物探索
3+阅读 · 2018年2月10日
Neo4j 和图数据库起步
Linux中国
8+阅读 · 2017年12月20日
相关论文
Arxiv
38+阅读 · 2020年3月10日
On Feature Normalization and Data Augmentation
Arxiv
15+阅读 · 2020年2月25日
Arxiv
6+阅读 · 2020年2月15日
Arxiv
7+阅读 · 2017年12月28日
Arxiv
6+阅读 · 2016年1月15日
Top
微信扫码咨询专知VIP会员