Dispatch barriers處理讀與寫的衝突

摘錄自:http://www.cocoachina.com/industry/20140428/8248.html


處理讀者與寫者問題
線程安全實例不是處理單例時的唯一問題。如果單例屬性表示一個可變對象,那麼你就需要考慮是否那個對象自身線程安全。
 
如果問題中的這個對象是一個 Foundation 容器類,那麼答案是——“很可能不安全”!Apple 維護一個有用且有些心寒的列表,衆多的 Foundation 類都不是線程安全的。 NSMutableArray,已用於你的單例,正在那個列表裏休息。
 
雖然許多線程可以同時讀取 NSMutableArray 的一個實例而不會產生問題,但當一個線程正在讀取時讓另外一個線程修改數組就是不安全的。你的單例在目前的狀況下不能預防這種情況的發生。
 
要分析這個問題,看看 PhotoManager.m 中的 addPhoto:,轉載如下:
  1. - (void)addPhoto:(Photo *)photo 
  2.     if (photo) { 
  3.         [_photosArray addObject:photo]; 
  4.         dispatch_async(dispatch_get_main_queue(), ^{ 
  5.             [self postContentAddedNotification]; 
  6.         }); 
  7.     } 
這是一個寫方法,它修改一個私有可變數組對象。
 
現在看看 photos ,轉載如下: 
  1. - (NSArray *)photos 
  2.   return [NSArray arrayWithArray:_photosArray]; 
 
這是所謂的讀方法,它讀取可變數組。它爲調用者生成一個不可變的拷貝,防止調用者不當地改變數組,但這不能提供任何保護來對抗當一個線程調用讀方法 photos 的同時另一個線程調用寫方法 addPhoto: 。
 
這就是軟件開發中經典的讀者寫者問題。GCD 通過用 dispatch barriers 創建一個讀者寫者鎖 提供了一個優雅的解決方案。
 
Dispatch barriers 是一組函數,在併發隊列上工作時扮演一個串行式的瓶頸。使用 GCD 的障礙(barrier)API 確保提交的 Block 在那個特定時間上是指定隊列上唯一被執行的條目。這就意味着所有的先於調度障礙提交到隊列的條目必能在這個 Block 執行前完成。
 
當這個 Block 的時機到達,調度障礙執行這個 Block 並確保在那個時間裏隊列不會執行任何其它 Block 。一旦完成,隊列就返回到它默認的實現狀態。 GCD 提供了同步和異步兩種障礙函數。
 
下圖顯示了障礙函數對多個異步隊列的影響: 

 

注意到正常部分的操作就如同一個正常的併發隊列。但當障礙執行時,它本質上就如同一個串行隊列。也就是,障礙是唯一在執行的事物。在障礙完成後,隊列回到一個正常併發隊列的樣子。
 
下面是你何時會——和不會——使用障礙函數的情況:
1. 自定義串行隊列:一個很壞的選擇;障礙不會有任何幫助,因爲不管怎樣,一個串行隊列一次都只執行一個操作。
2. 全局併發隊列:要小心;這可能不是最好的主意,因爲其它系統可能在使用隊列而且你不能壟斷它們只爲你自己的目的。
3. 自定義併發隊列:這對於原子或臨界區代碼來說是極佳的選擇。任何你在設置或實例化的需要線程安全的事物都是使用障礙的最佳候選。
 
由於上面唯一像樣的選擇是自定義併發隊列,你將創建一個你自己的隊列去處理你的障礙函數並分開讀和寫函數。且這個併發隊列將允許多個多操作同時進行。
 
打開 PhotoManager.m,添加如下私有屬性到類擴展中:
  1. @interface PhotoManager () 
  2. @property (nonatomic,strong,readonly) NSMutableArray *photosArray; 
  3. @property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< Add this 
  4. @end 
 
找到 addPhoto: 並用下面的實現替換它:
  1. - (void)addPhoto:(Photo *)photo 
  2.     if (photo) { // 1 
  3.         dispatch_barrier_async(self.concurrentPhotoQueue, ^{ // 2  
  4.             [_photosArray addObject:photo]; // 3 
  5.             dispatch_async(dispatch_get_main_queue(), ^{ // 4 
  6.                 [self postContentAddedNotification];  
  7.             }); 
  8.         }); 
  9.     } 
 
你新寫的函數是這樣工作的:
1. 在執行下面所有的工作前檢查是否有合法的相片。
2. 添加寫操作到你的自定義隊列。當臨界區在稍後執行時,這將是你隊列中唯一執行的條目。
3. 這是添加對象到數組的實際代碼。由於它是一個障礙 Block ,這個 Block 永遠不會同時和其它 Block 一起在 concurrentPhotoQueue 中執行。
4. 最後你發送一個通知說明完成了添加圖片。這個通知將在主線程被髮送因爲它將會做一些 UI 工作,所以在此爲了通知,你異步地調度另一個任務到主線程。
這就處理了寫操作,但你還需要實現 photos 讀方法並實例化 concurrentPhotoQueue 。
 
在寫者打擾的情況下,要確保線程安全,你需要在 concurrentPhotoQueue 隊列上執行讀操作。既然你需要從函數返回,你就不能異步調度到隊列,因爲那樣在讀者函數返回之前不一定運行。
 
在這種情況下,dispatch_sync 就是一個絕好的候選。
 
dispatch_sync() 同步地提交工作並在返回前等待它完成。使用 dispatch_sync 跟蹤你的調度障礙工作,或者當你需要等待操作完成後才能使用 Block 處理過的數據。如果你使用第二種情況做事,你將不時看到一個 __block 變量寫在 dispatch_sync 範圍之外,以便返回時在 dispatch_sync 使用處理過的對象。
 
但你需要很小心。想像如果你調用 dispatch_sync 並放在你已運行着的當前隊列。這會導致死鎖,因爲調用會一直等待直到 Block 完成,但 Block 不能完成(它甚至不會開始!),直到當前已經存在的任務完成,而當前任務無法完成!這將迫使你自覺於你正從哪個隊列調用——以及你正在傳遞進入哪個隊列。
 
下面是一個快速總覽,關於在何時以及何處使用 dispatch_sync :
1. 自定義串行隊列:在這個狀況下要非常小心!如果你正運行在一個隊列並調用 dispatch_sync 放在同一個隊列,那你就百分百地創建了一個死鎖。
2. 主隊列(串行):同上面的理由一樣,必須非常小心!這個狀況同樣有潛在的導致死鎖的情況。
3. 併發隊列:這纔是做同步工作的好選擇,不論是通過調度障礙,或者需要等待一個任務完成才能執行進一步處理的情況。
 
繼續在 PhotoManager.m 上工作,用下面的實現替換 photos :
  1. - (NSArray *)photos 
  2.     __block NSArray *array; // 1 
  3.     dispatch_sync(self.concurrentPhotoQueue, ^{ // 2 
  4.         array = [NSArray arrayWithArray:_photosArray]; // 3 
  5.     }); 
  6.     return array; 
 
這就是你的讀函數。按順序看看編過號的註釋,有這些:
1. __block 關鍵字允許對象在 Block 內可變。沒有它,array 在 Block 內部就只是只讀的,你的代碼甚至不能通過編譯。
2. 在 concurrentPhotoQueue 上同步調度來執行讀操作。
3. 將相片數組存儲在 array 內並返回它。
 
最後,你需要實例化你的 concurrentPhotoQueue 屬性。修改 sharedManager 以便像下面這樣初始化隊列:
  1. + (instancetype)sharedManager 
  2.     static PhotoManager *sharedPhotoManager = nil; 
  3.     static dispatch_once_t onceToken; 
  4.     dispatch_once(&onceToken, ^{ 
  5.         sharedPhotoManager = [[PhotoManager alloc] init]; 
  6.         sharedPhotoManager->_photosArray = [NSMutableArray array]; 
  7.  
  8.         // ADD THIS: 
  9.         sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue"
  10.                                                     DISPATCH_QUEUE_CONCURRENT);  
  11.     }); 
  12.  
  13.     return sharedPhotoManager; 
 
這裏使用 dispatch_queue_create 初始化 concurrentPhotoQueue 爲一個併發隊列。第一個參數是反向DNS樣式命名慣例;確保它是描述性的,將有助於調試。第二個參數指定你的隊列是串行還是併發。
 
注意:當你在網上搜索例子時,你會經常看人們傳遞 0 或者 NULL 給 dispatch_queue_create 的第二個參數。這是一個創建串行隊列的過時方式;明確你的參數總是更好。
恭喜——你的 PhotoManager 單例現在是線程安全的了。不論你在何處或怎樣讀或寫你的照片,你都有這樣的自信,即它將以安全的方式完成,不會出現任何驚嚇。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章