iOS 从实际出发理解多线程

前言ios


    

      多线程不少开发者多多少少相信也都有了解,之前有些东西理解的不是很透,慢慢的积累以后,这方面的东西也须要本身好好的总结一下。多线程从我刚接触到iOS的时候就知道这玩意挺重要的,但那时也是能力有限,没办法很好的理解它,要是只是查它的概念性的东西,网上一搜一大把,咱们再那样去总结就显得意义不大了。这篇文章从我刚开始构思着去写的时候,就但愿本身能换个角度去写,想从实际问题出发总结多线程,那就从第三方以及本身看到的一些例子还有前段时间读的多线程和内存管理的书中分析理解总结一下多线程。数据库

 

这几个概念很容易绕晕macos


 

       进程:进程就是线程的容器,你打开一个App就是打开了一个进程,QQ有QQ的进程,微信有微信的进程,一个进程能够包含多个线程,要是把进程比喻成一条高速公路,线程就是高速路上的一条条车道,也正是由于有了这些车道,整个交通的运行效率变得更高,也正是由于有了多线程的出现,整个系统运行效率变得更高。编程

      二 线程:线程就是在进程中我么开辟的一条条为咱们作事的进程实体,总结的通俗一点,线程就是咱们在进程上开辟的一条条作咱们想作的事的通道。 一条线程在一个时间点上只能作一件“事”,多线程在同一时间点上,就能作多件“事”,这个理解,仍是咱们前面说的高速路的例子。微信

      一条高速路是一个进程, 一条条车道就是不一样的线程,在过收费站的时候,这条进程上要是只有一条线程,也就是一条高速路上只有一个车道,那你就只能排队一辆一辆的经过,同一时间不可能有两辆车一块儿过去,但要是你一个进程上有多个线程,也就是高速路上有几个车道,也就有多个窗口收费,这样的话同一时间就彻底有可能两辆车一块儿交完费经过了,这样说相信也能理解这个进程和线程的关系了。多线程

  • 同步线程:同步线程会阻塞当前的线程去执行同步线程里面想作的“事”(任务),执行完以后才会返回当前线程。       
  • 异步线程:异步线程不会阻塞当前的线程去执行异步线程里面想作的“事”,由于是异步,因此它会从新开启一个线程去作想作的“事”。 

      三 队列:队列就是用来管理下面说的“任务”的,它采用的是先进先出(FIFO)的原则,它衍生出来的就是下面的它的分类并行和串行队列,一条线程上能够有多个队列。并发

  • 并行队列:这个队列里面的任务是能够并发(同时)执行的,因为咱们知道,同步执行任务不会开启新的线程,因此并行队列同步执行任务任务只会在一条线程里面同步执行这些任务,又因为同步执行也就是在当前线程中作事,这个时候就须要一件一件的让“事”(任务)作完在接着作下一个。但要是是并发队列异步执行,就对应着开启异步线程执行要作的“事”(任务),就会同一时间又许多的“事”被作着。
  • 串行队列:这个队列里面的任务是串行也就是一件一件作的,串行同步会一件一件的等事作完再接着作下一件,要是异步的就会开启一条新的线程串行的执行咱们的任务。

    四 任务:任务按照本身通俗一点的理解,就是提到的“事”这个概念,这个“事”就能够理解为任务,那这个“事”也确定是在线程上面执行的(不论是在当前线程仍是你另开启的线程)。这个“事”你能够选择同步或者而是异步执行,这就衍生出了东西也就契合线程上面的同步线程和异步线程。app

  • 同步任务:不须要开启新的线程,在当前线程执行就能够。
  • 异步任务:你须要开辟一条新的线程去异步的执行这个任务。     

      iOS当中还有一个特殊的串行队列-- 主队列, 这个主队列中运行着一条特殊的线程 -- 主线程异步

      主线程又叫UI线程,UI线程顾名思义主要的任务及时处理UI,也只有主线程有处理UI的能力,其余的耗时间的操做咱们就放在子线程(也就是开辟线程)去执行,开线程也会占据必定的内存的,因此不要同时开启不少的线程。 async

      经过上面的内容解释了多线程里面几个关键的概念的东西,要是有不理解的地方欢迎多交流,下面再给出队列执行时候的一个运行的表格,咱们一个一个慢慢的解释。

     

 

NSThread


 

      其实在咱们平常的开发中NSThread使用也是挺多的,具体关于它的一些咱们须要注意的地方咱们一步步的开始说,先看看它的初始化的几个方法

/*
 初始化NSThread的类方法,具体的任务在Block中执行
 + (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
 
 利用selector方法初始化NSThread,target指selector方法从属于的对象  selector方法也是指定的target对象的方法
 + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
 
 初始化NSThread的方法,这两个方法和上面两个方法的区别就是这两个你能获取到NSThread的对象
 具体的参数和前面解释的参数意义都是同样的
 切记一点:  下面两个方法初始化的NSThread你须要手动start开启线程
 - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);
 
 - (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
 */

 

      除了上面四个咱们提出的方法,咱们在初始化这个问题上还须要注意的还有一点,就是 NSObject (NSThreadPerformAdditions) ,为咱们的NSObject添加的这个类别,它里面的具体的一些方法咱们也是很经常使用的:

/*
 这个方法你执行的aSelector就是在MainThread执行的,也就是在主线程
 注意这里的waitUntilDone这个后面的BOOL类型的参数,这个参数表示是否等待一直到aSelector这个方法执行结束
 modes是RunLoop的运行的类型这个RunLoop我也会好好在总结后面
 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
 
 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
   // equivalent to the first method with kCFRunLoopCommonModes
 
 上面的两个方法是直接在主线程里面运行,下面的这两个方法是要在你初始化的thr中去运行,其余的参数和上面解释的同样
 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
 
 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
   // equivalent to the first method with kCFRunLoopCommonModes
 
 - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
 */

     

      咱们在说说前面说的waitUntilDone后面的这个BOOL类型的参数,这个参数的意义有点像咱们是否同步执行aSelector这个任务!具体的看下面两张图的内容就一目了然了:

      在看看等于YES的时候结果的输出状况:

 

      关于NSThread咱们再说下面几个方法的具体的含义就不在描述了,关于NSThread有什么其余的问题,能够加我QQ交流:

/*
 
 设置线程沉睡到指定日期
 + (void)sleepUntilDate:(NSDate *)date;
 
 线程沉睡时间间隔,这个方法在设置启动页间隔的时候比较常见
 + (void)sleepForTimeInterval:(NSTimeInterval)ti;
 
 线程退出,当执行到某一个特殊状况下的时候你能够退出当前的线程,注意不要在主线程随便调用
 + (void)exit;
 
 线程的优先级
 + (double)threadPriority;
 
 设置线程的优先级
 + (BOOL)setThreadPriority:(double)p;
 
 */

 

NSOperation


 

      多线程咱们还得提一下NSOperation,它可能比咱们认识中的要强大一点,NSOperation也是有不少东西能够说的,前面的NSThread其实也是同样,这些要是仔细说的话都能写一篇文章出来,可能之后随着本身接触的愈来愈多,关于多线程这一块的东西咱们会独立的建立一个分类总结出去。

      首先得知道NSOperation是基于GCD封装的,NSOperation这个类自己咱们使用的时候不躲,更多的是集中在苹果帮咱们封装好的NSInvocationOperation和NSBlockOperation

      你command一下NSOperation进去看看,有几个点你仍是的了解一下的,主要的就是下面的几个方法:

NSOperation * operation = [[NSOperation alloc]init];
[operation start];   //开始
[operation cancel]; //取消
[operation setCompletionBlock:^{
    //operation完成以后的操做
}];

      咱们具体的说一下咱们上面说的两个类:NSInvocationOperation和NSBlockOperation,先看看NSInvocationOperation的初始化:

/*
 
 初始化方法 看过前面的文章以后它的target 、sel 、arg 等参数相信不难理解
 -(nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
 
 -(instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER;
 
 */

       补充: NS_DESIGNATED_INITIALIZER 指定初识化方法并非对使用者。而是对内部的现实,能够点击进去具体了解一下它!NSInvocationOperation实际上是同步执行的,所以单独使用的话就价值不大了,它和NSOperationQueue一块儿去使用才能实现多线程调用。这个咱们后面再具体的说

      在看看NSBlockOperation这个,它重要的方法就咱们下面的两个        

/*
 初始化方法
 + (instancetype)blockOperationWithBlock:(void (^)(void))block;
 
 添加一个能够执行的block到前面初始化获得的NSBlockOperation中
 - (void)addExecutionBlock:(void (^)(void))block;
 */

      NSBlockOperation这个咱们得提一点: 它的最大的并发具体的最大并发数和运行环境也是有关系的,具体的内容咱们能够戳戳这里同行总结以及验证的,咱们因为篇幅的缘由就不在这里累赘。

      其实只要是上面这些的话是不够咱们平常使用的,但还有一个激活他们俩的类咱们也得说说:NSOPerationQueue 下面是关于它的大概的一个说明,都挺简单,就不在特地写Demo。

      

      关于NSOperation的咱们就说这么多,下面重点说一下GCD。

  

主角GCD -- 主线程


      

      一、咱们先从主队列,主线程开始提及,经过下面的方法咱们就能够获取获得主队列:

dispatch_queue_t mainqueue = dispatch_get_main_queue(); 

      二、咱们在主线程同步执行任务,下面是操做的结果以及打印的信息:

 

      咱们解释一下为何在主线程中执行同步任务会出现这个结果,咱们一步一步的梳理一下这个执行过程:

  1. 获取到在主队列主线程中执行了最前面的打印信息,这个没什么问题
  2. 开始执行dispatch_sync这个函数,主队列是串行队列,这个函数会把这个任务插入到主队列的最后面(理解队列添加任务)
  3. 主线程执行到这里的时候就会等待插入的这个同步任务执行完以后再执行后面的操做
  4. 但因为这个同步任务是插入到主队列的最后面,最队列前面的任务没有执行完以前是不会执行这个block的(主线程在执行initMainQueue任务)
  5. 这样就形成了一个相互等待的过程,主线程在等待block完返回,block却在等待主线程执行它,这样就形成了死锁,看打印的信息你也就知道block是没有被执行的。

      这里咱们你可能会思考,主队列是一个串行队列,那咱们在主线程中添加一个串行队列,再给串行队列添加一个同步任务,这时候和前面主线程主队列添加同步任务不就场景同样了吗?那结果呢? 咱们看看下面的打印:

 

 

      咱们按照前面的方式解释一下这个的执行步骤:

  1. 主线程在执行主队列中的方法initSerialQueue,到这个方法时候建立了一个串行队列(注意不是主队列)打印了前面的第一条信息
  2. 执行到dispatch_sync函数,这个函数给这个串行队列中添加了一个同步任务,同步任务是会立马执行的
  3. 主线程就直接操做执行了这个队列中的同步任务,打印的第二条信息
  4. 主线程接着执行下面的第三条打印信息

      理解:看这个执行的过程对比前面的,你就知道了不一样的地方就是前面是添加在了主队列当中,但这里有添加到主队列,因为是插入到主队列的末尾,因此须要主队列的任务都执行完才能指定到它,但主线程执行到initMainQueue这个方法的时候在等待这个方法中添加的同步任务执行完接着往下执行,但它里面的同步任务又在等待主线程执行完在执行它,就相互等待了,但主线程执行不是主队列里面的同步任务的时候是不须要主线程执行完全部操做在执行这个任务的,这个任务是它添加到串行队列的开始也是结束的任务,因为不须要等待,就不会形成死锁!

      上面这个问题常常会看到有人问,有许多解释,也但愿本身能把这个问题给说清楚了!

 

      三、主线程这里咱们再提一点,就是线程间的信息简单传递

      前面咱们有说到主线程又叫作UI线程,全部关于UI的事咱们都是在主线程里面更新的,像下载数据以及数据库的访问等这些耗时的操做咱们是建议放在子线程里面去作,那就会产生子线程处理完这些以后要回到主线程更行UI的问题上,这一点值得咱们好好的注意一下,但其实这一点也是咱们用的最多的,相信你们也都理解!

 

主角GCD  --  串行队列


 

      串行队列的概念性的东西咱们就不在这里累赘,不论是串行队列+同步任务仍是串行队列+异步任务都简单,有兴趣能够本身是这写一下,后面分析会提到他们的具体使用的,咱们在一个稍微比前面的说的复杂一点点的问题,串行队列+异步+同步,能够先试着不要往下面看先分析一下下面这段代码的执行结果是什么?

static void * DISPATCH_QUEUE_SERIAL_IDENTIFY;

-(void)initDiapatchQueue{

        dispatch_queue_t serialQueue = dispatch_queue_create(DISPATCH_QUEUE_SERIAL_IDENTIFY, DISPATCH_QUEUE_SERIAL);
        dispatch_async(serialQueue, ^{
           
                NSLog(@"一个异步任务的内容%@",[NSThread currentThread]);
                dispatch_sync(serialQueue, ^{
                   
                        NSLog(@"一个同步任务的内容%@",[NSThread currentThread]);
                });
        });
}

 

不知道你分析数来的这点代码的结果是什么,咱们这里来看看结果,而后和上面一步一步的分析一下它的整个的执行过程,就能找到答案:

 

 

      答案就是crash了,其实也是死锁,下面一步一步的走一下这整个过程,分析一下哪里死锁了:

  1. 主线程主队列中执行任务initDispatchQueue,进入了这个方法,在这个方法里面建立了一个串行队列,这一步相信你们都明白,没什么问题。
  2. 给这个串行队列添加了一个异步任务,因为是异步任务,因此会开启一条新的线程,为了方便描述,咱们把新开的这个线程记作线程A, 把这个任务记作任务A,也因为是异步任务,主线程就不会等待这个任务返回,就接着往下执行其余任务了。
  3. 接下来的分析就到了这个线程A上,这个任务A被添加到串行队列以后就开始在线程A上执行,打印出了咱们的第一条信息,也证实了不是在主线程,这个也没问题。
  4. 线程A开始执行这个任务A,进入这个任务A以后在这个任务A里面又同步在串行队列里面添加任务,记作任务B,因为任务B是dispatch_sync函数同步添加的,须要立马被执行,就等待线程A执行它
  5. 可是这个任务B是添加到串行队列的末尾的,线程A在没有执行完当前任务A是不会去执行它的,这样就形成线程A在等待当前任务A执行完,任务B又在等待线程A执行它,就造成了死锁

      通过上面的分析,你就能看到这个场景和你在主线程同步添加任务是同样的,咱们再仔细的考虑一下这整个过程,在分析一下上面主线程+串行队列+同步任务为何没有造成死锁!相互对比理解,就能把整个问题想明白。

 

主角GCD  --  并行队列


 

      下面咱们接着再说说这个并行队列,并行队列+同步执行或者并行队列+异步执行这个咱们也就没什么好说的了,在这里说说并行+异步的须要注意的地方,不知道你们有没有想过,并行的话不少任务会一块儿执行,要是异步任务的话会开启新的线程,那是否是咱们添加了十个异步任务就会开启十条线程呢?那一百个异步任务岂不是要开启一百条线程,答案确定是否认的!那系统究竟是怎么处理的,咱们也说说,下面的是高级编程书里面的解释咱们梳理一下给出结论。

  • 当为DISPATCH_QUEUE_CONCURRENT的时候,不用等待前面任务的处理结束,后面的任务也是可以直接执行的
  • 并行执行的处理数量取决于当前系统的状态,即iOS和OS X基于Dispatch Queue中的处理数、CPU核数以及CPU负荷等当前系统状态来决定DISPATCH_QUEUE_CONCURRENT中并行执行的处理数
  • iOS 和 OS X的核心 -- XNU内核决定应当使用的线程数,而且生成所需的线程执行处理
  • 当处理结束,应当执行的处理数减小时,XNU内核会结束不在须要的线程
  • 处理并行异步任务时候线程是能够循环往复使用的,好比任务1的线程执行完了任务1,线程能够接着去执行后面没有执行的任务

      这里的东西就这些,咱们在前面串行队列的时候,串行队列+异步任务嵌套同步任务会形成死锁,那咱们要是把它变成同步队列呢?结果又会是什么样子呢?咱们看看下面这段代码的执行结果:     

 

      从上面的结果能够看得出来,是没有问题的,这里咱们就不在一步一步的分析它的执行过程了,就说说为何并行的队列就没有问题,可是串行的队列就会出问题:

      并行队列添加了异步任务也是建立了一个新的线程,而后再在这个任务里面给并行队列添加一个同步任务,因为是并行队列 ,执行这个同步任务是不须要前面的异步任务执行完了,就直接开始执行,因此也就有了下面的打印信息,经过上面几个问题,相信理解了以后,对于串行队列或者并行队列添加同步任务或者异步任务都有了一个比较深的理解了,咱们再接着往下总结。

 

GCD不只仅这些 


 

    关于GCD的内容还有下面这些都是值得咱们关注的,下面咱们开始一一说一说:

  • dispatch_barrier_async

        dispatch_barrier_async 函数是咱们俗称的栅栏方法,“栅栏”的意思理解一下字面的,就是把外面和里面阻隔开,这个函数的做用就是这样,把插入的这个栅栏以前和以后的阻隔开,等前面的执行完了就执行“栅栏函数”插入的任务,等栅栏的任务执行结束了就开始执行栅栏后面的任务。看下面一个简单的Demo就理解了。

      从上面就能够看到,咱们把0插入到第三个任务的位置,它是等前面的两个任务执行完了,在去执行第三个,要是你以为这里前两个任务简单,执行不须要太多的时间的话,你能够试着把前面两个任务的“任务量”设置大一点,这样有助于你更好的理解这个“栅栏”操做!

  • dispatch_after

        dispatch_after 延时操做

        若是某一条任务你想等多少时间以后再执行的话,你就彻底可使用这个函数处理,写法很简单,由于已经帮咱们封装好了,看下面这两行代码:

        // DISPATCH_TIME_NOW 当前时间开始
        // NSEC_PER_SEC 表示时间的宏,这个能够本身上网搜索理解
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                
                NSLog(@"延迟了10秒执行");
        });
  • dispatch_apply

       dispatch_apply 相似一个for循环,会在指定的dispatch queue中运行block任务n次,若是队列是并发队列,则会并发执行block任务,dispatch_apply是一个同步调用,block任务执行n次后才返回。 因为它是同步的,要是咱们下面这样写就会有出问题:

      能够看到出问题了,但咱们要是把它放在串行队列或者并行队列就会是下面这样的状况

     

  • dispatch_group_t

        dispatch_group_t的做用咱们先说说,在追加到Dispatch Queue 中的多个任务所有结束以后想要执行结束的处理,这种状况也会常常的出现,在只使用一个Serial Dispatch Queue时,只要将想执行的操做所有追加该Serial Dispatch Queue中而且追加在结束处理就能够实现,可是在使用 Concurrent Dispatch Queue 时或者同时使用多个 Dispatch Queue时候,就比较的复杂了,在这样的状况下 Dispatch Group 就能够发挥它的做用了。看看下面的这段代码:

-(void)testDispatch_group_t{

        dispatch_group_t group_t = dispatch_group_create();
        dispatch_queue_t queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_group_async(group_t, queue_t, ^{
                
                NSLog(@"1--当前的线程%@",[NSThread currentThread]);
        });
        dispatch_group_async(group_t, queue_t, ^{
                
                NSLog(@"2--当前的线程%@",[NSThread currentThread]);
        });
        dispatch_group_async(group_t, queue_t, ^{
                
                NSLog(@"3--当前的线程%@",[NSThread currentThread]);
        });
        dispatch_group_async(group_t, queue_t, ^{
                
                for (int i = 1; i<10; i++) {
                        
                     NSLog(@"4--当前的线程%@",[NSThread currentThread]);
                }
        });
        // 当前的全部的任务都执行结束
        dispatch_group_notify(group_t, queue_t, ^{
           
                NSLog(@"前面的全都执行结束了%@",[NSThread currentThread]);
        });
}

      这段代码的意图很明显,看了下面的打印信息这个你也就理解它了:

      总结: 关于多线程的最基本的问题暂时先总结这么多,还有许多的问题,本身也在总结当中,好比如下线程锁等等的问题,等总结到差很少的时候再分享!

相关文章
相关标签/搜索