zoukankan      html  css  js  c++  java
  • iOS 多线程 NSthread

     

    前言

    多线程的价值无需赘述,对于App性能和用户体验都有着至关重要的意义,在iOS开发中,Apple提供了不同的技术支持多线程编程,除了跨平台的pthread之外,还提供了NSThread、NSOperationQueue、GCD等多线程技术,从本篇Blog开始介绍这几种多线程技术的细节。

    对于pthread这种跨平台的多线程技术,这本Programming with POSIX Threads做了详细介绍,不再提及。

    NSThread

    使用NSThead创建线程有很多方法:

    • +detachNewThreadSelector:toTarget:withObject:类方法直接生成一个子线程
    1
    [NSThread detachNewThreadSelector:@selector(threadRoutine:) toTarget:self withObject:nil];
    
    • 创建一个NSThread类实例,然后调用start方法。
    1
    2
    NSThread* aThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRoutine:) object:nil];
    [aThread start];
    
    • 调用NSObject的+performSelectorInBackground:withObject:方法生成子线程。
    1
    [myObj performSelectorInBackground:@selector(threadRoutine:) withObject:nil];
    
    • 创建一个NSThread子类,然后调用子类实例的start方法,。

    创建线程也是有开销的,iOS下主要成本包括构造内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,不过可以使用方法-setStackSize:自己设置,注意必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间。

    第二种和第四种方法创建的线程有个好处是拥有线程的对象,因此可以使用performSelector:onThread:withObject:waitUntilDone:在该线程上执行方法,这是一种非常方便的线程间通讯的方法(相对于设置麻烦的NSPort用于通讯),所要执行的方法可以直接添加到目标线程的Runloop中执行。Apple建议使用这个接口运行的方法不要是耗时或者频繁的操作,以免子线程的负载过重。

    第三种方法其实与第一种方法是一样的,都会直接生成一个子线程。

    上面四种方法生成的子线程都是detached状态,即主线程结束时这些线程都会被直接杀死;如果要生成joinable状态的子线程,只能使用pthread接口啦。

    如果需要,可以设置线程的优先级(-setThreadPriority:);如果要在线程中保存一些状态信息,还可以使用到-threadDictionary得到一个NSMutableDictionary,以key-value的方式保存信息用于线程内读写。

    NSThread的入口方法

    要写一个有效的子线程入口方法需要注意很多问题,示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    - (void)threadRoutine
    {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
      BOOL moreWorkToDo = YES;
        BOOL exitNow = NO;
        NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
    
        NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
        [threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];
    
      //添加事件源
        [self myInstallCustomInputSource];
    
        while (moreWorkToDo && !exitNow)
        {
            //执行线程真正的工作方法,如果完成了可以设置moreWorkToDo为False
    
            [runLoop runUntilDate:[NSDate date]];
    
            exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
        }
    
        [pool release];
    }
    
    • 必须创建一个NSAutoreleasePool,因为子线程不会自动创建。同时要注意这个pool因为是最外层pool,如果线程中要进行长时间的操作生成大量autoreleased的对象,则只有在该子线程退出时才会回收,因此如果线程中会大量创建autoreleased对象,那么需要创建额外的NSAutoreleasePool,可以在NSRunloop每次迭代时创建和销毁一个NSAutoreleasePool。
    • 如果你的子线程会抛出异常,最好在子线程中设置一个异常处理函数,因为如果子线程无法处理抛出的异常,会导致程序直接Crash关闭。
    • (可选)设置Run Loop,如果子线程只是做个一次性的操作,那么无需设置Run Loop;如果子线程进入一个循环需要不断处理一些事件,那么设置一个Run Loop是最好的处理方式,如果需要Timer,那么Run Loop就是必须的。
    • 如果需要在子线程运行的时候让子线程结束操作,子线程每次Run Loop迭代中检查相应的标志位来判断是否还需要继续执行,可以使用threadDictionary以及设置Input Source的方式来通知这个子线程。那么什么是Run Loop呢?这是涉及NSThread及线程相关的编程时无法回避的一个问题。

    Run Loop

    Run Loop本身并不具备并发执行的功能,但是和多线程开发息息相关,而且概念令人迷惑,相关的介绍资料也很少,它的主要的特性如下:

    • 每个线程都有一个Run Loop,主线程的Run Loop会在App运行时自动运行,子线程中需要手动运行。
    • 每个Run Loop都会以一个模式mode来运行,可以使用NSRunLoop的- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate 方法运行在某个特定模式mode。
    • Run Loop的处理两大类事件源:Timer Source和Input Source(包括performSelector***方法簇、Port或者自定义Input Source),每个事件源都会绑定在Run Loop的某个特定模式mode上,而且只有RunLoop在这个模式运行的时候才会触发该Timer和Input Source。
    • 如果没有任何事件源添加到Run Loop上,Run Loop就会立刻exit。

    Run Loop接口

    要操作Run Loop,Foundation层和Core Foundation层都有对应的接口可以操作Run Loop。

    Foundation层对应的是NSRunLoop:

    Core Foundation层对应的是CFRunLoopRef:

    两组接口差不多,不过功能上还是有许多区别的,例如CF层可以添加自定义Input Source事件源(CFRunLoopSourceRef)和Run Loop观察者Observer(CFRunLoopObserverRef),很多类似功能的接口特性也是不一样的。

    Run Loop运行

    Run Loop如何运行呢?在上一节NSThread的入口函数中使用了一种NSRunLoop的使用场景,再看一例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    - (void)main
    {
        @autoreleasepool {
            NSLog(@"starting thread.......");
            NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(doTimerTask) userInfo:nil repeats:YES];
            [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
            [timer release];
            while (! self.isCancelled) {
                [self doOtherTask];
                BOOL ret = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
                NSLog(@"after runloop counting.........: %d", ret);
            }
            NSLog(@"finishing thread.........");
        }
    }
    
    - (void)doTimerTask
    {
        NSLog(@"do timer task");
    }
    
    - (void)doOtherTask
    {
        NSLog(@"do other task");
    }
    

    我们看到入口方法里创建了一个NSTimer,并且以NSDefaultRunLoopMode模式加入到当前子线程的NSRunLoop中。进入循环后肯定会执行-doOtherTask方式法一次,然后再以NSDefaultRunLoopMode模式运行NSRunLoop,如果一次Timer事件触发处理后,这个Run Loop会返回吗?答案是不会,Why?

    NSRunLoop的底层是由CFRunLoopRef实现的,你可以想象成一个循环或者类似Linux下select或者epoll,当没有事件触发时,你调用的Run Loop运行方法不会立刻返回,它会持续监听其他事件源,如果需要Run Loop会让子线程进入sleep等待状态而不是空转,只有当Timer Source或者Input Source事件发生时,子线程才会被唤醒,然后处理触发的事件,然而由于Timer source比较特殊,Timer Source事件发生处理后,Run Loop运行方法- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;也不会返回;而其他非Timer事件的触发处理会让这个Run Loop退出并返回YES。当Run Loop运行在一个特定模式时,如果该模式下没有事件源,运行Run Loop会立刻返回NO。

    NSRunLoop的运行接口:

    1
    2
    3
    4
    5
    6
    7
    8
    //运行 NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制
    - (void)run;
    
    //运行 NSRunLoop: 参数为运行模式、时间期限,返回值为YES表示是处理事件后返回的,NO表示是超时或者停止运行导致返回的
    - (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
    
    //运行 NSRunLoop: 参数为运时间期限,运行模式为默认的NSDefaultRunLoopMode模式 
    -(void)runUntilDate:(NSDate *)limitDate;
    

    CFRunLoopRef的运行接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //运行 CFRunLoopRef
    void CFRunLoopRun();
    
    //运行 CFRunLoopRef: 参数为运行模式、时间和是否在处理Input Source后退出标志,返回值是exit原因
    SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);
    
    //停止运行 CFRunLoopRef
    void CFRunLoopStop( CFRunLoopRef rl );
    
    //唤醒 CFRunLoopRef
    void CFRunLoopWakeUp ( CFRunLoopRef rl );
    

    详细讲解下NSRunLoop的三个运行接口:

    • - (void)run; 无条件运行

    不建议使用,因为这个接口会导致Run Loop永久性的运行在NSDefaultRunLoopMode模式,即使使用CFRunLoopStop(runloopRef);也无法停止Run Loop的运行,那么这个子线程就无法停止,只能永久运行下去。

    • - (void)runUntilDate:(NSDate *)limitDate; 有一个超时时间限制

    比上面的接口好点,有个超时时间,可以控制每次Run Loop的运行时间,也是运行在NSDefaultRunLoopMode模式。这个方法运行Run Loop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行Run Loop。注意CFRunLoopStop(runloopRef);也无法停止Run Loop的运行,因此最好自己设置一个合理的Run Loop运行时间。示例:

    1
    2
    3
    4
    5
    6
    while (!Done)
    {
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate
                    dateWithTimeIntervalSinceNow:10]];
        NSLog(@"exiting runloop.........:");
    }
    
    • - (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate; 有一个超时时间限制,而且设置运行模式

    这个接口在非Timer事件触发、显式的用CFRunLoopStop停止Run Loop、到达limitDate后会退出返回。如果仅是Timer事件触发并不会让Run Loop退出返回;如果是PerfromSelector***事件或者其他Input Source事件触发处理后,Run Loop会退出返回YES。示例:

    1
    2
    3
    4
    5
    6
    while (!Done)
    {
        BOOL ret = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                            beforeDate:[NSDate distantFuture]];
        NSLog(@"exiting runloop.........: %d", ret);
    }
    

    那么如何知道一个Run Loop是因为什么原因exit退出的呢?NSRunLoop中没有接口可以知道,而需要通过Core Foundation的接口来运行CFRunLoopRef,NSRunLoop其实就是CFRunLoopRef的二次封装。使用CFRunLoop的接口(C的接口)来运行Run Loop,有两个接口:

    • void CFRunLoopRun(void);

    运行在默认的kCFRunLoopDefaultMode模式下,直到使用CFRunLoopStop接口停止这个Run Loop,或者Run Loop的所有事件源都被删除。

    • SInt32 CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

    第一个参数是指RunLoop运行的模式(例如kCFRunLoopDefaultMode或者kCFRunLoopCommonModes),第二个参数是运行时间,第三个参数是是否在处理事件后让Run Loop退出返回。 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    while (!self.isCancelled)
    {
        [self doOtherTask];
    
        SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 2, YES);
        if (result == kCFRunLoopRunStopped)
        {
            [self cancel];
        }
        NSLog(@"exit run loop.........: %ld", result);
    }
    

    如果Run Loop退出返回后,返回值是SInt32类型(signed long),表明Run Loop返回的原因,目前有四种:

    1
    2
    3
    4
    5
    6
    enum {
        kCFRunLoopRunFinished = 1, //Run Loop结束,没有Timer或者其他Input Source
        kCFRunLoopRunStopped = 2, //Run Loop被停止,使用CFRunLoopStop停止Run Loop
        kCFRunLoopRunTimedOut = 3, //Run Loop超时
        kCFRunLoopRunHandledSource = 4 ////Run Loop处理完事件,注意Timer事件的触发是不会让Run Loop退出返回的,即使CFRunLoopRunInMode的第三个参数是YES也不行
    };
    

    注意:Run Loop是可以嵌套调用的(就像NSAutoreleasePool),例如一个Run Loop运行过程中一个事件触发后,那么在触发方法里可以再运行当前子线程的Run Loop,然后由这个Run Loop等待其他事件触发。不过这种嵌套Run Loop调用方式我用的比较少。

    以上Run Loop运行方法参考本文最后的Sample Code自行尝试。

    Run Loop的运行模式Mode

    iOS下Run Loop的主要运行模式mode有:

    1) NSDefaultRunLoopMode: 默认的运行模式,除了NSConnection对象的事件。

    2) NSRunLoopCommonModes: 是一组常用的模式集合,将一个input source关联到这个模式集合上,等于将input source关联到这个模式集合中的所有模式上。在iOS系统中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode,我有个timer要关联到这些模式上,一个个注册很麻烦,我可以用CFRunLoopAddCommonMode([[NSRunLoop currentRunLoop] getCFRunLoop],(__bridge CFStringRef) NSEventTrackingRunLoopMode)将NSEventTrackingRunLoopMode或者其他模式添加到这个NSRunLoopCommonModes模式中,然后只需要将Timer关联到NSRunLoopCommonModes,即可以实现Run Loop运行在这个模式集合中任何一个模式时,这个Timer都可以被触发。默认情况下NSRunLoopCommonModes包含了NSDefaultRunLoopMode和UITrackingRunLoopMode。注意:让Run Loop运行在NSRunLoopCommonModes下是没有意义的,因为一个时刻Run Loop只能运行在一个特定模式下,而不可能是个模式集合。

    3) UITrackingRunLoopMode: 用于跟踪触摸事件触发的模式(例如UIScrollView上下滚动),主线程当触摸事件触发时会设置为这个模式,可以用来在控件事件触发过程中设置Timer。

    4) GSEventReceiveRunLoopMode: 用于接受系统事件,属于内部的Run Loop模式。

    5) 自定义Mode:可以设置自定义的运行模式Mode,你也可以用CFRunLoopAddCommonMode添加到NSRunLoopCommonModes中。

    Run Loop运行时只能以一种固定的模式运行,只会监控这个模式下添加的Timer Source和Input Source,如果这个模式下没有相应的事件源,Run Loop的运行也会立刻返回的。注意Run Loop不能在运行在NSRunLoopCommonModes模式,因为NSRunLoopCommonModes其实是个模式集合,而不是一个具体的模式,我可以在添加事件源的时候使用NSRunLoopCommonModes,只要Run Loop运行在NSRunLoopCommonModes中任何一个模式,这个事件源都可以被触发。

    Run Loop的事件源

    归根结底,Run Loop就是个处理事件的Loop,可以添加Timer和其他Input Source等各种事件源,如果事件源没有发生时,Run Loop就可能让线程进入asleep状态,而事件源发生时就会唤醒休眠的(asleep)的子线程来处理事件。Run Loop的事件源事件源分两类:Timer Source和Input Source(包括-performSelector:***API调用簇,Port Input Source、自定义Input Source)。

    从上图可以看出Run Loop就是处理事件的一个循环,不同的是Timer Source事件处理后不会使Run Loop结束,而Input Source事件处理后会让Run Loop退出。因此你需要自己的一个Loop去不断运行Run Loop来处理事件,就像本文开头的示例那样。

    细分下Run Loop的事件源:

    1) Timer Souce就是创建Timer添加到Run Loop中,没啥好说的,Cocoa或者Core Foundation都有相应接口实现。需要注意的是scheduledTimerWith****开头生成的Timer会自动帮你以默认NSDefaultRunLoopMode模式加载到当前的Run Loop中,而其他接口生成的Timer则需要你手动使用-addTimer:forMode添加到Run Loop中。需要额外注意的是Timer的触发不会让Run Loop返回。(Timer sources deliver events to their handler routines but do not cause the run loop to exit.) 具体实验可以看下面的Sample Code。

    2) Input Source中的-performSelector:***API调用簇方法,有以下这些接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    performSelectorOnMainThread:withObject:waitUntilDone:
    performSelectorOnMainThread:withObject:waitUntilDone:modes:
    
    performSelector:onThread:withObject:waitUntilDone:
    performSelector:onThread:withObject:waitUntilDone:modes:
    
    performSelector:withObject:afterDelay:
    performSelector:withObject:afterDelay:inModes:
    
    cancelPreviousPerformRequestsWithTarget:
    cancelPreviousPerformRequestsWithTarget:selector:object:
    

    这些API最后两个是取消当前线程中调用,其他API是在主线程或者当前线程下的Run Loop中执行指定的@selector。

    3) Port Input Source:概念上也比较简单,可以用NSMachPort作为线程之间的通讯通道。例如在主线程创建子线程时传入一个NSPort对象,这样主线程就可以和这个子线程通讯啦,如果要实现双向通讯,那么子线程也需要回传给主线程一个NSPort。

    NSPort的子类除了NSMachPort,还可以使用NSMessagePort或者Core Foundation中的CFMessagePortRef。

    注意:虽然有这么棒的方式实现线程间通讯方式,但是估计是由于危及iOS的Sandbox沙盒环境,所以这些API都是私有接口,如果你用到NSPortMessage,XCode会提示'NSPortMessage' for instance message is a forward declaration

    4) 自定义Input Source:

    向Run Loop添加自定义Input Source只能使用Core Foundation的接口:CFRunLoopSourceCreate创建一个source,CFRunLoopAddSource向Run Loop中添加source,CFRunLoopRemoveSource从Run Loop中删除source,CFRunLoopSourceSignal通知source,CFRunLoopWakeUp唤醒Run Loop。

    Apple官方文档提供了一个自定义Input Source使用模式。

    主线程持有包含子线程的Run Loop和Source的context对象,还有一个用于保存需要运行操作的数据buffer。主线程需要子线程干活时,首先将需要的操作数据添加到数据buffer,然后通知source,唤醒子线程Run Loop(因为子线程可能正在sleep状态,CFRunLoopWakeUp唤醒Run Loop可以通知线程醒来干活),由于子线程也持有这个source和数据buffer,因此在触发唤醒时可以使用这个数据buffer的数据来执行相关操作(需要注意数据buffer访问时的同步)。

    具体实现参见本文最后的Sample Code。

    Run Loop的Observer

    Core Foundation层的接口可以定义一个Run Loop的观察者在Run Loop进入以下某个状态时得到通知:

    • Run loop的进入
    • Run loop处理一个Timer的时刻
    • Run loop处理一个Input Source的时刻
    • Run loop进入睡眠的时刻
    • Run loop被唤醒的时刻,但在唤醒它的事件被处理之前
    • Run loop的终止

    Observer的创建以及添加到Run Loop中需要使用Core Foundation的接口:

    1
    2
    3
    4
    5
    6
    7
    CFRunLoopObserverContext  context = {0, (__bridge void *)(self), NULL, NULL, NULL};
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeTimers, YES, 0, &myRunLoopObserver, &context);
    if (observer)
    {
      CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer,
                                     kCFRunLoopCommonModes);
    }
    

    首先创建Observer的context,然后调用Core Foundation方法CFRunLoopObserverCreate创建Observer,再加入到当前线程的Run Loop中,注意CFRunLoopObserverCreate方法的第二个参数是Observer观察类型,有如下几种:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),
        kCFRunLoopBeforeTimers = (1UL << 1),
        kCFRunLoopBeforeSources = (1UL << 2),
        kCFRunLoopBeforeWaiting = (1UL << 5),
        kCFRunLoopAfterWaiting = (1UL << 6),
        kCFRunLoopExit = (1UL << 7),
        kCFRunLoopAllActivities = 0x0FFFFFFFU
    };
    

    对应Run Loop的各种事件,kCFRunLoopAllActivities比较特殊,可以观察所有事件。具体样例代码请参考Sample Code。

    总结

    Run Loop就是一个处理事件源的循环,你可以控制这个Run Loop运行多久,如果当前没有事件发生,Run Loop会让这个线程进入睡眠状态(避免再浪费CPU时间),如果有事件发生,Run Loop就处理这个事件。Run Loop处理事件和发送给Observer通知的流程如下:

    • 1) 进入Run Loop运行,此时会通知观察者进入Run Loop;
    • 2) 如果有Timer即将触发时,通知观察者;
    • 3) 如果有非Port的Input Sourc即将e触发时,通知观察者;
    • 4)触发非Port的Input Source事件源;
    • 5)如果基于Port的Input Source事件源即将触发时,立即处理该事件,跳转到步骤9;
    • 6)通知观察者当前线程将进入休眠状态;
    • 7)将线程进入休眠状态直到有以下事件发生:基于Port的Input Source被触发、Timer被触发、Run Loop运行时间到了过期时间、Run Loop被唤醒。
    • 8) 通知观察者线程将要被唤醒。
    • 9) 处理被触发的事件:
      • 如果是用户自定义的Timer,处理Timer事件后重新启动Run Loop进入步骤2;
      • 如果线程被唤醒又没有到过期时间,则进入步骤2;
      • 如果是其他Input Source事件源有事件发生,直接处理这个事件;
    • 10)到达此步骤说明Run Loop运行时间到期,或者是非Timer的Input Source事件被处理后,Run Loop将要退出,退出前通知观察者线程已退出。

    什么时候需要用到Run Loop?官方文档的建议是:

    • 需要使用Port或者自定义Input Source与其他线程进行通讯。
    • 需要在线程中使用Timer。
    • 需要在线程上使用performSelector*****方法。
    • 需要让线程执行周期性的工作。

    我个人在开发中遇到的需要使用Run Loop的情况有:

    • 使用自定义Input Source和其他线程通信
    • 子线程中使用了定时器
    • 使用任何performSelector*****到子线程中运行方法
    • 使用子线程去执行周期性任务
    • NSURLConnection在子线程中发起异步请求

    Sample Code

    RunLoop刚开始用确实坑很多,理解概念最好的方式还是动手写代码,写了个例子放在GitHub上(工程NSThreadExample),欢迎大家讨论。

    Apple官方也有一个基于Run Loop的异步网络请求示例程序SimpleURLConnections

    参照:http://www.hrchen.com

  • 相关阅读:
    heat模板
    Leetcode812.Largest Triangle Area最大三角形面积
    Leetcode812.Largest Triangle Area最大三角形面积
    Leetcode811.Subdomain Visit Count子域名访问计数
    Leetcode811.Subdomain Visit Count子域名访问计数
    Leetcode806.Number of Lines To Write String写字符串需要的行数
    Leetcode806.Number of Lines To Write String写字符串需要的行数
    Leetcode819.Most Common Word最常见的单词
    Leetcode819.Most Common Word最常见的单词
    Leetcode783.Minimum Distance Between BST Nodes二叉搜索树结点最小距离
  • 原文地址:https://www.cnblogs.com/yulang314/p/3751481.html
Copyright © 2011-2022 走看看