• ios高级开发之多线程(三)GCD技术


    GCD是基于C的API,它是libdispatch的的市场名称。而libdispatch作为Apple公司的一个库,为并发代码在多核硬件(跑IOS或者OS X)上执行提供有力支持。

    那么我们为什么要用GCD技术呢?

    1.GCD能够推迟昂贵的计算任务,并在后台运行它们来改善你的应用的性能。

    2.GCD提供一个易于使用的并发模型而不仅仅是锁和线程。以帮助我们避开并发陷阱。

    3.GCD具有在常见模式(比如单例)上用更高性能的原语优化你的代码的潜在能力。

    4.GCD旨在替换NSTread等线程技术。

    5.GCD可充分利用设备的多核。

    6.GCD可自动管理线程的生命周期。

    说了这些GCD的优点,那么在实际开发中,如何使用GCD来更好满足我们的需求呢?

    一、Synchronous&Asynchronous 同步&异步

    1.Synchronous同步:同步任务的执行的方式:在当前线程中执行,必须等待当前语句执行完毕,才会执行下一条语句。

    来看下同步的代码:

    //同步的打印顺序
    -(void)syncTask{
        NSLog(@"begin");
        //GCD的同步方法
        dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            //任务中要执行的代码
            [NSThread sleepForTimeInterval:2.0];
            NSLog(@"%@",[NSThread currentThread]);
        });
        NSLog(@"end");
    }

    来看打印出来的:

    2019-03-29 12:00:54.993042+0800 wftest[5191:88411] begin

    2019-03-29 12:00:56.994525+0800 wftest[5191:88411] <NSThread: 0x600000ff5380>{number = 1, name = main}

    2019-03-29 12:00:56.994799+0800 wftest[5191:88411] end

     

    可以看到,即使线程休眠了2秒,他依然会按照顺序执行,等代码块内的代码执行完毕后,才会执行end.

     

    接着我们再来看异步,不在当前线程中执行,不用等当前语句执行完毕,就可以执行下一条语句

    来看代码:

    //异步顺序
    -(void)asyncTask{
        //异步不会在当前线程执行,首先需要开辟新的线程,而开辟新的线程也需要一定的时间
        NSLog(@"begin");
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%@",[NSThread currentThread]);
        });
        NSLog(@"end");
    }
    

      我们来看下打印的情况:

    2019-03-29 16:06:34.600530+0800 wftest[1336:19777] begin

    2019-03-29 16:06:34.600763+0800 wftest[1336:19777] end

    2019-03-29 16:06:34.600892+0800 wftest[1336:20090] <NSThread: 0x600000acc600>{number = 3, name = (null)}

     

    可以看到,打印出来begin后,直接打印出了end.然后才执行了异步块里的代码。

     

    接下来,我们来看看串行队列Serial queues,和并行队列(并发队列)Concurrent queues.

    1.串行队列的特点:

    以先进先出的方式执行,顺序调度队列中的任务执行。

    无论队列中的任务函数是同步还是异步,都会等待前一个任务执行完成后,再调度后面的任务。

    我们先来看看,串行队列的同步代码:

    //串行队列同步函数(在一个线程中执行,注意,GCD的是C语言API,不要和OC弄混)
    -(void)serialSync{
        //这里有两个参数,第一个参数的标识符,一般为公司域名倒写,第二个参数队列类型,DISPATCH_QUEUE_SERIAL串行,DISPATCH_QUEUE_CONCURRENT为并发队列
        dispatch_queue_t  serialQueue = dispatch_queue_create("com.feng", DISPATCH_QUEUE_SERIAL);
        //创建任务
        void (^task1) (void) = ^(){
            NSLog(@"task1---%@",[NSThread currentThread]);
        };
        
        void (^task2) (void) = ^(){
            NSLog(@"task2 --  %@",[NSThread currentThread]);
        };
        
        void (^task3) (void) = ^(){
            NSLog(@"task3  -- %@",[NSThread currentThread]);
        };
        
        //添加任务到队列,同步执行方法
        dispatch_sync(serialQueue, task1);
        dispatch_sync(serialQueue, task2);
        dispatch_sync(serialQueue, task3);
    }
    

      然后来看下NSLog打印的东西:

    2019-03-29 17:08:10.362074+0800 wftest[2989:50297] task1---<NSThread: 0x600002009340>{number = 1, name = main}

    2019-03-29 17:08:10.364550+0800 wftest[2989:50297] task2 --  <NSThread: 0x600002009340>{number = 1, name = main}

    2019-03-29 17:08:10.365860+0800 wftest[2989:50297] task3  -- <NSThread: 0x600002009340>{number = 1, name = main}

    可以看到task1,taks2,task3是完全按照顺序执行的。

    再来看串行队列的异步方法:

    先来看代码

     

    //串行队列异步函数
    -(void)serialAsync{
        //创建一个串行队列
        dispatch_queue_t serialQuene = dispatch_queue_create("com.feng", DISPATCH_QUEUE_SERIAL);
        //2.创建任务
        void (^task1)(void) = ^(){
            NSLog(@"task1 --- %@",[NSThread currentThread]);
        };
        void (^task2)(void) = ^(){
            NSLog(@"task 2-- %@",[NSThread currentThread]);
        };
        void (^task3)(void) = ^(){
            NSLog(@"task 3 -- %@",[NSThread currentThread]);
        };
        
        //3.添加任务队列
        dispatch_async(serialQuene, task1);
        dispatch_async(serialQuene, task2);
        dispatch_async(serialQuene, task3);
    }
    

     

     来看下打印结果:

    2019-03-30 14:27:21.730761+0800 wftest[4929:84017] 主线程 -- <NSThread: 0x600000650540>{number = 1, name = main}
    2019-03-30 14:27:21.731252+0800 wftest[4929:84090] task1 --- <NSThread: 0x600000638340>{number = 3, name = (null)}
    2019-03-30 14:27:21.731536+0800 wftest[4929:84090] task 2-- <NSThread: 0x600000638340>{number = 3, name = (null)}
    2019-03-30 14:27:21.731691+0800 wftest[4929:84090] task 3 -- <NSThread: 0x600000638340>{number = 3, name = (null)}
    

      

     

     

    可以看到,串行队列异步执行,仍然按顺序执行的。也就是说,只要是串行队列,无论是异步,还是同步函数,都是按顺序执行的。

     

     

    2.看完串行队列,我们来看并发(并行)队列。

    并发队列的特点:

    1.以先进先出的方法,并发调度队列中的任务的执行。

    2.如果是并发队列的同步执行,就会等先被调度的任务执行完毕后,再执行下一个任务。

    3.如果是并发队列的异步执行,同时底层线程池有可用的线程资源,会在新的任务调度后,调度下一个任务。

    也就是说,先加进来的任务会先被执行,但不用等他执行完毕,就可以接着调度下一个任务。

    那么,我们先来看并发队列的同步任务的代码:

    //并发队列同步函数
    -(void)concurrentSync{
        //1.创建并发队列
        dispatch_queue_t concurrentQueue = dispatch_queue_create("com.feng", DISPATCH_QUEUE_CONCURRENT);
        
        //2.创建任务
        void (^task1) (void) = ^(){
            NSLog(@"task1 -- %@",[NSThread currentThread]);
        };
        void (^task2) (void) = ^(){
            NSLog(@"task2 -- %@",[NSThread currentThread]);
        };
        void (^task3) (void) = ^(){
            NSLog(@"task3 -- %@",[NSThread currentThread]);
        };
        
        //3.添加同步任务到并发队列
        dispatch_sync(concurrentQueue, task1);
        dispatch_sync(concurrentQueue, task2);
        dispatch_sync(concurrentQueue, task3);
    }
    

      再来看打印情况:

    2019-03-30 14:01:51.358796+0800 wftest[4073:69627] 主线程 -- <NSThread: 0x600001bdcd00>{number = 1, name = main}
    2019-03-30 14:01:51.359220+0800 wftest[4073:69627] task1 -- <NSThread: 0x600001bdcd00>{number = 1, name = main}
    2019-03-30 14:01:51.359668+0800 wftest[4073:69627] task2 -- <NSThread: 0x600001bdcd00>{number = 1, name = main}
    2019-03-30 14:01:51.360195+0800 wftest[4073:69627] task3 -- <NSThread: 0x600001bdcd00>{number = 1, name = main}
    

      

      

      

     

    可以看到,虽然是并发队列,但因为是同步任务,所以也是按顺序执行的。因为是同步任务,所以就在当前线程,主线程中执行。异步任务则会在子线程中执行。

    (可以简单总结:串行 ,要等待上个任务执行完毕,才执行下个任务,所以会在同一个线程中执行。 并行:不用等上个任务执行完毕,就可以执行下个任务。同步:在当前线程中执行,不会开辟子线程。异步:在子线程中执行(这是指串行和并行队列。后面说的主队列异步,也是在主线程中执行))。

     

    再来看并发队列的异步执行任务:

    //并发队列的异步执行
    -(void)concurrentAsyn{
        //1.创建队列
        dispatch_queue_t concurrentQueue = dispatch_queue_create("com.feng", DISPATCH_QUEUE_CONCURRENT);
        //2.创建任务
        void(^task1) (void) = ^(){
            NSLog(@"task1 --   %@",[NSThread currentThread]);
        };
        void(^task2) (void) = ^(){
            NSLog(@"task2 -- %@",[NSThread currentThread]);
        };
        void(^task3) (void) = ^(){
            NSLog(@"task3 -- %@",[NSThread currentThread]);
        };
        //3.把任务添加到队列中去
        dispatch_async(concurrentQueue, task1);
        dispatch_async(concurrentQueue, task2);
        dispatch_async(concurrentQueue, task3);
    }
    

      来看打印情况:

    2019-03-30 14:02:45.003384+0800 wftest[4112:70392] 主线程 -- <NSThread: 0x600000ba8c40>{number = 1, name = main}
    2019-03-30 14:02:45.005834+0800 wftest[4112:70450] task2 -- <NSThread: 0x600000bf7840>{number = 4, name = (null)}
    2019-03-30 14:02:45.005834+0800 wftest[4112:70449] task1 --   <NSThread: 0x600000bcc380>{number = 3, name = (null)}
    2019-03-30 14:02:45.005838+0800 wftest[4112:70454] task3 -- <NSThread: 0x600000bcc480>{number = 5, name = (null)}
    

      可以看到,异步任务另外开辟了子线程。可以看到打印顺序发生了变化。

    接着,我们来看全局队列。

    全局队列的工作表现和并发队列一致。

    但是全局队列是否就是并发队列呢?不是的。我们来看下他们的区别:

    1.全局队列没有名称,无论是MRC&ARC都不用考虑释放,所以在日常开发中,建议使用全局队列。

    2.并发队列:有名字,和NSThread的name属性作用类似,如果你在MRC的开发中,则需要使用dispatch_releas(q)来释放对应的对象。

    那么并发队列在什么时候使用呢?在你开发第三方的框架的时候,则需要使用并发队列了。这样可以避开和使用你的开发框架的程序员弄混队列。

    咱们先来看下全局队列的同步任务。(日常开发中几乎用不到。)

    //全局队列的同步任务
    -(void)globalSync{
        NSLog(@"begin");
        //1.创建全局队列
        dispatch_queue_t gloabalQueue = dispatch_get_global_queue(0, 0);
        //2.创建任务
        void(^task1) (void) = ^(){
            NSLog(@"task1 --   %@",[NSThread currentThread]);
        };
        void(^task2) (void) = ^(){
            NSLog(@"task2 -- %@",[NSThread currentThread]);
        };
        void(^task3) (void) = ^(){
            NSLog(@"task3 -- %@",[NSThread currentThread]);
        };
        //3.加入任务到队列中执行
        dispatch_sync(gloabalQueue, task1);
        dispatch_sync(gloabalQueue, task2);
        dispatch_sync(gloabalQueue, task3);
        NSLog(@"end");
    }
    

      来看打印结果:

    2019-03-30 14:35:08.160967+0800 wftest[5189:88230] 主线程 -- <NSThread: 0x600000684300>{number = 1, name = main}
    2019-03-30 14:35:08.161223+0800 wftest[5189:88230] begin
    2019-03-30 14:35:08.161470+0800 wftest[5189:88230] task1 --   <NSThread: 0x600000684300>{number = 1, name = main}
    2019-03-30 14:35:08.161639+0800 wftest[5189:88230] task2 -- <NSThread: 0x600000684300>{number = 1, name = main}
    2019-03-30 14:35:08.161773+0800 wftest[5189:88230] task3 -- <NSThread: 0x600000684300>{number = 1, name = main}
    2019-03-30 14:35:08.161891+0800 wftest[5189:88230] end
    

      可以看到,按熟悉执行,同步的,都是当前线程中执行的。

    再来看全局队列的异步任务,他是在子线程池上执行的,每个任务都有一个自己的线程,前提是线程池里有线程资源,底层有一个线程重用机制的。看下代码:

    2019-03-30 14:39:06.429812+0800 wftest[5330:90708] 主线程 -- <NSThread: 0x600003606580>{number = 1, name = main}
    2019-03-30 14:39:06.430060+0800 wftest[5330:90708] begin
    2019-03-30 14:39:06.430228+0800 wftest[5330:90708] end
    2019-03-30 14:39:06.430381+0800 wftest[5330:90762] task1 --   <NSThread: 0x600003660a40>{number = 3, name = (null)}
    2019-03-30 14:39:06.430416+0800 wftest[5330:90763] task3 -- <NSThread: 0x600003660a00>{number = 4, name = (null)}
    2019-03-30 14:39:06.430422+0800 wftest[5330:90760] task2 -- <NSThread: 0x600003660ec0>{number = 5, name = (null)}
    

      可以看到,每个任务都有自己的独立的线程。

    有点累,一会再来看看主队列。

    主队列的特点:

    1.专门用来在主线程上调度任务的队列。

    2.不会开启子线程

    3.以先进先出的方式,在主线程空闲的时候才会调度主队列中的任务在主线程中执行。

    4.如果当前主线程中有任务在执行,那么无论主队列中添加了什么任务,都不会被调度。

    主队列是负责在主线程中调度任务的。

    会随着程序启动一起创建。

    对于我们程序员来说,主队列只需要获取,不需要创建。

    那么我们来看下主队列的异步任务的代码:

    //主队列的异步任务
    -(void)mainAsync{
        NSLog(@"begin");
        
        //1.创建主队列
        dispatch_queue_main_t mainAsync = dispatch_get_main_queue();
        
        //2.创建任务
        void(^task1) (void) = ^(){
            NSLog(@"task1 --   %@",[NSThread currentThread]);
        };
        void(^task2) (void) = ^(){
            NSLog(@"task2 -- %@",[NSThread currentThread]);
        };
        void(^task3) (void) = ^(){
            NSLog(@"task3 -- %@",[NSThread currentThread]);
        };
        //添加任务到队列
        dispatch_async(mainAsync, task1);
        dispatch_async(mainAsync, task2);
        dispatch_async(mainAsync, task3);
        NSLog(@"end");
    }
    

      来看看打印情况:

    2019-04-01 16:23:52.002922+0800 wftest[2320:37104] 主线程 -- <NSThread: 0x600001676a80>{number = 1, name = main}
    2019-04-01 16:23:52.003178+0800 wftest[2320:37104] begin
    2019-04-01 16:23:52.003342+0800 wftest[2320:37104] end
    2019-04-01 16:23:52.092118+0800 wftest[2320:37104] task1 --   <NSThread: 0x600001676a80>{number = 1, name = main}
    2019-04-01 16:23:52.092324+0800 wftest[2320:37104] task2 -- <NSThread: 0x600001676a80>{number = 1, name = main}
    2019-04-01 16:23:52.092497+0800 wftest[2320:37104] task3 -- <NSThread: 0x600001676a80>{number = 1, name = main}
    

      大家可以注意到这里的几个情况:

    1.虽然是异步的,但是三个任务仍然按顺序调度执行。

    2.先执行了begin,紧接着执行的了end.然后才执行了三个任务,也就是说,主线程有空闲的时候才执行这三个任务。

    我们说下deadlock死锁:是两个或者更多的线程之间出现的情况:比如第一个线程在等待第二个线程的完成才能继续执行,而第二个线程在等待第一个线程的完成才能继续执行。

    看起来似乎异步更有用,效率更高,那么同步有什么用呢,我们说下同步的作用。

    1.首先,同步肯定是保证了任务执行的顺序。

    2.可以让后面的异步任务要依赖于某一个同步的任务。比如,必须让用户登录之后,才允许他下载电影。

    我们看下代码:

    //同步+异步
    -(void)loadMovies{
        dispatch_async(dispatch_get_global_queue(0, 0), ^{//开辟一条子线程
            NSLog(@"开辟了子线程----%@",[NSThread currentThread]);
            dispatch_sync(dispatch_get_global_queue(0, 0), ^{
                //登录,在当前的线程执行
                NSLog(@"登录了---%@", [NSThread currentThread]);
                sleep(3);
            });
            
            //2.同时下载3部电影
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSLog(@"正在下载第一部电影---%@",[NSThread currentThread]);
            });
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSLog(@"正在下载第二部电影---%@",[NSThread currentThread]);
            });
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSLog(@"正在下载第三部电影---%@",[NSThread currentThread]);
            });
            
            dispatch_sync(dispatch_get_main_queue(), ^{
                [NSThread sleepForTimeInterval:1.0];
                NSLog(@"计算机将在三秒后关闭 --%@",[NSThread currentThread]);
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    NSLog(@"关机了---%@", [NSThread currentThread]);
               });
            });
        });
    }
    

      然后我们来看打印log:

    2019-04-01 17:52:46.491858+0800 wftest[5359:81060] 开辟了子线程----<NSThread: 0x600002ff8580>{number = 3, name = (null)}
    2019-04-01 17:52:46.492263+0800 wftest[5359:81060] 登录了---<NSThread: 0x600002ff8580>{number = 3, name = (null)}
    2019-04-01 17:52:49.497610+0800 wftest[5359:81063] 正在下载第一部电影---<NSThread: 0x600002ffaa00>{number = 4, name = (null)}
    2019-04-01 17:52:49.497634+0800 wftest[5359:81062] 正在下载第三部电影---<NSThread: 0x600002ffefc0>{number = 6, name = (null)}
    2019-04-01 17:52:49.497654+0800 wftest[5359:81061] 正在下载第二部电影---<NSThread: 0x600002ffef40>{number = 5, name = (null)}
    2019-04-01 17:52:50.498825+0800 wftest[5359:80998] 计算机将在三秒后关闭 --<NSThread: 0x600002f9d600>{number = 1, name = main}
    2019-04-01 17:52:53.752223+0800 wftest[5359:80998] 关机了---<NSThread: 0x600002f9d600>{number = 1, name = main}
    

      我们注意到这几个方面:虽然下载电影的时候,又开启了三个新的线程,但是他们仍然要等待登录后,才能执行,以及最后,计算机回到主队列去关闭计算机的时候,也是等电影下载完毕。这是因为主队列这里的也是同步任务。前面也是同步任务。

    接下来我们来看下dispatch_time的延迟操作

    什么时候使用dispatch_after呢?

    1.最好坚持在主队列上使用dispatch_after。而不是在自定义串行队列上,并发队列也尽量不要使用。

    2.主队列(串行)是使用dispatch_after的最好选择。xcode也提供了自动完成模板。

    我们来看下代码:

    //延迟执行
    -(void)delay{
        dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
        void(^task)(void)=^(){
            NSLog(@"%@",[NSThread currentThread]);
        };
        //主队列
        dispatch_after(when, dispatch_get_main_queue(), task);
         NSLog(@"come here");
    }
    

      来看打印:

    2019-04-02 10:55:47.020688+0800 wftest[2069:28686] come here
    2019-04-02 10:55:49.216037+0800 wftest[2069:28686] <NSThread: 0x6000018e1c40>{number = 1, name = main}
    

    IOS提供的一些方便使用的延迟:

    //延迟执行
    -(void)after{
        [self.view performSelector:@selector(setBackgroundColor:) withObject:[UIColor orangeColor] afterDelay:1.0];
    }
    

      再来看看线程安全:

    线程安全是多线程不可避免的问题。

    dispatch_once以线程安全的方式执行,仅且执行代码一次。她会给代码设立一个临界区。试图访问临界区(即要传递给dispatch_onece的代码)的不同线程,在临界区已经有一个线程在执行的情况下会被阻塞,直到临界区完成为止。

    我们来看下使用dispatch_once来实现单例线程安全:

    //使用dispatch_once实现线程安全的单例
    +(instancetype)sharedSingleton{
        static id instance;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            instance = [[self alloc] init];
        });
        return instance;
    }
    

     如果一个单例中的单例属性是一个可变对象。那么就要考虑线程安全问题了。比如NSMutableArray。可能会出现一个线程正在读取,另外一个线程正在修改。这样就会出现线程不安全的情况。在GCD中可以通过dispatch_barrier_async来进行创建读写锁,这样一个解决方案。

    接下来,我们来看下调度组(dispatch_group):

    调度组的实现原理:类似引用计数,进行+1,-1;

    应用场景:

    比如当你开启了下载任务,当下载三个任务,只有等这三个任务全部下载完毕后,才能下一步做事情。这个时候就可以用到调度组,这个调度组,就能监听它里面的任务是否执行完毕:

    //调度组
    -(void)groupDispatch{
        //1.创建调度组
        dispatch_group_t group = dispatch_group_create();
        //2.获取全局队列
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        //3.创建三个下载任务
        void(^task1)(void) = ^(){
            NSLog(@"%@----下载片头",[NSThread currentThread]);
        };
        
        dispatch_group_enter(group);//引用计算+1
        void (^task2) (void) = ^(){
            NSLog(@"%@---下载内容",[NSThread currentThread]);
            [NSThread sleepForTimeInterval:3.0];
            NSLog(@"----下载内容完毕");
            dispatch_group_leave(group);//引用计数-1
        };
        
        dispatch_group_enter(group);//引用计数+1
        void(^task3)(void)=^(){
            NSLog(@"%@----下载片尾",[NSThread currentThread]);
            dispatch_group_leave(group);//引用计数-1
        };
        
        //4.需要将我们的队列和任务放到组内去监控
        dispatch_group_async(group, queue, task1);
        dispatch_group_async(group, queue, task2);
        dispatch_group_async(group, queue, task3);
        
        //5.监听函数
        // 参数2,表示参数3这里的代码在哪个队列中执行
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            //表示组内所有的任务都完成之后,执行这里的代码
            NSLog(@"把下载好的视频按顺序拼接好,然后显示在UI上播放%@",[NSThread currentThread]);
        });
        
    }
    

      看打印:

    2019-04-02 16:18:45.168288+0800 wftest[12368:305146] <NSThread: 0x6000023d5fc0>{number = 4, name = (null)}---下载内容
    2019-04-02 16:18:45.168288+0800 wftest[12368:305147] <NSThread: 0x6000023d5f00>{number = 3, name = (null)}----下载片头
    2019-04-02 16:18:45.168288+0800 wftest[12368:305149] <NSThread: 0x6000023e9c80>{number = 5, name = (null)}----下载片尾
    2019-04-02 16:18:48.174292+0800 wftest[12368:305146] ----下载内容完毕
    2019-04-02 16:18:48.174604+0800 wftest[12368:305085] 把下载好的视频按顺序拼接好,然后显示在UI上播放<NSThread: 0x6000023b25c0>{number = 1, name = main}
    

      dispatch_group_enter手动通知group任务已经开始。注意,enter和leave必须成对。否则会造成诡异崩溃问题。

    最后,再来看定时源事件和子线程的运行循环:

    -(void)myMain{
        //1.定义一个定时器
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeEvnet) userInfo:nil repeats:YES];
        //2.将定时器加入到运行循环中,只有当加入到运行循环中,他才知道这个时候,有一个定时任务
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    }
    
    -(void)timeEvnet{
        NSLog(@"%d----%@",self.count,[NSThread currentThread]);
        if(self.count++ == 10){
            NSLog(@"挂了");
            //停止当前的运行循环
            CFRunLoopStop(CFRunLoopGetCurrent());
        }
    }
    

      

  • 相关阅读:
    DevOps系列【gitlab如何新建项目仓库?】
    Redis系列【docker带密码进入redis】
    Vue系列【js把一个类对象数组抽取类中几个属性重新组成新的类对象数组】
    一句话概括SSO和OAuth2.0
    CListCtrl控件使用指南(大全)
    CListCtrl的主要事件及鼠标响应函数
    c语言动态内存分配(上课)
    OSPF 路由聚合
    华为 OSPF基本配置
    OSPF NSSA区域
  • 原文地址:https://www.cnblogs.com/howlaa/p/10620437.html
Copyright © 2020-2023  润新知