手撕iOS底层31 -- GCD之NSThread & GCD & NSOperation进阶使用

737 阅读11分钟

上一篇介绍了GCD的基本使用

1.NSThread 每个NSThread对象对应一个线程,真正最原始的线程。 1)优点:NSThread 轻量级最低,相对简单。 2)缺点:手动管理所有的线程活动,如生命周期、线程同步、睡眠等。

2.NSOperation 自带线程管理的抽象类。 1)优点:自带线程周期管理,操作上可更注重自己逻辑。 2)缺点:面向对象的抽象类,只能实现它或者使用它定义好的两个子类:NSInvocationOperation 和 NSBlockOperation。

3.GCD Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法。 1)优点:最高效,避开并发陷阱。 2)缺点:基于C实现。

4.选择小结 1)简单而安全的选择NSOperation实现多线程即可。 2)处理大量并发数据,又追求性能效率的选择GCD。 3) 在频繁使用多线程的程序中一般不建议使用NSThread

0x00 - NSThread

NSThread是苹果官方提供的面向对象的线程操作技术,是thread的上层封装, 比较偏于底层,简单方便,可以直接操作对象,但使用频率较少,不过需要自己控制线程的生命周期,经常使用[NSThread currentThread]获取当前线程

官方参考NSThread

iOS进阶之多线程--NSThread详解

创建线程

  • 通过alloc init初始化创建
  • 通过detachNewThreadSelector构造器方式创建
  • 隐式创建通过performSelector...方法创建,主要用于在主线程后台线程执行任务
  • 自定义继承NSThread类, 重写main方法,在方法里不需要写super
@interface MyThread: NSThread

@end
@implementation MyThread

- (void)main {
    NSLog(@"%s --- %@", __func__, [NSThread currentThread]);
}
@end

//1、创建
- (void)test_createNSThread{
    NSString *threadName1 = @"NSThread1";
    NSString *threadName2 = @"NSThread2";
    NSString *threadName3 = @"NSThread3";
    NSString *threadNameMain = @"NSThreadMain";
    
    //方式一:初始化方式,需要手动启动
    NSThread *threadWithArgs = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:threadName1];
    NSThread *thread1 = [[NSThread alloc] init];
    NSThread *threadWithBlock = [[NSThread alloc] initWithBlock:^{
        NSLog(@"threadWithBlock - %@", [NSThread currentThread]);
    }];
    thread1.threadPriority = 1;// 设置线程的优先级(0.0 - 1.0,1.0最高级)
    [threadWithArgs start];
    MyThread *myThread = [[MyThread alloc] init];
    [myThread start];
  
    //方式二:构造器方式,自动启动
    [NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:threadName2];
    [NSThread detachNewThreadWithBlock:^{
        
    }];
    //方式三:performSelector...方法创建
    [self performSelectorInBackground:@selector(doSomething:) withObject:threadName3];
    
    //方式四
    [self performSelectorOnMainThread:@selector(doSomething:) withObject:threadNameMain waitUntilDone:YES];
    
}
- (void)doSomething:(NSObject *)objc{
    NSLog(@"%@ - %@", objc, [NSThread currentThread]);
}

常用属性

- thread.isExecuting    //线程是否在执行
- thread.isCancelled    //线程是否被取消
- thread.isFinished     //是否完成
- thread.isMainThread   //是否是主线程
- thread.threadPriority //线程的优先级,取值范围0.0-1.0,默认优先级0.5,1.0表示最高优先级,优先级高,CPU调度的频率高

常用类方法

@property (class, readonly, strong) NSThread *currentThread; //获取当前线程
+ (void)sleepUntilDate:(NSDate *)date; //阻塞线程,休眠到指定时间
+ (void)sleepForTimeInterval:(NSTimeInterval)ti; // 阻塞线程 当前线程睡多长时间
+ (void)exit; // 退出线程
@property(class, readonly, strong) NSThread *mainThread; // 获取主线程
- (void)test_NSThreadClassMethod{
    //当前线程
    [NSThread currentThread];
    // 如果number=1,则表示在主线程,否则是子线程
    NSLog(@"%@", [NSThread currentThread]);
    
    //阻塞休眠
    [NSThread sleepForTimeInterval:2];//休眠多久
    [NSThread sleepUntilDate:[NSDate date]];//休眠到指定时间
    
    //其他
    [NSThread exit];//退出线程
    [NSThread isMainThread];//判断当前线程是否为主线程
    [NSThread isMultiThreaded];//判断当前线程是否是多线程
    NSThread *mainThread = [NSThread mainThread];//主线程的对象
    NSLog(@"%@", mainThread);

0x01 - GCD进阶

dispatch_once

    /*
     dispatch_once保证在App运行期间,block中的代码只执行一次
     应用场景:单例、method-Swizzling
     */
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //创建单例、method swizzled或其他任务
        NSLog(@"创建单例");
    });

dispatch_after

    /*
     dispatch_after表示在某队列中的block延迟执行
     应用场景:在主队列上延迟执行一项任务,如viewDidload之后延迟1s,提示一个alertview(是延迟加入到队列,而不是延迟执行)
     */
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"2s后输出");
    });

dispatch_apply

    /*
     dispatch_apply将指定的Block追加到指定的队列中重复执行,并等到全部的处理执行结束——相当于线程安全的for循环

     应用场景:用来拉取网络数据后提前算出各个控件的大小,防止绘制时计算,提高表单滑动流畅性
     - 添加到串行队列中——按序执行
     - 添加到主队列中——死锁
     - 添加到并发队列中——乱序执行
     - 添加到全局队列中——乱序执行
     */
    
    dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_SERIAL);
    NSLog(@"dispatch_apply前");
    /**
         param1:重复次数
         param2:追加的队列
         param3:执行任务
         */
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"dispatch_apply 的线程 %zu - %@", index, [NSThread currentThread]);
    });
    NSLog(@"dispatch_apply后");

dispatch_group 俩种配合使用方式

队列组可以将很多队列添加到一个组里,这样做的好处是,当这个组里所有的任务都执行完了,队列组会通过一个方法通知我们。

dispatch_group_async可以实现监听一组任务是否完成,完成后得到通知执行其他的操作。这个方法很有用,比如你执行三个下载任务,当三个任务都下载完成后你才通知界面说完成的了。

  • 使用dispatch_group_async + dispatch_group_notify
    /*
     dispatch_group_t:调度组将任务分组执行,能监听任务组完成,并设置等待时间

     应用场景:多个接口请求之后刷新页面
     */
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"请求一完成");
    });
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"请求二完成");
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"刷新页面");
    });
  • 使用dispatch_group_enter + dispatch_group_leave + dispatch_group_notify
    /*
     dispatch_group_enter和dispatch_group_leave成对出现,使进出组的逻辑更加清晰
     */
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"请求一完成");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"请求二完成");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"刷新界面");
    });
  • 增加dispatch_group_wait函数
    /*
     long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)

     group:需要等待的调度组
     timeout:等待的超时时间(即等多久)
        - 设置为DISPATCH_TIME_NOW意味着不等待直接判定调度组是否执行完毕
        - 设置为DISPATCH_TIME_FOREVER则会阻塞当前调度组,直到调度组执行完毕


     返回值:为long类型
        - 返回值为0——在指定时间内调度组完成了任务
        - 返回值不为0——在指定时间内调度组没有按时完成任务

     */
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"请求一完成");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"请求二完成");
        dispatch_group_leave(group);
    });
    
//    long timeout = dispatch_group_wait(group, DISPATCH_TIME_NOW);
//    long timeout = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 *NSEC_PER_SEC));
    NSLog(@"timeout = %ld", timeout);
    if (timeout == 0) {
        NSLog(@"按时完成任务");
    }else{
        NSLog(@"超时");
    }
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"刷新界面");
    });

dispatch_barrier_sync & dispatch_barrier_async 栅栏函数

dispatch_barrier_async是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行。

  • 串行队列
- (void)testBarrier{
    /*
     dispatch_barrier_sync & dispatch_barrier_async
     
     应用场景:同步锁
     
     等栅栏前追加到队列中的任务执行完毕后,再将栅栏后的任务追加到队列中。
     简而言之,就是先执行栅栏前任务,再执行栅栏任务,最后执行栅栏后任务
     
     - dispatch_barrier_async:前面的任务执行完毕才会来到这里
     - dispatch_barrier_sync:作用相同,但是这个会堵塞线程,影响后面的任务执行
    
     - dispatch_barrier_async可以控制队列中任务的执行顺序,
     - 而dispatch_barrier_sync不仅阻塞了队列的执行,也阻塞了线程的执行(尽量少用)
     */
    
    [self testBarrier1];
    [self testBarrier2];
}
- (void)testBarrier1{
    //串行队列使用栅栏函数
    
    dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_SERIAL);
    
    NSLog(@"开始 - %@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2s的任务1 - %@", [NSThread currentThread]);
    });
    NSLog(@"第一次结束 - %@", [NSThread currentThread]);
    
    //栅栏函数的作用是将队列中的任务进行分组,所以我们只要关注任务1、任务2
    dispatch_barrier_async(queue, ^{
        NSLog(@"------------栅栏任务------------%@", [NSThread currentThread]);
    });
    NSLog(@"栅栏结束 - %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2s的任务2 - %@", [NSThread currentThread]);
    });
    NSLog(@"第二次结束 - %@", [NSThread currentThread]);
}
- (void)testBarrier2{
    //并发队列使用栅栏函数
    
    dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"开始 - %@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2s的任务1 - %@", [NSThread currentThread]);
    });
    NSLog(@"第一次结束 - %@", [NSThread currentThread]);
    
    //由于并发队列异步执行任务是乱序执行完毕的,所以使用栅栏函数可以很好的控制队列内任务执行的顺序
    dispatch_barrier_async(queue, ^{
        NSLog(@"------------栅栏任务------------%@", [NSThread currentThread]);
    });
    NSLog(@"栅栏结束 - %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2s的任务2 - %@", [NSThread currentThread]);
    });
    NSLog(@"第二次结束 - %@", [NSThread currentThread]);
}

dispatch_semaphore_t 信号量

信号量主要用作同步锁,用于控制GCD最大并发数

 /*
     应用场景:同步当锁, 控制GCD最大并发数

     - dispatch_semaphore_create():创建信号量
     - dispatch_semaphore_wait():等待信号量,信号量减1。当信号量< 0时会阻塞当前线程,根据传入的等待时间决定接下来的操作——如果永久等待将等到信号(signal)才执行下去
     - dispatch_semaphore_signal():释放信号量,信号量加1。当信号量>= 0 会执行wait之后的代码

     */
    dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"当前 - %d, 线程 - %@", i, [NSThread currentThread]);
        });
    }
    
    //利用信号量来改写 控制顺序
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"当前 - %d, 线程 - %@", i, [NSThread currentThread]);
            
            dispatch_semaphore_signal(sem);
        });
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    }

dispatch_source_t

dispatch_source_t主要用于计时操作,其原因是因为它创建的timer不依赖于RunLoop,且计时精准度比NSTimer

 /*
     dispatch_source
     
     应用场景:GCDTimer
     在iOS开发中一般使用NSTimer来处理定时逻辑,但NSTimer是依赖Runloop的,而Runloop可以运行在不同的模式下。如果NSTimer添加在一种模式下,当Runloop运行在其他模式下的时候,定时器就挂机了;又如果Runloop在阻塞状态,NSTimer触发时间就会推迟到下一个Runloop周期。因此NSTimer在计时上会有误差,并不是特别精确,而GCD定时器不依赖Runloop,计时精度要高很多
     
     dispatch_source是一种基本的数据类型,可以用来监听一些底层的系统事件
        - Timer Dispatch Source:定时器事件源,用来生成周期性的通知或回调
        - Signal Dispatch Source:监听信号事件源,当有UNIX信号发生时会通知
        - Descriptor Dispatch Source:监听文件或socket事件源,当文件或socket数据发生变化时会通知
        - Process Dispatch Source:监听进程事件源,与进程相关的事件通知
        - Mach port Dispatch Source:监听Mach端口事件源
        - Custom Dispatch Source:监听自定义事件源

     主要使用的API:
        - dispatch_source_create: 创建事件源
        - dispatch_source_set_event_handler: 设置数据源回调
        - dispatch_source_merge_data: 设置事件源数据
        - dispatch_source_get_data: 获取事件源数据
        - dispatch_resume: 继续
        - dispatch_suspend: 挂起
        - dispatch_cancle: 取消
     */
    
    //1.创建队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    //2.创建timer
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //3.设置timer首次执行时间,间隔,精确度
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0*NSEC_PER_SEC, 0.1*NSEC_PER_SEC);
    //4.设置timer事件回调
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"GCDTimer");
    });
    //5.默认是挂起状态,需要手动激活
    dispatch_resume(timer);

0x02 - NSOperation

NSOperation是苹果公司对 GCD 的封装,完全面向对象,NSOperationNSOperationQueue 分别对应 GCD 的任务和队列 。也就是NSOperation需要配合NSOperationQueue来实现多线程

操作步骤也很好理解:

  1. 将要执行的任务封装到一个 NSOperation 对象中。
  2. 将此任务添加到一个 NSOperationQueue 对象中。
  3. 将任务加入到队列中,即将NSOperation 对象添加到NSOperationQueue
  1. 使用子类NSInvocationOperation
//1.创建NSInvocationOperation对象
  NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
   //2.开始执行
 [operation start];
  1. 使用子类NSBlockOperation

除了上面的两种 Operation 以外,我们还可以自定义 Operation。自定义 Operation 需要继承 NSOperation 类,并实现其 main() 方法,因为在调用 start() 方法的时候,内部会调用 main() 方法完成相关逻辑。所以如果以上的两个类无法满足你的欲望的时候,你就需要自定义了。你想要实现什么功能都可以写在里面。除此之外,你还需要实现 cancel() 在内的各种方法。

如果是需要并发执行的话,还必须要重写

  • start
  • asynchronous
  • executing
  • finished
  //1.创建NSBlockOperation对象
  NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
      NSLog(@"%@", [NSThread currentThread]);
  }];

  //2.开始任务
  [operation start];

NSOperationQueue

NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];  //主队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //自定义队列

NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];  //主队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //自定义队列

//添加一个NSOperation
[queue addOperation:operation]

//添加一组NSOperation
[queue addOperations:operations waitUntilFinished:NO

//添加一个block形式的Operation
[queue addOperationWithBlock:^{
    //执行一个Block的操作        
}];

[queue setMaxConcurrentOperationCount:1];

//单个NSOperation取消
[operation cancel]

//取消NSOperationQueue中的所有操作
[queue cancelAllOperations]

// 暂停queue  
[queue setSuspended:YES];  
  
// 继续queue  
[queue setSuspended:NO]; 

NSOperationQueue 有两种不同类型的队列:主队列和自定义队列(加入到'非队列'中的任务默认就是并发,开启多线程。)。主队列运行在主线程之上,而自定义队列在后台执行。在两种类型中,这些队列所处理的任务都使用 NSOperation 的子类来表述。

    /*
     NSInvocationOperation和NSBlockOperation两者的区别在于:
     - 前者类似target形式
     - 后者类似block形式——函数式编程,业务逻辑代码可读性更高
     
     NSOperationQueue是异步执行的,所以任务一、任务二的完成顺序不确定
     */
    // 初始化添加事务
    NSBlockOperation *bo = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"任务1————%@",[NSThread currentThread]);
    }];
    // 添加事务
    [bo addExecutionBlock:^{
        NSLog(@"任务2————%@",[NSThread currentThread]);
    }];
    // 回调监听
    bo.completionBlock = ^{
        NSLog(@"完成了!!!");
    };
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:bo];
    NSLog(@"事务添加进了NSOperationQueue");
  • 设置顺序输出
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:1]; // 控制并发任务的数量,当设置为 1 时, 那么它就是一个串行队列。主对列默认是串行队列
        for (int i = 0; i < 5; i++) {
            [queue addOperationWithBlock:^{
                NSLog(@"%@---%d", [NSThread currentThread], i);
            }];
        }
  • 设置优先级
    /*
     NSOperation设置优先级只会让CPU有更高的几率调用,不是说设置高就一定全部先完成
     - 不使用sleep——高优先级的任务一先于低优先级的任务二
     - 使用sleep进行延时——高优先级的任务一慢于低优先级的任务二
     */
    NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; i++) {
            //sleep(1);
            NSLog(@"第一个操作 %d --- %@", i, [NSThread currentThread]);
        }
    }];
    // 设置最高优先级
    bo1.qualityOfService = NSQualityOfServiceUserInteractive;
    
    NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"第二个操作 %d --- %@", i, [NSThread currentThread]);
        }
    }];
    // 设置最低优先级
    bo2.qualityOfService = NSQualityOfServiceBackground;
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:bo1];
    [queue addOperation:bo2];
  • 设置并发数
   /* 在GCD中只能使用信号量来设置并发数
      而NSOperation轻易就能设置并发数
      通过设置maxConcurrentOperationCount来控制单次出队列去执行的任务数
    */
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.name = @"Felix";
    queue.maxConcurrentOperationCount = 2;
    
    for (int i = 0; i < 5; i++) {
        [queue addOperationWithBlock:^{ // 一个任务
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        }];
    }
  • 添加依赖

NSOperation 有一个非常实用的功能,那就是添加依赖。比如有 3 个任务:A: 从服务器上下载一张图片,B:给这张图片加个水印,C:把图片返回给服务器。这时就可以用到依赖了:

//1.任务一:下载图片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"下载图片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//2.任务二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"打水印   - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//3.任务三:上传图片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"上传图片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//4.设置依赖
[operation2 addDependency:operation1];      //任务二依赖任务一
[operation3 addDependency:operation2];      //任务三依赖任务二

//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
  • 线程通讯
   NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.name = @"Felix";
    [queue addOperationWithBlock:^{
        NSLog(@"请求网络%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
        
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"刷新UI%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
        }];
    }];

iOS多线程总结

欢迎大佬留言指正😄,码字不易,觉得好给个赞👍 有任何表达或者理解失误请留言交流;共同进步;