Notification與多執行緒

發表於2016-07-14

前幾天與同事討論到Notification在多執行緒下的轉發問題,所以就此整理一下。

先來看看官方的文件,是這樣寫的:

翻譯過來是:

也就是說,Notification的傳送與接收處理都是在同一個執行緒中。為了說明這一點,我們先來看一個示例:

程式碼清單1:Notification的傳送與處理

其輸出結果如下:

可以看到,雖然我們在主執行緒中註冊了通知的觀察者,但在全域性佇列中post的Notification,並不是在主執行緒處理的。所以,這時候就需要注意,如果我們想在回撥中處理與UI相關的操作,需要確保是在主執行緒中執行回撥。

這時,就有一個問題了,如果我們的Notification是在二級執行緒中post的,如何能在主執行緒中對這個Notification進行處理呢?或者換個提法,如果我們希望一個Notification的post執行緒與轉發執行緒不是同一個執行緒,應該怎麼辦呢?我們看看官方文件是怎麼說的:

這裡講到了“重定向”,就是我們在Notification所在的預設執行緒中捕獲這些分發的通知,然後將其重定向到指定的執行緒中。

一種重定向的實現思路是自定義一個通知佇列(注意,不是NSNotificationQueue物件,而是一個陣列),讓這個佇列去維護那些我們需要重定向的Notification。我們仍然是像平常一樣去註冊一個通知的觀察者,當Notification來了時,先看看post這個Notification的執行緒是不是我們所期望的執行緒,如果不是,則將這個Notification儲存到我們的佇列中,併傳送一個訊號(signal)到期望的執行緒中,來告訴這個執行緒需要處理一個Notification。指定的執行緒在收到訊號後,將Notification從佇列中移除,並進行處理。

官方文件已經給出了示例程式碼,在此借用一下,以測試實際結果:

程式碼清單2:在不同執行緒中post和轉發一個Notification

執行後,其輸出如下:

可以看到,我們在全域性dispatch佇列中丟擲的Notification,如願地在主執行緒中接收到了。

這種實現方式的具體解析及其侷限性大家可以參考官方文件Delivering Notifications To Particular Threads,在此不多做解釋。當然,更好的方法可能是我們自己去子類化一個NSNotificationCenter,或者單獨寫一個類來處理這種轉發。

NSNotificationCenter的執行緒安全性

蘋果之所以採取通知中心在同一個執行緒中post和轉發同一訊息這一策略,應該是出於執行緒安全的角度來考量的。官方文件告訴我們,NSNotificationCenter是一個執行緒安全類,我們可以在多執行緒環境下使用同一個NSNotificationCenter物件而不需要加鎖。原文在Threading Programming Guide中,具體如下:

我們可以在任何執行緒中新增/刪除通知的觀察者,也可以在任何執行緒中post一個通知。

NSNotificationCenter線上程安全性方面已經做了不少工作了,那是否意味著我們可以高枕無憂了呢?再回過頭來看看第一個例子,我們稍微改造一下,一點一點來:

程式碼清單3:NSNotificationCenter的通用模式

上面的程式碼就是我們通常所做的事情:新增一個通知監聽者,定義一個回撥,並在所屬物件釋放時移除監聽者;然後在程式的某個地方post一個通知。簡單明瞭,如果這一切都是發生在一個執行緒裡面,或者至少dealloc方法是在-postNotificationName:的執行緒中執行的(注意:NSNotification的post和轉發是同步的),那麼都OK,沒有執行緒安全問題。但如果dealloc方法和-postNotificationName:方法不在同一個執行緒中執行時,會出現什麼問題呢?

我們再改造一下上面的程式碼:

程式碼清單4:NSNotificationCenter引發的執行緒安全問題

這段程式碼是在主執行緒新增了一個TEST_NOTIFICATION通知的監聽者,並在主執行緒中將其移除,而我們的NSNotification是在後臺執行緒中post的。在通知處理函式中,我們讓回撥所在的執行緒睡眠1秒鐘,然後再去設定屬性i值。這時會發生什麼呢?我們先來看看輸出結果:

經典的記憶體錯誤,程式崩潰了。其實從輸出結果中,我們就可以看到到底是發生了什麼事。我們簡要描述一下:

  1. 當我們註冊一個觀察者是,通知中心會持有觀察者的一個弱引用,來確保觀察者是可用的。
  2. 主執行緒呼叫dealloc操作會讓Observer物件的引用計數減為0,這時物件會被釋放掉。
  3. 後臺執行緒傳送一個通知,如果此時Observer還未被釋放,則會向其轉發訊息,並執行回撥方法。而如果在回撥執行的過程中物件被釋放了,就會出現上面的問題。

當然,上面這個例子是故意而為之,但不排除在實際編碼中會遇到類似的問題。雖然NSNotificationCenter是執行緒安全的,但並不意味著我們在使用時就可以保證執行緒安全的,如果稍不注意,還是會出現執行緒問題。

那我們該怎麼做呢?這裡有一些好的建議:

  1. 儘量在一個執行緒中處理通知相關的操作,大部分情況下,這樣做都能確保通知的正常工作。不過,我們無法確定到底會在哪個執行緒中呼叫dealloc方法,所以這一點還是比較困難。
  2. 註冊監聽都時,使用基於block的API。這樣我們在block還要繼續呼叫self的屬性或方法,就可以通過weak-strong的方式來處理。具體大家可以改造下上面的程式碼試試是什麼效果。
  3. 使用帶有安全生命週期的物件,這一點物件單例物件來說再合適不過了,在應用的整個生命週期都不會被釋放。
  4. 使用代理。

小結

NSNotificationCenter雖然是執行緒安全的,但不要被這個事實所誤導。在涉及到多執行緒時,我們還是需要多加小心,避免出現上面的執行緒問題。想進一步瞭解的話,可以檢視Observers and Thread Safety

參考

  1. Notification Programming Topics
  2. Threading Programming Guide
  3. NSNotification的幾點說明
  4. NSNotificationCenter is thread-safe NOT
  5. Observers and Thread Safety

相關文章