多線程下的轉(zhuǎn)發(fā)問題

前幾天與同事討論到Notification在多線程下的轉(zhuǎn)發(fā)問題,所以就此整理一下墩莫。

先來看看官方的文檔宾尚,是這樣寫的:

In a multithreaded application, notifications are always delivered in the thread in which the notification was posted, which may not be the same thread in which an observer registered itself.

翻譯過來是:

在多線程應(yīng)用中,Notification在哪個(gè)線程中post捐凭,就在哪個(gè)線程中被轉(zhuǎn)發(fā)记舆,而不一定是在注冊(cè)觀察者的那個(gè)線程中鸽捻。

也就是說,Notification的發(fā)送與接收處理都是在同一個(gè)線程中泽腮。為了說明這一點(diǎn)御蒲,我們先來看一個(gè)示例:

代碼清單1:Notification的發(fā)送與處理

implementation ViewController
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    NSLog(@"current thread = %@", [NSThread currentThread]);
 
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil];
 
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
        [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil userInfo:nil];
    });
}
 
- (void)handleNotification:(NSNotification *)notification
{
    NSLog(@"current thread = %@", [NSThread currentThread]);
 
    NSLog(@"test notification");
}
 
@end

其輸出結(jié)果如下:

2016-02-13 22:05:12.856 test[865:45102] current thread = {number = 1, name = main}
2016-02-13 22:05:12.857 test[865:45174] current thread = {number = 2, name = (null)}
2016-02-13 22:05:12.857 test[865:45174] test notification

可以看到,雖然我們?cè)谥骶€程中注冊(cè)了通知的觀察者诊赊,但在全局隊(duì)列中post的Notification厚满,并不是在主線程處理的。所以碧磅,這時(shí)候就需要注意碘箍,如果我們想在回調(diào)中處理與UI相關(guān)的操作,需要確保是在主線程中執(zhí)行回調(diào)鲸郊。

這時(shí)丰榴,就有一個(gè)問題了,如果我們的Notification是在二級(jí)線程中post的秆撮,如何能在主線程中對(duì)這個(gè)Notification進(jìn)行處理呢四濒?或者換個(gè)提法,如果我們希望一個(gè)Notification的post線程與轉(zhuǎn)發(fā)線程不是同一個(gè)線程职辨,應(yīng)該怎么辦呢盗蟆?我們看看官方文檔是怎么說的:

For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.

這里講到了“重定向”,就是我們?cè)贜otification所在的默認(rèn)線程中捕獲這些分發(fā)的通知舒裤,然后將其重定向到指定的線程中喳资。

一種重定向的實(shí)現(xiàn)思路是自定義一個(gè)通知隊(duì)列(注意,不是NSNotificationQueue對(duì)象腾供,而是一個(gè)數(shù)組)仆邓,讓這個(gè)隊(duì)列去維護(hù)那些我們需要重定向的Notification亏栈。我們?nèi)匀皇窍衿匠R粯尤プ?cè)一個(gè)通知的觀察者,當(dāng)Notification來了時(shí)宏赘,先看看post這個(gè)Notification的線程是不是我們所期望的線程,如果不是黎侈,則將這個(gè)Notification存儲(chǔ)到我們的隊(duì)列中察署,并發(fā)送一個(gè)信號(hào)(signal)到期望的線程中,來告訴這個(gè)線程需要處理一個(gè)Notification峻汉。指定的線程在收到信號(hào)后贴汪,將Notification從隊(duì)列中移除,并進(jìn)行處理休吠。

官方文檔已經(jīng)給出了示例代碼扳埂,在此借用一下,以測(cè)試實(shí)際結(jié)果:

代碼清單2:在不同線程中post和轉(zhuǎn)發(fā)一個(gè)Notification

@interface ViewController () @property (nonatomic) NSMutableArray    *notifications;         // 通知隊(duì)列
@property (nonatomic) NSThread          *notificationThread;    // 期望線程
@property (nonatomic) NSLock            *notificationLock;      // 用于對(duì)通知隊(duì)列加鎖的鎖對(duì)象瘤礁,避免線程沖突
@property (nonatomic) NSMachPort        *notificationPort;      // 用于向期望線程發(fā)送信號(hào)的通信端口
 
@end
 
@implementation ViewController
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    NSLog(@"current thread = %@", [NSThread currentThread]);
 
    // 初始化
    self.notifications = [[NSMutableArray alloc] init];
    self.notificationLock = [[NSLock alloc] init];
 
    self.notificationThread = [NSThread currentThread];
    self.notificationPort = [[NSMachPort alloc] init];
    self.notificationPort.delegate = self;
 
    // 往當(dāng)前線程的run loop添加端口源
    // 當(dāng)Mach消息到達(dá)而接收線程的run loop沒有運(yùn)行時(shí)阳懂,則內(nèi)核會(huì)保存這條消息,直到下一次進(jìn)入run loop
    [[NSRunLoop currentRunLoop] addPort:self.notificationPort
                                forMode:(__bridge NSString *)kCFRunLoopCommonModes];
 
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"TestNotification" object:nil];
 
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
        [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil userInfo:nil];
 
    });
}
 
- (void)handleMachMessage:(void *)msg {
 
    [self.notificationLock lock];
 
    while ([self.notifications count]) {
        NSNotification *notification = [self.notifications objectAtIndex:0];
        [self.notifications removeObjectAtIndex:0];
        [self.notificationLock unlock];
        [self processNotification:notification];
        [self.notificationLock lock];
    };
 
    [self.notificationLock unlock];
}
 
- (void)processNotification:(NSNotification *)notification {
 
    if ([NSThread currentThread] != _notificationThread) {
        // Forward the notification to the correct thread.
        [self.notificationLock lock];
        [self.notifications addObject:notification];
        [self.notificationLock unlock];
        [self.notificationPort sendBeforeDate:[NSDate date]
                                   components:nil
                                         from:nil
                                     reserved:0];
    }
    else {
        // Process the notification here;
        NSLog(@"current thread = %@", [NSThread currentThread]);
        NSLog(@"process notification");
    }
}
 
@end

運(yùn)行后柜思,其輸出如下:

2016-02-13 23:38:31.637 test[1474:92483] current thread = {number = 1, name = main}
2016-02-13 23:38:31.663 test[1474:92483] current thread = {number = 1, name = main}
2016-02-13 23:38:31.663 test[1474:92483] process notification

可以看到岩调,我們?cè)谌謉ispatch隊(duì)列中拋出的Notification,如愿地在主線程中接收到了赡盘。

這種實(shí)現(xiàn)方式的具體解析及其局限性大家可以參考官方文檔Delivering Notifications To Particular Threads号枕,在此不多做解釋。當(dāng)然陨享,更好的方法可能是我們自己去子類化一個(gè)NSNotificationCenter葱淳,或者單獨(dú)寫一個(gè)類來處理這種轉(zhuǎn)發(fā)。

NSNotificationCenter的線程安全性

蘋果之所以采取通知中心在同一個(gè)線程中post和轉(zhuǎn)發(fā)同一消息這一策略抛姑,應(yīng)該是出于線程安全的角度來考量的赞厕。官方文檔告訴我們,NSNotificationCenter是一個(gè)線程安全類定硝,我們可以在多線程環(huán)境下使用同一個(gè)NSNotificationCenter對(duì)象而不需要加鎖坑傅。原文在Threading Programming Guide中,具體如下:

The following classes and functions are generally considered to be thread-safe. You can use the same instance from multiple threads without first acquiring a lock.
 
NSArray
...
NSNotification
NSNotificationCenter
...

我們可以在任何線程中添加/刪除通知的觀察者喷斋,也可以在任何線程中post一個(gè)通知唁毒。

NSNotificationCenter在線程安全性方面已經(jīng)做了不少工作了,那是否意味著我們可以高枕無憂了呢星爪?再回過頭來看看第一個(gè)例子浆西,我們稍微改造一下,一點(diǎn)一點(diǎn)來:

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

@interface Observer : NSObject
 
@end
 
@implementation Observer
 
- (instancetype)init
{
    self = [super init];
 
    if (self)
    {
        _poster = [[Poster alloc] init];
 
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil]
    }
 
    return self;
}
 
- (void)handleNotification:(NSNotification *)notification
{
    NSLog(@"handle notification ");
}
 
- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
 
@end
 
// 其它地方
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];

上面的代碼就是我們通常所做的事情:添加一個(gè)通知監(jiān)聽者顽腾,定義一個(gè)回調(diào)近零,并在所屬對(duì)象釋放時(shí)移除監(jiān)聽者诺核;然后在程序的某個(gè)地方post一個(gè)通知。簡單明了久信,如果這一切都是發(fā)生在一個(gè)線程里面窖杀,或者至少dealloc方法是在-postNotificationName:的線程中運(yùn)行的(注意:NSNotification的post和轉(zhuǎn)發(fā)是同步的),那么都OK裙士,沒有線程安全問題入客。但如果dealloc方法和-postNotificationName:方法不在同一個(gè)線程中運(yùn)行時(shí),會(huì)出現(xiàn)什么問題呢腿椎?我們?cè)俑脑煲幌律厦娴拇a:

代碼清單4:NSNotificationCenter引發(fā)的線程安全問題

#pragma mark - Poster
 
@interface Poster : NSObject
 
@end
 
@implementation Poster
 
- (instancetype)init
{
    self = [super init];
 
    if (self)
    {
        [self performSelectorInBackground:@selector(postNotification) withObject:nil];
    }
 
    return self;
}
 
- (void)postNotification
{
    [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
}
 
@end
 
#pragma mark - Observer
 
@interface Observer : NSObject
{
    Poster  *_poster;
}
 
@property (nonatomic, assign) NSInteger i;
 
@end
 
@implementation Observer
 
- (instancetype)init
{
    self = [super init];
 
    if (self)
    {
        _poster = [[Poster alloc] init];
 
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil];
    }
 
    return self;
}
 
- (void)handleNotification:(NSNotification *)notification
{
    NSLog(@"handle notification begin");
    sleep(1);
    NSLog(@"handle notification end");
 
    self.i = 10;
}
 
- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
 
    NSLog(@"Observer dealloc");
}
 
@end
 
#pragma mark - ViewController
 
@implementation ViewController
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    __autoreleasing Observer *observer = [[Observer alloc] init];
}
 
@end

這段代碼是在主線程添加了一個(gè)TEST_NOTIFICATION通知的監(jiān)聽者桌硫,并在主線程中將其移除,而我們的NSNotification是在后臺(tái)線程中post的啃炸。在通知處理函數(shù)中铆隘,我們讓回調(diào)所在的線程睡眠1秒鐘,然后再去設(shè)置屬性i值南用。這時(shí)會(huì)發(fā)生什么呢膀钠?我們先來看看輸出結(jié)果:

2016-02-13 00:31:41.286 SKTest[932:88791] handle notification begin
2016-02-13 00:31:41.291 SKTest[932:88713] Observer dealloc
2016-02-13 00:31:42.361 SKTest[932:88791] handle notification end
(lldb) 
 
// 程序在self.i = 10處拋出了"Thread 6: EXC_BAD_ACCESS(code=EXC_I386_GPFLT)"

經(jīng)典的內(nèi)存錯(cuò)誤,程序崩潰了裹虫。其實(shí)從輸出結(jié)果中托修,我們就可以看到到底是發(fā)生了什么事。我們簡要描述一下:

  • 當(dāng)我們注冊(cè)一個(gè)觀察者是恒界,通知中心會(huì)持有觀察者的一個(gè)弱引用睦刃,來確保觀察者是可用的。
  • 主線程調(diào)用dealloc操作會(huì)讓Observer對(duì)象的引用計(jì)數(shù)減為0十酣,這時(shí)對(duì)象會(huì)被釋放掉涩拙。
  • 后臺(tái)線程發(fā)送一個(gè)通知,如果此時(shí)Observer還未被釋放耸采,則會(huì)用其轉(zhuǎn)出消息兴泥,并執(zhí)行回調(diào)方法。而如果在回調(diào)執(zhí)行的過程中對(duì)象被釋放了虾宇,就會(huì)出現(xiàn)上面的問題搓彻。

當(dāng)然,上面這個(gè)例子是故意而為之嘱朽,但不排除在實(shí)際編碼中會(huì)遇到類似的問題旭贬。雖然NSNotificationCenter是線程安全的,但并不意味著我們?cè)谑褂脮r(shí)就可以保證線程安全的搪泳,如果稍不注意稀轨,還是會(huì)出現(xiàn)線程問題。

那我們?cè)撛趺醋瞿匕毒窟@里有一些好的建議:

  • 盡量在一個(gè)線程中處理通知相關(guān)的操作奋刽,大部分情況下瓦侮,這樣做都能確保通知的正常工作。不過佣谐,我們無法確定到底會(huì)在哪個(gè)線程中調(diào)用dealloc方法肚吏,所以這一點(diǎn)還是比較困難。
  • 注冊(cè)監(jiān)聽都時(shí)狭魂,使用基于block的API罚攀。這樣我們?cè)赽lock還要繼續(xù)調(diào)用self的屬性或方法招刨,就可以通過weak-strong的方式來處理。具體大家可以改造下上面的代碼試試是什么效果寇仓。
  • 使用帶有安全生命周期的對(duì)象又官,這一點(diǎn)對(duì)象單例對(duì)象來說再合適不過了,在應(yīng)用的整個(gè)生命周期都不會(huì)被釋放检柬。
  • 使用代理。

小結(jié)

NSNotificationCenter雖然是線程安全的,但不要被這個(gè)事實(shí)所誤導(dǎo)任柜。在涉及到多線程時(shí),我們還是需要多加小心沛厨,避免出現(xiàn)上面的線程問題宙地。想進(jìn)一步了解的話,可以查看Observers and Thread Safety逆皮。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宅粥,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子电谣,更是在濱河造成了極大的恐慌秽梅,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剿牺,死亡現(xiàn)場(chǎng)離奇詭異企垦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)晒来,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門钞诡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人湃崩,你說我怎么就攤上這事荧降。” “怎么了攒读?”我有些...
    開封第一講書人閱讀 157,490評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵誊抛,是天一觀的道長。 經(jīng)常有香客問我整陌,道長拗窃,這世上最難降的妖魔是什么瞎领? 我笑而不...
    開封第一講書人閱讀 56,521評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮随夸,結(jié)果婚禮上九默,老公的妹妹穿的比我還像新娘。我一直安慰自己宾毒,他們只是感情好驼修,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著诈铛,像睡著了一般乙各。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上幢竹,一...
    開封第一講書人閱讀 49,842評(píng)論 1 290
  • 那天耳峦,我揣著相機(jī)與錄音,去河邊找鬼焕毫。 笑死蹲坷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的邑飒。 我是一名探鬼主播循签,決...
    沈念sama閱讀 38,997評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼疙咸!你這毒婦竟也來了县匠?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,741評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤撒轮,失蹤者是張志新(化名)和其女友劉穎聚唐,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腔召,經(jīng)...
    沈念sama閱讀 44,203評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡杆查,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了臀蛛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亲桦。...
    茶點(diǎn)故事閱讀 38,673評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖浊仆,靈堂內(nèi)的尸體忽然破棺而出客峭,到底是詐尸還是另有隱情,我是刑警寧澤抡柿,帶...
    沈念sama閱讀 34,339評(píng)論 4 330
  • 正文 年R本政府宣布舔琅,位于F島的核電站,受9級(jí)特大地震影響洲劣,放射性物質(zhì)發(fā)生泄漏备蚓。R本人自食惡果不足惜课蔬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望郊尝。 院中可真熱鬧二跋,春花似錦、人聲如沸流昏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽况凉。三九已至谚鄙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間刁绒,已是汗流浹背闷营。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留膛锭,地道東北人粮坞。 一個(gè)月前我還...
    沈念sama閱讀 46,394評(píng)論 2 360
  • 正文 我出身青樓蚊荣,卻偏偏與公主長得像初狰,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子互例,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容

  • 近期接觸項(xiàng)目中用的通知比較多奢入,對(duì)于通知有一個(gè)系統(tǒng)的理解與學(xué)習(xí),已下是做的一些總結(jié) 先來看看官方的文檔媳叨,是這樣寫的:...
    9de75b652cd9閱讀 590評(píng)論 0 0
  • http://www.cocoachina.com/ios/20150316/11335.html iOS線程no...
    紫色冰雨閱讀 233評(píng)論 0 0
  • 以下內(nèi)容基本上基于蘋果官方文檔,可能會(huì)有些許地方翻譯不準(zhǔn)確腥光,歡迎指正!個(gè)人實(shí)驗(yàn)代碼在這里 一糊秆、需求(為什么要通知)...
    大鵬鳥閱讀 973評(píng)論 0 4
  • NSNotificationCenter對(duì)象(通知中心)提供了在程序中廣播消息的機(jī)制武福,它實(shí)質(zhì)上就是一個(gè)通知分發(fā)表。...
    9de75b652cd9閱讀 751評(píng)論 0 1
  • 不覺間伍纫,今年已經(jīng)是已經(jīng)高中畢業(yè)的第十個(gè)年頭,不得不感嘆歲月如梭昂芜。過往的十年間莹规,有人結(jié)婚,有人生子泌神,有人獨(dú)行良漱,有人不...
    大餅說科教閱讀 538評(píng)論 0 0