總結ing

1,iOS的GCD中如何關閉或者殺死一個還沒執行完的後臺線程?html

舉例來講,我經過導航進入到了一個視圖,這個視圖加載的時候會新建一個線程在後臺運行,假設這個線程須要從網絡中讀取許多數據,須要必定的時間,而且會在獲取所有數據之後刷新UI界面(經過dispatch_async(dispatch_get_main_queue(), ^{...});),我可能在它還未運行完的時候經過導航返回到上一層視圖,因此我想在我返回前殺死或者結束這個還未執行完的線程,應該怎麼作? 還有,若是我聽任這個線程,在我返回到上層視圖的時候,本視圖應該會被釋放,那麼線程中有關主線程中運行的代碼會怎麼辦?數組

回答:GCD自己是沒有提供這樣的API的。想要實現這樣的功能須要本身在代碼裏實現:安全

著做權歸做者全部。
商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
做者:劉子洋
連接:https://www.zhihu.com/question/23919984/answer/26129416
來源:知乎

@interface Canceller
{
    BOOL _shouldCancel;
}
- (void)setShouldCancel:(BOOL)shouldCancel;
- (BOOL)shouldCancel;
@end

@implementation Canceller
- (void)setShouldCancel:(BOOL)shouldCancel {
    _shouldCancel = shouldCancel;
}
- (BOOL)shouldCancel {
    return _shouldCancel;
}
@end

static void test(int a){
    static Canceller * canceller = nil;

    if(q){
        [canceller setShouldCancel:YES];
        [canceller release];
        dispatch_suspend(q);
        dispatch_release(q);
        q=nil;
    }
    canceller = [[Canceller alloc] init];
    q=dispatch_get_global_queue(0,0);
    dispatch_async(q,^ {
        while(![canceller shouldCancel]){NSLog(@"query %d",a);sleep(2);}
    });

}

 

2.  Notification與多線程網絡

前幾天與同事討論到Notification在多線程下的轉發問題,因此就此整理一下。多線程

先來看看官方的文檔,是這樣寫的:併發

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.app

翻譯過來是:async

在多線程應用中,Notification在哪一個線程中post,就在哪一個線程中被轉發,而不必定是在註冊觀察者的那個線程中。ide

也就是說,Notification的發送與接收處理都是在同一個線程中。爲了說明這一點,咱們先來看一個示例:函數

代碼清單1:Notification的發送與處理

@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

其輸出結果以下:

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

能夠看到,雖然咱們在主線程中註冊了通知的觀察者,但在全局隊列中post的Notification,並非在主線程處理的。因此,這時候就須要注意,若是咱們想在回調中處理與UI相關的操做,須要確保是在主線程中執行回調。

這時,就有一個問題了,若是咱們的Notification是在二級線程中post的,如何能在主線程中對這個Notification進行處理呢?或者換個提法,若是咱們但願一個Notification的post線程與轉發線程不是同一個線程,應該怎麼辦呢?咱們看看官方文檔是怎麼說的:

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.

這裏講到了「重定向」,就是咱們在Notification所在的默認線程中捕獲這些分發的通知,而後將其重定向到指定的線程中。

一種重定向的實現思路是自定義一個通知隊列(注意,不是NSNotificationQueue對象,而是一個數組),讓這個隊列去維護那些咱們須要重定向的Notification。咱們仍然是像日常同樣去註冊一個通知的觀察者,當Notification來了時,先看看post這個Notification的線程是否是咱們所指望的線程,若是不是,則將這個Notification存儲到咱們的隊列中,併發送一個信號(signal)到指望的線程中,來告訴這個線程須要處理一個Notification。指定的線程在收到信號後,將Notification從隊列中移除,並進行處理。

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

代碼清單2:在不一樣線程中post和轉發一個Notification

@interface ViewController () @property (nonatomic) NSMutableArray    *notifications;         // 通知隊列
@property (nonatomic) NSThread          *notificationThread;    // 指望線程
@property (nonatomic) NSLock            *notificationLock;      // 用於對通知隊列加鎖的鎖對象,避免線程衝突
@property (nonatomic) NSMachPort        *notificationPort;      // 用於向指望線程發送信號的通訊端口

@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;

    // 往當前線程的run loop添加端口源
    // 當Mach消息到達而接收線程的run loop沒有運行時,則內核會保存這條消息,直到下一次進入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

運行後,其輸出以下:

2015-03-11 23:38:31.637 test[1474:92483] current thread = {number = 1, name = main}
2015-03-11 23:38:31.663 test[1474:92483] current thread = {number = 1, name = main}
2015-03-11 23:38:31.663 test[1474:92483] process notification

能夠看到,咱們在全局dispatch隊列中拋出的Notification,如願地在主線程中接收到了。

這種實現方式的具體解析及其侷限性你們能夠參考官方文檔Delivering Notifications To Particular Threads,在此很少作解釋。固然,更好的方法多是咱們本身去子類化一個NSNotificationCenter,或者單獨寫一個類來處理這種轉發。

NSNotificationCenter的線程安全性

蘋果之因此採起通知中心在同一個線程中post和轉發同一消息這一策略,應該是出於線程安全的角度來考量的。官方文檔告訴咱們,NSNotificationCenter是一個線程安全類,咱們能夠在多線程環境下使用同一個NSNotificationCenter對象而不須要加鎖。原文在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一個通知。

NSNotificationCenter在線程安全性方面已經作了很多工做了,那是否意味着咱們能夠高枕無憂了呢?再回過頭來看看第一個例子,咱們稍微改造一下,一點一點來:

代碼清單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];

上面的代碼就是咱們一般所作的事情:添加一個通知監聽者,定義一個回調,並在所屬對象釋放時移除監聽者;而後在程序的某個地方post一個通知。簡單明瞭,若是這一切都是發生在一個線程裏面,或者至少dealloc方法是在-postNotificationName:的線程中運行的(注意:NSNotification的post和轉發是同步的),那麼都OK,沒有線程安全問題。但若是dealloc方法和-postNotificationName:方法不在同一個線程中運行時,會出現什麼問題呢?

咱們再改造一下上面的代碼:

代碼清單4:NSNotificationCenter引起的線程安全問題

#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

這段代碼是在主線程添加了一個TEST_NOTIFICATION通知的監聽者,並在主線程中將其移除,而咱們的NSNotification是在後臺線程中post的。在通知處理函數中,咱們讓回調所在的線程睡眠1秒鐘,而後再去設置屬性i值。這時會發生什麼呢?咱們先來看看輸出結果:

2015-03-14 00:31:41.286 SKTest[932:88791] handle notification begin
2015-03-14 00:31:41.291 SKTest[932:88713] Observer dealloc
2015-03-14 00:31:42.361 SKTest[932:88791] handle notification end
(lldb) 

// 程序在self.i = 10處拋出了"Thread 6: EXC_BAD_ACCESS(code=EXC_I386_GPFLT)"

經典的內存錯誤,程序崩潰了。其實從輸出結果中,咱們就能夠看到究竟是發生了什麼事。咱們簡要描述一下:

  1. 當咱們註冊一個觀察者是,通知中心會持有觀察者的一個弱引用,來確保觀察者是可用的。

  2. 主線程調用dealloc操做會讓Observer對象的引用計數減爲0,這時對象會被釋放掉。

  3. 後臺線程發送一個通知,若是此時Observer還未被釋放,則會用其轉出消息,並執行回調方法。而若是在回調執行的過程當中對象被釋放了,就會出現上面的問題。

固然,上面這個例子是故意而爲之,但不排除在實際編碼中會遇到相似的問題。雖然NSNotificationCenter是線程安全的,但並不意味着咱們在使用時就能夠保證線程安全的,若是稍不注意,仍是會出現線程問題。

那咱們該怎麼作呢?這裏有一些好的建議:

  1. 儘可能在一個線程中處理通知相關的操做,大部分狀況下,這樣作都能確保通知的正常工做。不過,咱們沒法肯定到底會在哪一個線程中調用dealloc方法,因此這一點仍是比較困難。

  2. 註冊監聽都時,使用基於block的API。這樣咱們在block還要繼續調用self的屬性或方法,就能夠經過weak-strong的方式來處理。具體你們能夠改造下上面的代碼試試是什麼效果。

  3. 使用帶有安全生命週期的對象,這一點對象單例對象來講再合適不過了,在應用的整個生命週期都不會被釋放。

  4. 使用代理。

小結

NSNotificationCenter雖然是線程安全的,但不要被這個事實所誤導。在涉及到多線程時,咱們仍是須要多加當心,避免出現上面的線程問題。想進一步瞭解的話,能夠查看Observers and Thread Safety

相關文章
相關標籤/搜索