NSThread & GCD & NSOperation简单介绍

392 阅读9分钟

本文学习引用iOS-底层原理 27:GCD 之 NSThread & GCD & NSOperation,在此致谢 本文的主要目的是介绍 NSThreadGCDNSOperation常见的使用方式

NSthread

NSthread是苹果官方提供面向对象的线程操作技术,是对thread的上层封装,比较偏向于底层。简单方便,可以直接操作线程对象,使用频率较少。

创建线程
线程的创建方式主要以下三种方式

  • 通过init初始化方式创建
  • 通过detachNewThreadSelector构造器方式创建
  • 通过performSelector...方法创建,主要是用于获取主线程,以及后台线程
//1、创建
- (void)cjl_createNSThread{
    NSString *threadName1 = @"NSThread1";
    NSString *threadName2 = @"NSThread2";
    NSString *threadName3 = @"NSThread3";
    NSString *threadNameMain = @"NSThreadMain";
    
    //方式一:初始化方式,需要手动启动
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:threadName1];
    [thread1 start];
    
    //方式二:构造器方式,自动启动
    [NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:threadName2];
    
    //方式三: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调度的频率高

类方法
常用的类方法有以下几个:

-   `currentThread`:获取当前线程
-   `sleep...`:阻塞线程
-   `exit`:退出线程
-   `mainThread`:获取主线程
- (void)cjl_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);

GCD

dispatch_after

- (void)cjl_testAfter{
    /*
     dispatch_after不是在制定时间后执行,而只是在指定时间追加处理到队列中
     应用场景:在主队列上延迟执行一项任务,如viewDidload之后延迟2s,提示一个alertview(是延迟加入到队列,而不是延迟执行)
     */
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"2s后输出");
    });
   
}

dispatch_once

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

dispatch_apply

- (void)cjl_testApply{
    /*
     dispatch_apply将指定的Block追加到指定的队列中重复执行,并等到全部的处理执行结束——相当于线程安全的for循环
     里面的任务执行是无序的,但最后一定会全部执行完毕

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

dispatch_apply和dispatch_sync函数相同,会等待处理结束,因此我们会在dispatch_async函数中非同步的执行dispatch_apply函数

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    NSLog(@"dispatch_apply前");
    dispatch_apply(10, globalQueue, ^(size_t index) {
        NSLog(@"dispatch_apply 的线程 %zu - %@", index, [NSThread currentThread]);
    });
    NSLog(@"dispatch_apply后");
    //dispatch_apply处理结束后 在main dispatch queue中非同步zhixing
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"dispatch_apply处理结束了,可以更新页面了 - %@", [NSThread currentThread]);
    });
});

dispatch_group_t

多任务处理结束后统一处理,有以下两种使用方式

  • 【方式一】使用dispatch_group_async + dispatch_group_notify
- (void)cjl_testGroup1{
    /*
     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
    //在第一个group里关联的block任务全部执行结束后,会发出通知,接收到通知后将第三个block任务加到第二个group队列中
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"请求均已经完成,现在可以刷新页面");
    });
}

任务可以用dispatch_group_async(block里面写同步耗时代码),也可以用dispatch_group_enter和dispatch_group_leave进行组织(enter和leave适用于异步任务,enter在异步的开始,leave在异步任务结束的回调)。dispatch_group_leave要在任务完成时候添加,比如sd下载图片应该在completed回调里leave。 如下【方式二】:

  • 【方式二】使用dispatch_group_enter + dispatch_group_leave + dispatch_group_notify
- (void)cjl_testGroup2{
    /*
     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_notify,使用dispatch_group_wait函数等待全部执行结束
- (void)cjl_testGroup3{
    /*
    在经过dispatch_group_wait函数中指定的时间或属于指定group的处理全部执行结束之前,执行该函数的线程停止。
     long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)

     group:需要等待的调度组
     timeout:等待的超时时间(即等多久)
        - 设置为DISPATCH_TIME_NOW 意味着不用任何等待直接判定调度组是否执行完毕 long timeout = dispatch_group_wait(group, DISPATCH_TIME_NOW);
        - 设置为DISPATCH_TIME_FOREVER 永久等待 只要group的处理尚未结束就会一直等待中途不能取消。DISPATCH_TIME_FOREVER该函数就一直处于调用状态而不返回,即执行dispatch_group_wait的线程(当前线程)停止。

     返回值:为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) {
    //DISPATCH_TIME_NOW时 属于group关联的任务全部执行结束
        NSLog(@"按时完成任务");
    }else{
        //DISPATCH_TIME_NOW时 属于group关联的任务还在执行中
        NSLog(@"超时");
    }
    /*
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"刷新界面");
    });
    */
}

在主线程Runloop的每次循环中可检查执行是否结束,从而不耗费多余的等待时间。虽然这样也可以,但是在一般情况下还是推荐使用dispatch_group_notify函数追加结束处理.

dispatch_barrier_sync & dispatch_barrier_async栅栏函数

当访问数据库或者文件时,使用串行队列可避免数据竞争问题。

写入以及读写不可以并行执行,但是读取之间可以并行处理。也就是说,为了高效,读取追加到并行队列,写入处理在人一个读取没有执行的状态下追加到串行队列即可(在写入处理结束之前,读取处理不可执行)

利用dispatch_group和dispatch_set_target_queue函数也可实现,但是代码会很复杂。我们可以通过dispatch_barrier_async并行队列结合使用处理。

 /*
     dispatch_barrier_sync & dispatch_barrier_async
     应用场景:同步锁
     
     等栅栏前追加到队列中的任务执行完毕后,再将栅栏后的任务追加到队列中。
     简而言之,就是先执行栅栏前任务,再执行栅栏任务,最后执行栅栏后任务 栅栏函数拦截的是队列
     dispatch_barrier_async中队列通常是自定义并行队列
     
     - dispatch_barrier_async:前面的任务执行完毕才会来到这里
     - dispatch_barrier_sync:作用相同,但是这个会堵塞线程,影响后面的任务执行
    
     - dispatch_barrier_async可以控制队列中任务的执行顺序,
     - 而dispatch_barrier_sync不仅阻塞了队列的执行,也阻塞了线程的执行(尽量少用)
  */
  
dispatch_queue_t queue = dispatch_queue_create("com.xxx.test", DISPATCH_QUEUE_CONCURRENT);
    
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
//    dispatch_async(queue, blk_for_writing);//写入 很可能会有数据问题 用下面的dispatch_barrier_async
dispatch_barrier_async(queue, blk_for_writing)
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);
dispatch_async(queue, blk7_for_reading);

image.png 使用并行队列和栅栏函数可实现高效的数据库访问和文件访问。

dispatch_semaphore_t

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

- (void)cjl_testSemaphore{
    /*
     应用场景:同步锁, 控制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(1);//当信号量初始化为1时 相当于同步锁 一次执行一个任务
    
    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_suspend挂起和dispatch_resume恢复

当追加大量处理到queue中时候,在追加处理过程中,有时候希望不执行已追加的处理。比如演算结果被block截获时,一些处理会对这个演算结果造成影响。
这种情况下只需要挂起queue即可,当可以执行时再恢复。 dispatch_suspend(queue)//挂起
dispatch_resume(queue) //恢复 这些函数对已经执行的处理没有影响。挂起后,追加到queue中的但尚未执行的处理在此之后停止执行.而恢复则是使这些处理继续执行。

dispatch_source_t

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

- (void)cjl_testSource{
    /*
     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);
    
}

NSOperation

NSOperation是基于GCD之上的更高一层封装,NSOperation需要配合NSOperationQueue来实现多线程

NSOperatino实现多线程的步骤如下:

  • 1、创建任务:先将需要执行的操作封装到NSOperation对象中。
  • 2、创建队列:创建NSOperationQueue。
  • 3、将任务加入到队列中:将NSOperation对象添加到NSOperationQueue中。
//基本使用
- (void)cjl_testBaseNSOperation{
    //处理事务
    NSInvocationOperation *op =  [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(handleInvocation::) object:@"CJL"];
    //创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //操作加入队列
    [queue addOperation:op];
    
}
- (void)handleInvocation:(id)operation{
    NSLog(@"%@ - %@", operation, [NSThread currentThread]);
}

需要注意的是,NSOperation是个抽象类,实际运用时中需要使用它的子类,有三种方式:

  • 1、使用子类NSInvocationOperation

//直接处理事务,不添加隐性队列
- (void)cjl_createNSOperation{
    //创建NSInvocationOperation对象并关联方法,之后start。
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doSomething:) object:@"CJL"];
    
    [invocationOperation start];
}
  • 2、使用子类NSBlockOperation
- (void)cjl_testNSBlockOperationExecution{
    //通过addExecutionBlock这个方法可以让NSBlockOperation实现多线程。
    //NSBlockOperation创建时block中的任务是在主线程执行,而运用addExecutionBlock加入的任务是在子线程执行的。
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"main task = >currentThread: %@", [NSThread currentThread]);
    }];
    
    [blockOperation addExecutionBlock:^{
            NSLog(@"task1 = >currentThread: %@", [NSThread currentThread]);
    }];
    
    [blockOperation addExecutionBlock:^{
            NSLog(@"task2 = >currentThread: %@", [NSThread currentThread]);
    }];
    
    [blockOperation addExecutionBlock:^{
            NSLog(@"task3 = >currentThread: %@", [NSThread currentThread]);
    }];
    
    [blockOperation start];
}
  • 3、定义继承自NSOperation的子类,通过实现内部相应的方法来封装任务。
//*********自定义继承自NSOperation的子类*********
@interface CJLOperation : NSOperation
@end

@implementation CJLOperation
- (void)main{
    for (int i = 0; i < 3; i++) {
        NSLog(@"NSOperation的子类:%@",[NSThread currentThread]);
    }
}
@end

//*********使用*********
- (void)cjl_testCJLOperation{
    //运用继承自NSOperation的子类 首先我们定义一个继承自NSOperation的类,然后重写它的main方法。
    CJLOperation *operation = [[CJLOperation alloc] init];
    [operation start];
}

NSOperationQueue

NSOperationQueue添加事务

NSOperationQueue有两种队列:主队列其他队列。其他队列包含了 串行和并发

  • 主队列:主队列上的任务是在主线程执行的。
  • 其他队列(非主队列):加入到'非队列'中的任务默认就是并发,开启多线程
- (void)cjl_testNSOperationQueue{
    /*
     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");
}

设置执行顺序

//执行顺序
- (void)cjl_testQueueSequence{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        for (int i = 0; i < 5; i++) {
            [queue addOperationWithBlock:^{
                NSLog(@"%@---%d", [NSThread currentThread], i);
            }];
        }
}

设置优先级

- (void)cjl_testOperationQuality{
    /*
     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];

}

设置并发数

//设置并发数
- (void)cjl_testOperationMaxCount{
    /*
     在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]);
        }];
    }
}

添加依赖

//添加依赖
- (void)cjl_testOperationDependency{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *bo1 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"请求token");
    }];
    
    NSBlockOperation *bo2 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"拿着token,请求数据1");
    }];
    
    NSBlockOperation *bo3 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"拿着数据1,请求数据2");
    }];
    
    [bo2 addDependency:bo1];
    [bo3 addDependency:bo2];
    
    [queue addOperations:@[bo1,bo2,bo3] waitUntilFinished:YES];
    
    NSLog(@"执行完了?我要干其他事");
}

线程间通讯

//线程间通讯
- (void)cjl_testOperationNoti{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.name = @"Felix";
    [queue addOperationWithBlock:^{
        NSLog(@"请求网络%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
        
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"刷新UI%@--%@", [NSOperationQueue currentQueue], [NSThread currentThread]);
        }];
    }];

}