问题

  一般一个 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缓存数据线程安全问题的更多相关文章

  1. 【转】数据存储——APP 缓存数据线程安全问题探讨

    http://blog.cnbang.net/tech/3262/ 问题 一般一个 iOS APP 做的事就是:请求数据->保存数据->展示数据,一般用 Sqlite 作为持久存储层,保存 ...

  2. Java基础-线程操作共享数据的安全问题

    Java基础-线程操作共享数据的安全问题 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.引发线程安全问题 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运 ...

  3. redis是单进程数据库,多用户排队对统一数据进行访问,不存在并发访问生产的线程安全问题

    redis是单进程数据库,多用户排队对统一数据进行访问,不存在并发访问生产的线程安全问题. oracle是多进程数据库,存在并发访问的问题,必须事务加锁等方式进行处理.

  4. 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的无状态模 ...

  5. Core Data 的线程安全问题

    前言: 很多小的App只需要一个ManagedContext在主线程就可以了,但是有时候对于CoreData的操作要耗时很久的,比如App开启的时候要载入大量数据,如果都放在主线程,毫无疑问会阻塞UI ...

  6. iOS五种本地缓存数据方式

    iOS五种本地缓存数据方式   iOS本地缓存数据方式有五种:前言 1.直接写文件方式:可以存储的对象有NSString.NSArray.NSDictionary.NSData.NSNumber,数据 ...

  7. (2.1)servlet线程安全问题

    本文参考链接:http://www.yesky.com/334/1951334.shtml 摘 要:介绍了Servlet多线程机制,通过一个实例并结合Java 的内存模型说明引起Servlet线程不安 ...

  8. SimpleDateFormat使用和线程安全问题

    SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的具体类. 它允许格式化 (date -> text).语法分析 (text -> date)和标准化. Simpl ...

  9. SimpleDateFormat时间格式化存在线程安全问题

    想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调 ...

随机推荐

  1. Provider Pattern for Beginners in .net

    Download ProviderPattern.zip Introduction Provider pattern allows the developers to create pluggable ...

  2. Direct2D教程III——几何(Geometry)对象

    目前博客园中成系列的Direct2D的教程有 1.万一的 Direct2D 系列,用的是Delphi 2009 2.zdd的 Direct2D 系列,用的是VS中的C++ 3.本文所在的 Direct ...

  3. javascript捕获事件event

    var e = e ? e : window.event; window.event ? window.event.cancelBubble = true : e.stopPropagation(); ...

  4. 基于VM10+Win7安装Mac OSX10.11 El Capitan

    前言 此文写给那些像我一样的屌丝程序员(呵呵,我现在从事的是最底层的工作了,但是不想放弃我的梦想) 说明 基于VM10+Win7安装Mac OSX10.11 El Capitan 工具 VMware- ...

  5. python 微信企业号

    python 微信企业号 准备,如果没有微信企业号,可以先申请体验号记下CorpID和Secret(获取Token用) 发送消息首先可以在微信的开发者中心,查看接口文档 下面就是python代码:1. ...

  6. 使用angular5+ionic3+sqlite创建离线app应用

    1.安装sqlite和toast插件 npm install --save @ionic-native/sqlite npm install --save @ionic-native/toast 未完 ...

  7. C#--串行化与反串行化

    串行化是指存储和获取磁盘文件.内存或其他地方中的对象.在串行化时,所有的实例数据都保存到存储介质上,在取消串行化时,对象会被还原,且不能与其原实例区别开来.只需给类添加Serializable属性,就 ...

  8. 牛客网-《剑指offer》-矩形覆盖

    题目:http://www.nowcoder.com/practice/72a5a919508a4251859fb2cfb987a0e6 C++ class Solution { public: in ...

  9. bash和shell的关系

    bash是borne again shell的缩写,它是shell的一种,Linux上默认采用的是bash. shell脚本中的方法带不带function的区别,例如: function foo () ...

  10. 使用sphinx自动提取python中的注释成为接口文档

    写好了代码,交付给他人使用的时候,查看代码固然可以了解各类和函数的功能细节,但接口文档能更方便的查找和说明功能.所以,一价与代码同步的接口文档是很有必要的.sphinx可以根据python中的注释,自 ...