APP缓存数据线程安全问题
问题
一般一个 iOS APP 做的事就是:请求数据->保存数据->展示数据,一般用 Sqlite 作为持久存储层,保存从网络拉取的数据,下次读取可以直接从 Sqlite DB 读取。我们先忽略从网络请求数据这一环节,假设数据已经保存在 DB 里,那我们要做的事就是,ViewController 从 DB 取数据,再传给 view 渲染:
这是最简单的情况,随着程序变复杂,多个 ViewController 都要向 DB 取数据,ViewController本身也会因为数据变化重新去 DB 取数据,会有两个问题:
数据每次有变动,ViewController 都要重新去DB读取,做 IO 操作。
多个 ViewController 之间可能会共用数据,例如同一份数据,本来在 Controller1 已经从 DB 取出来了,在 Controller2 要使用得重新去 DB 读取,浪费 IO。
对这里做优化,自然会想到在 DB 和 VC 层之间再加一层 cache,把从 DB 读取出来的数据 cache 在内存里,下次来取同样的数据就不需要再去磁盘读取 DB 了。
几乎所有的数据库框架都做了这个事情,包括微信读书开源的 GYDataCenter,CoreData,Realm 等。但这样做会导致一个问题,就是数据的线程安全问题。
按上面的设计,Cache层会有一个集合,持有从DB读取的数据。
除了 VC 层,其他层也会从cache取数据,例如网络层。上层拿到的数据都是对 cache 层这里数据的引用:
可能还会在网络层子线程,或其他一些用于预加载的子线程使用到,如果某个时候一条子线程对这个 Book1 对象的属性进行修改,同时主线程在读这个对象的属性,就会 crash,因为一般我们为了性能会把对象属性设为nonatomic,是非线程安全的,多线程读写时会有问题:
//Network
WRBook *book = [WRCache bookWithId:@“”];
book.fav = YES; //子线程在写
[book save]; //VC1
WRBook *book = [WRCache bookWithId:@“”];
self.view.title = book.title; //主线程在读
可以通过这个测试看到 crash 场景:
@interface TestMultiThread : NSObject
@property (nonatomic) NSArray *arr;
@end @implementation TestMultiThread
@end TestMultiThread *obj = [[TestMultiThread alloc] init];
for (int i = ; i < ; i ++) {
dispatch_async(dispatch_get_global_queue(, ), ^{
id a = obj.arr;
});
dispatch_async(dispatch_get_global_queue(, ), ^{
obj.arr = [NSArray arrayWithObject:@"b"];
});
}
解决方案
对这种情况,一般有三种解决方案:
1. 加锁
既然这个对象的属性是非线程安全的,那加锁让它变成线程安全就行了。可以给每个对象自定义一个锁,也可以直接用 OC 里支持的属性指示符 atomic:
@property (atomic) NSArray *arr;
这样就不用担心多线程同时读写的问题了。但在APP里大规模使用锁很可能会导致出现各种不可预测的问题,锁竞争,优先级反转,死锁等,会让整个APP复杂性增大,问题难以排查,并不是一个好的解决方案。
2. 分线程cache
另一种方案是一条线程创建一个 cache,每条线程只对这条线程对应的 cache 进行读写,这样就没有线程安全问题了。CoreData 和 Realm 都是这种做法,但这个方案有两个缺点:
a.使用者需要知道当前代码在哪条线程执行。
b.多条线程里的 cache 数据需要同步。
CoreData 在不同线程要创建自己的 NSManagedObjectContext,这个 context 里维护了自己的 cache,如果某条子线程没有创建 NSManagedObjectContext,要读取数据就需要通过 performBlockAndWait: 等接口跑到其他线程去读取。如果多个 context 需要同步 cache 数据,就要调用它的 merge 方法,或者通过 parent-children context 层级结构去做。这导致它多线程使用起来很麻烦,API 友好度极低。
Realm 做得好一点,会在线程 runloop 开始执行时自动去同步数据,但如果线程没有 runloop 就需要手动去调 Realm.refresh() 同步。使用者还是需要明确知道代码在哪条线程执行,避免在多线程之间传递对象。
3.数据不可变
我们的问题是多线程同时读写导致,那如果只读不写,是不是就没有问题了?数据不可变指的就是一个数据对象生成后,对象里的属性值不会再发生改变,不允许像上述例子那样 book.fav = YES 直接设置,若一个对象属性值变了,那就新建一个对象,直接整个替换掉这个旧的对象:
//WRCache
@implementation WRCache
+(void) updateBookWithId:(NSString *)bookId params:(NSDictionary *)params
{
[WRDBCenter updateBookWithId:@“” params:{@“fav”: @(YES)}]; //更新DB数据
WRBook *book = [WRDBCenter readBookWithId:bookId]; //重新从DB读取,新对象
[self.cache setObject:book forKey:bookId]; //整个替换cache里的对象
}
@end self.book = [WRCache bookWithId:@“”];
// book.fav = YES; //不这样写
[WRCache updateBookWithId:@“” params:{@“fav”: @(YES)}]; //在cache里整个更新
self.book = [WRCache bookWithId:@“”]; //重新读取对象
这样就不会再有线程安全问题,一旦属性有修改,就整个数据重新从DB读取,这些对象的属性都不会再有写操作,而多线程同时读是没问题的。
但这种方案有个缺陷,就是数据修改后,会在 cache 层整个替换掉这个对象,但这时上层扔持有着旧的对象,并不会自动把对象更新过来:
所以怎样让上层更新数据呢?有两种方式,push 和 pull。
a. push
push 的方式就是 cache 层把更新 push 给上层,cache对整个对象更新替换掉时,发送广播通知上层,这里发通知的粒度可以按需求斟酌,上层监听自己关心的通知,如果发现自己持有的对象更新了,就要更新自己的数据,但这里的更新数据也是件挺麻烦的事。
举个例子,读书有一个想法列表WRReviewController,存着一个数组 reviews,保存着想法 review 数据对象,数组里的每一个 review 会持有这个这个想法对应的一本书,也就是 review.book 持有一个 WRBook 数据对象。然后这时 cache 层通知这个 WRReviewController,某个 book 对象有属性变了,这时这个 WRReviewController 要怎样处理呢?有两个选择:
遍历 reviews 数组,再遍历每一个 review 里的 book 对象,如果更新的是这个 book 对象,就把这个 book 对象替换更新。
什么都不管,只要有数据更新的通知过来,所有数据都重新往 cache 层读一遍,重新组装数据,界面全部刷新。
第一种是精细化的做法,优点是不影响性能,缺点是蛋疼,工作量增多,还容易漏更新,需要清楚知道当前模块持有了哪些数据,有哪些需要更新。
第二种是粗犷的做法,优点是省事省心,全部大刷一遍就行了,缺点是在一些复杂页面需要组装数据,会对性能造成较大影响。
b. pull
另一种 pull 的方式是指上层在特定时机自己去判断数据有没有更新。
首先所有数据对象都会有一个属性,暂时命名为 dirty,在 cache 层更新替换数据对象前,先把旧对象的 dirty 属性设为 YES,表示这个旧对象已经从 cache 里被抛弃了,属于脏数据,需要更新。然后上层在合适的时候自行去判断自己持有的对象的 dirty 属性是否为 YES,若是则重新在 cache 里取最新数据。
实际上这样做发生了多线程读写 dirty 属性,是有线程安全问题的,但因为 dirty 属性读取不频繁,可以直接给这个属性的读写加锁,不会像对所有属性加锁那样引发各种问题,解决对这个 dirty 属性读写的线程安全问题。
这里主要的问题是上层应该在什么时机去 pull 数据更新。可以在每次界面显示 -viewWillAppear 或用户操作后去检查,例如用户点个zan,就可以触发一次检查,去更新zan的数据,在这两个地方做检查已经可以解决90%的问题,剩下的就是同个界面联动的问题,例如 iPad 邮件左右两栏两个 controller,右边详情点个收藏,左边列表收藏图标也要高亮,这种情况可以做特殊处理,也可以结合上面 push 的方式去做通知。
push 和 pull 两种是可以结合在一起用的,pull 的方式弥补了 push 后数据全部重新读取大刷导致的性能低下问题,push 弥补了 pull 更新时机的问题,实际使用中配合一些事先制定的规则或框架一起使用效果更佳。
总结
对于 APP 缓存数据线程安全问题,分线程 cache 和数据不可变是比较常见的解决方案,都有着不同的实现代价,分线程 cache 接口不友好,数据不可变需要配合单向数据流之类的规则或框架才会变得好用,可以按需选择合适的方案。
APP缓存数据线程安全问题的更多相关文章
- 【转】数据存储——APP 缓存数据线程安全问题探讨
http://blog.cnbang.net/tech/3262/ 问题 一般一个 iOS APP 做的事就是:请求数据->保存数据->展示数据,一般用 Sqlite 作为持久存储层,保存 ...
- Java基础-线程操作共享数据的安全问题
Java基础-线程操作共享数据的安全问题 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.引发线程安全问题 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运 ...
- redis是单进程数据库,多用户排队对统一数据进行访问,不存在并发访问生产的线程安全问题
redis是单进程数据库,多用户排队对统一数据进行访问,不存在并发访问生产的线程安全问题. oracle是多进程数据库,存在并发访问的问题,必须事务加锁等方式进行处理.
- ASP.NET MVC深入浅出系列(持续更新) ORM系列之Entity FrameWork详解(持续更新) 第十六节:语法总结(3)(C#6.0和C#7.0新语法) 第三节:深度剖析各类数据结构(Array、List、Queue、Stack)及线程安全问题和yeild关键字 各种通讯连接方式 设计模式篇 第十二节: 总结Quartz.Net几种部署模式(IIS、Exe、服务部署【借
ASP.NET MVC深入浅出系列(持续更新) 一. ASP.NET体系 从事.Net开发以来,最先接触的Web开发框架是Asp.Net WebForm,该框架高度封装,为了隐藏Http的无状态模 ...
- Core Data 的线程安全问题
前言: 很多小的App只需要一个ManagedContext在主线程就可以了,但是有时候对于CoreData的操作要耗时很久的,比如App开启的时候要载入大量数据,如果都放在主线程,毫无疑问会阻塞UI ...
- iOS五种本地缓存数据方式
iOS五种本地缓存数据方式 iOS本地缓存数据方式有五种:前言 1.直接写文件方式:可以存储的对象有NSString.NSArray.NSDictionary.NSData.NSNumber,数据 ...
- (2.1)servlet线程安全问题
本文参考链接:http://www.yesky.com/334/1951334.shtml 摘 要:介绍了Servlet多线程机制,通过一个实例并结合Java 的内存模型说明引起Servlet线程不安 ...
- SimpleDateFormat使用和线程安全问题
SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的具体类. 它允许格式化 (date -> text).语法分析 (text -> date)和标准化. Simpl ...
- SimpleDateFormat时间格式化存在线程安全问题
想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调 ...
随机推荐
- Java try-catch-finally 返回值
1.只有 try-catch 的情况下,如果不发生异常,则会返回 try 中的 return ; 如果发生异常,则会返回 catch 中的 return, try 中的 return 被盖掉; ...
- Spring(十六):泛型依赖注入
简介: Spring4.X之后开始支持泛型依赖注入. 使用示例: 1.定义实体 package com.dx.spring.bean.componentscan; import java.io.Ser ...
- Eclipse Maven项目报错3之找不到配置文件spring-servlet-context.xml
一.具体错误如下图所示 根据文字提示可以看出是这个文件找不到,但是我去项目的这个目录找了,这个文件确实存在,那么是什么问题呢 二.解决问题 原因分析(来自网上) 代码编译的过程,是一个自动生成相应编译 ...
- C++ 函数适配器
1.考虑下面的需求,在一个int的vector中,找出一个比5的元素,容易想到的解决办法,定义一个方法对象,使用模板,如下:vector<int>::iterator iter = fin ...
- Android 原生 Android ActionBar Tab (滑动)导航
本文内容 环境 项目结构 演示一:ActionBar Tab 导航 演示二:ActionBar Tab 带滑动导航 本文演示 Tab 导航.第一个演示,是基本的 Tab 导航,第二个是带滑动的 Tab ...
- angular中的 登录检查 和 过期Session清理
angular利用ui-router进行登录检查 SAP都会有这个问题,session过期或者页面被刷新的情况下应该进入登录页. 监听ui-router的satte事件可以实现当state切换的时候检 ...
- unable to find the sources of your current Linux kernel.
运行 sh ./VBoxLinuxAdditions.run 时FAILED,查看日志: /tmp/vbox.0/Makefile.include.header:97: *** Error: unab ...
- Dwr 框架简单实例
Dwr 是一个 Java 开源库,帮助你实现Ajax网站. 它可以让你在浏览器中的Javascript代码调用Web服务器上的Java,就像在Java代码就在浏览器中一样. Dwr 主要包括两部分: ...
- 关于Android中50M+的文本入库处理细节
好久没有写技术文章,明早4点还要爬起来赶飞机,感觉这个坑有必要记录一下,以慰藉一下自己脆弱的灵魂.周一和周二忙了2天的样子才解决这个问题,中间填了不少的坑,反正已经夜深了,慢慢地记录一点. 场景:项目 ...
- Oracle Data Integrator学习资料
http://docs.oracle.com/middleware/1213/odi/index.html https://docs.oracle.com/middleware/1213/core/O ...