成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

APP緩存數據線程安全問題探討

開發 后端
對于 APP 緩存數據線程安全問題,分線程 cache 和數據不可變是比較常見的解決方案,都有著不同的實現代價,分線程 cache 接口不友好,數據不可變需要配合單向數據流之類的規則或框架才會變得好用,可以按需選擇合適的方案。

問題

一般一個 iOS APP 做的事就是:請求數據->保存數據->展示數據,一般用 Sqlite 作為持久存儲層,保存從網絡拉取的數據,下次讀取可以直接從 Sqlite DB 讀取。我們先忽略從網絡請求數據這一環節,假設數據已經保存在 DB 里,那我們要做的事就是,ViewController 從 DB 取數據,再傳給 view 渲染:

APP緩存數據線程安全問題探討

這是最簡單的情況,隨著程序變復雜,多個 ViewController 都要向 DB 取數據,ViewController本身也會因為數據變化重新去 DB 取數據,會有兩個問題:

數據每次有變動,ViewController 都要重新去DB讀取,做 IO 操作。

多個 ViewController 之間可能會共用數據,例如同一份數據,本來在 Controller1 已經從 DB 取出來了,在 Controller2 要使用得重新去 DB 讀取,浪費 IO。

APP緩存數據線程安全問題探討

對這里做優化,自然會想到在 DB 和 VC 層之間再加一層 cache,把從 DB 讀取出來的數據 cache 在內存里,下次來取同樣的數據就不需要再去磁盤讀取 DB 了。

APP緩存數據線程安全問題探討

幾乎所有的數據庫框架都做了這個事情,包括微信讀書開源的 GYDataCenter,CoreData,Realm 等。但這樣做會導致一個問題,就是數據的線程安全問題。

按上面的設計,Cache層會有一個集合,持有從DB讀取的數據。

APP緩存數據線程安全問題探討

除了 VC 層,其他層也會從cache取數據,例如網絡層。上層拿到的數據都是對 cache 層這里數據的引用:

APP緩存數據線程安全問題探討

可能還會在網絡層子線程,或其他一些用于預加載的子線程使用到,如果某個時候一條子線程對這個 Book1 對象的屬性進行修改,同時主線程在讀這個對象的屬性,就會 crash,因為一般我們為了性能會把對象屬性設為nonatomic,是非線程安全的,多線程讀寫時會有問題:

 

  1. //Network  
  2. WRBook *book = [WRCache bookWithId:@“10000”];  
  3. book.fav = YES; //子線程在寫  
  4. [book save];  
  5. //VC1  
  6. WRBook *book = [WRCache bookWithId:@“10000”];  
  7. self.view.title = book.title; //主線程在讀  
  8. 可以通過這個測試看到 crash 場景:  
  9. @interface TestMultiThread : NSObject  
  10. @property (nonatomic) NSArray *arr;  
  11. @end  
  12. @implementation TestMultiThread  
  13. @end  
  14. TestMultiThread *obj = [[TestMultiThread alloc] init];  
  15. for (int i = 0; i < 1000; i ++) {  
  16. dispatch_async(dispatch_get_global_queue(0, 0), ^{  
  17. NSLog(@"%@", obj.arr);  
  18. });  
  19.  
  20. for (int i = 0; i < 1000; i ++) {  
  21. dispatch_async(dispatch_get_global_queue(0, 0), ^{  
  22. obj.arr = [NSArray arrayWithObject:@“b"];  
  23. });  

解決方案

對這種情況,一般有三種解決方案:

1. 加鎖

既然這個對象的屬性是非線程安全的,那加鎖讓它變成線程安全就行了??梢越o每個對象自定義一個鎖,也可以直接用 OC 里支持的屬性指示符 atomic:

  1. @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 直接設置,若一個對象屬性值變了,那就新建一個對象,直接整個替換掉這個舊的對象:

 

  1. //WRCache  
  2. @implementation WRCache  
  3. +(void) updateBookWithId:(NSString *)bookId params:(NSDictionary *)params  
  4.  
  5. [WRDBCenter updateBookWithId:@“10000” params:{@“fav”: @(YES)}]; //更新DB數據  
  6. WRBook *book = [WRDBCenter readBookWithId:bookId]; //重新從DB讀取,新對象  
  7. [self.cache setObject:book forKey:bookId]; //整個替換cache里的對象  
  8.  
  9. @end  
  10. self.book = [WRCache bookWithId:@“10000”];  
  11. // book.fav = YES; //不這樣寫  
  12. [WRCache updateBookWithId:@“10000” params:{@“fav”: @(YES)}]; //在cache里整個更新  
  13. self.book = [WRCache bookWithId:@“10000”]; //重新讀取對象 

這樣就不會再有線程安全問題,一旦屬性有修改,就整個數據重新從DB讀取,這些對象的屬性都不會再有寫操作,而多線程同時讀是沒問題的。

但這種方案有個缺陷,就是數據修改后,會在 cache 層整個替換掉這個對象,但這時上層扔持有著舊的對象,并不會自動把對象更新過來:

APP緩存數據線程安全問題探討

所以怎樣讓上層更新數據呢?有兩種方式,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 或用戶操作后去檢查,例如用戶點個贊,就可以觸發一次檢查,去更新贊的數據,在這兩個地方做檢查已經可以解決90%的問題,剩下的就是同個界面聯動的問題,例如 iPad 郵件左右兩欄兩個 controller,右邊詳情點個收藏,左邊列表收藏圖標也要高亮,這種情況可以做特殊處理,也可以結合上面 push 的方式去做通知。

push 和 pull 兩種是可以結合在一起用的,pull 的方式彌補了 push 后數據全部重新讀取大刷導致的性能低下問題,push 彌補了 pull 更新時機的問題,實際使用中配合一些事先制定的規則或框架一起使用效果更佳。

總結

對于 APP 緩存數據線程安全問題,分線程 cache 和數據不可變是比較常見的解決方案,都有著不同的實現代價,分線程 cache 接口不友好,數據不可變需要配合單向數據流之類的規則或框架才會變得好用,可以按需選擇合適的方案。

責任編輯:未麗燕 來源: bang's blog
相關推薦

2010-04-02 13:53:47

2019-02-21 06:51:31

2013-07-23 10:36:02

RFID技術個人隱私安全隱私安全

2023-10-27 13:31:18

線程安全多線程

2011-03-29 10:41:51

Java線程安全

2012-02-21 14:14:47

Java

2022-04-11 10:56:43

線程安全

2024-09-17 17:50:28

線程線程安全代碼

2018-08-29 12:05:54

云數據存儲安全

2013-08-14 09:11:43

云數據存儲云存儲云安全

2021-07-26 06:57:59

Synchronize線程安全

2012-11-20 10:47:16

2021-12-29 15:55:34

安全數據信息安全

2019-04-04 11:55:59

2012-12-18 13:56:55

2021-06-11 13:57:46

網絡安全大數據安全互聯網

2009-05-30 09:36:18

2022-04-06 07:50:28

線程安全代碼

2017-12-05 07:57:34

2013-03-11 17:37:36

大數據
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 综合九九 | 久久久婷婷 | 人人爽人人爽人人片av | 亚洲综合电影 | 毛片免费视频 | 操一草 | 久久久精 | 美日韩精品| 精品香蕉一区二区三区 | 成人片免费看 | 欧美日韩中文字幕 | 日韩免费一二三区 | 国产伦精品一区二区三区高清 | 久久成人av电影 | 日韩中文一区二区 | 国产黄色av电影 | 欧美美乳 | 久草热在线 | 久久久久久久久99 | 性高湖久久久久久久久 | ww亚洲ww亚在线观看 | 日韩中文字幕在线 | 国产永久免费 | 精品99在线| 成人性视频免费网站 | 精品国产乱码久久久久久丨区2区 | 在线亚州 | pacopacomama在线| 久久久久亚洲精品 | 美女拍拍拍网站 | 日韩精品1区2区3区 成人黄页在线观看 | 天堂成人国产精品一区 | 欧美精品一区在线发布 | 国产精品成人一区二区三区 | 国产精品久久久久久久久免费樱桃 | 在线不卡| 欧美888 | 精品无码久久久久久久动漫 | 丁香久久 | 亚洲国产免费 | 视频一区二区中文字幕日韩 |