08 - OC多线程之认识和使用

OC底层原理探索文档汇总

主要内容:

1、线程认识
2、NSThread认识
3、GCD的认识和使用(重点在于队列和任务)
4、NSOperation的认识和使用

1、基本概念

进程: 在内存中正在运行的程序就是进程,进程是系统进行资源调度分配的一个独立单位,进程有独立性、并发性、动态性。
线程: 线程是进程的执行单元,是一个独立的、并发的顺序执行流,进程所有的任务都在线程中执行。

  • 线程的执行是抢占式的,也就是说当前运行的线程在任何时候都可能被挂起,以便另一个线程可以运行。
  • 同一个进程中的多个线程共享附进程中的共享变量及部分环境,相互之间协同完成进程中所要完成的任务。
  • 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

一个程序后台至少有一个进程,一个进程里可以包含多个线程,至少要包含一个线程。

多线程: 在一个进程中可以开辟多条线程,多条线程可以并发执行不同的任务,多条线程并发执行,其实是CPU快速的在多条线程之间调度,CPU的执行很快,调度的时间足够快就会造成多线程同时执行的假象。

优点:

  • 能提高程序的执行效率
  • 能提高资源利用率(CPU、内存利用率)

缺点:

  • 开启线程需要占用一定的空间(默认情况下主线程占1M,子线程占512KB),如果开启大量的线程,就会占用大量的内存空间,降低了程序的性能
  • 线程越多,CPU花在调度线程上的开销也就越大
  • 程序设计更复杂,比如线程通信,线程安全等

主线程: 一个iOS程序运行后,会默认开启一个线程,这个线程就是主线程,也叫UI线程
作用:

  • 他可以通过RunLoop来待续运行和有事件触发时及时唤醒响应,没有事件时就睡眠
  • 显示/刷新UI界面
  • 处理UI事件

注:一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受到主线程的影响

进程和线程的关系 :线程是进程的执行单元,在进程中开辟的独立、并发的顺序执行流,进程所有的任务都在线程中执行。

  • 地址空间
    • 同一个进程的线程共享本进程的地址空间
    • 而进程之间则是独立的地址空间
  • 资源拥有
    • 同一个进程内线程共享本进程的资源
    • 进程之间的资源是独立的

线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不再拥有系统资源,它与附进程的其他线程共享该进程所拥有的全部资源。
因为多个线程共享父进程里的全部资源,因此编程更加方便;但必须更加小心,确保线程不会妨碍同一进程里的其他线程。

Runloop和线程的关系

  • runloop与线程是一一对应的,一个runloop对应一个核心的线程
  • runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休 眠状态,有了任务就会被唤醒去执行任务。
  • runloop在第一次获取时被创建,在线程结束时被销毁。
  • 对于主线程来说,runloop在程序一启动就默认创建好了。
  • 对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调。

线程生命周期:

线程生命周期.png

  • 新建
    • 创建新线程之后就进入新建状态,比如[NSThraed alloc]initWithTarget:
  • 就绪
    • 开启线程之后进入就绪状态,注意此时并没有运行,比如[NSThread start]
    • 该状态的线程并没有开始运行,只是表示可以运行了,至于何时运行,取决于CPU的调度
    • 获取CPU调度到其他线程后,这个线程就回到了就绪状态
    • 得到了同步锁,就又到了就绪状态(拿到锁之后并不是直接进入到运行状态,而是就绪)
    • sleep时间到了之后进入就绪状态
  • 运行
    • 当CPU调度到这个线程的时候才开始运行
    • 只能从就绪状态才能到运行状态,其他的都不行,
  • 阻塞
    • 调用sleep方法开始睡眠
    • 等待同步锁的时候处于阻塞状态
  • 死亡
    • 线程执行完成,正常退出
    • 线程执行过程中出现错误,异常退出
    • 主动退出,比如[thread exit]

2、NSThread

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

介绍

  • 使用更加面向对象,可直接操作线程对象
  • 需要手动管理生命周期,如果是很简单的线程操作可以使用
  • 一个NSThread对象就代表一个线程

2.1 NSThread的创建

共有三种方式创建:

  • 通过init初始化方式创建,必须手动启动
  • 通过detachNewThreadSelector构造器方式创建
  • 通过performSelector...方法创建,主要是用于获取主线程,以及后台线程
//1、创建
- (void)wy_createNSThreadTest{
    NSString *threadName1 = @"NSThread1";
    NSString *threadName2 = @"NSThread2";
    NSString *threadName3 = @"NSThread3";
    NSString *threadNameMain = @"NSThreadMain";
    
    //方式一:初始化方式,需要手动启动
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:threadName1];
    thread1.name = @"thread1";
    [thread1 start];
    
    //方式二:构造器方式,自动启动
    [NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:threadName2];
    
    //方式三:performSelector...方法创建子线程
    [self performSelectorInBackground:@selector(doSomething:) withObject:threadName3];
    
    //方式四:performSelector...方法创建主线程
    [self performSelectorOnMainThread:@selector(doSomething:) withObject:threadNameMain waitUntilDone:YES];
    
}

注意:selector方法最多只能接收一个参数,写的objcet就是传的参数

2.2 常用API

属性:

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

说明:
threadPropority已经被qualityOfService服务质量取代了,从上到下,数值越大,优先级越高

服务质量.png

注意:优先级高并不代表执行效率高,仅仅说明被执行的可能性更高

方法:

- (void)wy_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);
}

说明:

  • currentThread:获取当前线程
  • sleep...:阻塞线程
  • exit:退出线程
  • mainThread:获取主线程

简单案例:
在其他线程终止另一个线程
比如在UI线程中终止一个子线程,可以给子线程发送一个信号,子线程接收到后调用exit进行退出
在UI线程中调用cancel后,线程的isCancelled方法将会返回NO

2.3 线程同步

多个线程操作同一个代码,并且修改同一个资源,就会造成线程安全问题,这里就需要线程同步
线程同步就是通过加锁来实现的,通过锁可以保证在任意时刻,只有一个线程在操作共享数据,从而保证了线程的安全
具体锁的认识后面会详细分析。

2.3 线程通信

多个线程操作不同的代码,去修改同一个资源,这里的代码块不是同一个代码,所以就不能通过线程同步来实现。

注意与线程同步的区别,这里是多个线程操作不同代码,而不是相同代码。

这里使用到了NSCondition,他既可以实现线程同步,也可以实现线程通信。具体锁的使用后面会详细分析,这里先简单说明

等待唤醒机制:

当一个线程在执行时,将其他有相同锁的线程设置为等待状态,当自己执行结束,把其他线程设置为唤醒状态,这样就可以实现线程安全了。

等待
API:wait/waitUnitlData
将当前线程设置为等待状态,知道其他线程调用signal或broadcast来唤醒,如果是waitUnitdate则等到了过了某个时间点,会自动唤醒

唤醒
API:signal

  • 将当前NSCondition对象上的正在等待的线程进行唤醒
  • 如果有多个线程处于等待状态,则随机唤醒一个线程

API:boadcaset

  • 唤醒NSCondition对象上的所有线程

注意:

  • 主线程不能直接中断子线程,需要通过设置isCancelled来判断
    • 主线程中让子线程调用[thread cancel]方法即可让子线程的isCancelled设置为NO
    • 在子线程中加判断,如果isCancelled为NO,则用[NSThread exit]来中断自己
  • 线程优先级只是代表他抢夺CPU的概率大,不一定会真的能抢到
  • 子线程执行完后如果要更新数据,必须进入到主线程,不然出现安全问题
  • 当睡眠时间到了之后,或者获取到同步锁后,并不会立即运行,而是先处于就绪状态,等CPU调度到这个线程才会运行

3、GCD

全称是Grand Central Dispatch,中央调度中心,我们将任务添加到队列,并且指定执⾏任务的函数,GCD会帮我们将任务取出并放到线程中执行

优点:

  • GCD 是苹果公司为多核的并⾏运算提出的解决⽅案
  • GCD 会⾃动利⽤更多的CPU内核
  • GCD 会⾃动管理线程的⽣命周期
  • 不需要程序员手动管理线程,程序员只需要将任务提交到队列中,GCD会自动帮我们将任务取出并放到线程中执行。

3.1 队列和任务的认识

这里仅做简单认识,更详细的内容可以查看OC多线程之队列和任务的认识

3.1.1 队列

队列就是管理待执行任务的等待队列,用来调度任务给线程执行,符合先进先出原则
主队列是指主线程的队列,是一个串行队列,在主线程去执行其实就是放入了主队列
全局并发队列,可以供整个应用使用,没有特别之处,只是一个系统创建好的并发队列,我们可以直接使用

并发和串行决定了任务执行的顺序,并发是多个任务并发执行,串行是指任务顺序执行,也就是一个任务执行完成再执行下一个任务

  • 队列负责调度任务,提交任务给线程执行
  • 队列遵循先进先出原则,在这里指的是先进先调度。而不是先调度完成。
  • 队列底层会维护一个线程池来处理用户提交的任务,线程池的作用就是执行队列管理的任务。
  • 串行队列底层只维护了一个线程,并发队列的底层维护了多个线程
  • 串行队列一次只能处理一个任务,上一个任务处理完成才能处理下一个任务
  • 并发队列可以同时处理多个任务,虽然处理顺序还是先进先出的原则,但是有的任务处理时间比较长,有可能先进后调度完成。
  • 队列是先进先调度,如果是串行队列是先进先调度结束,并发队列并不是先进先调度结束,可以同时调度多个任务

创建队列:

- (void)createQueueTest{
    //创建队列
    dispatch_queue_t queue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue3 = dispatch_queue_create("串行队列", NULL);
    //获取已有队列
    //获取主队列
    dispatch_queue_t queue4 = dispatch_get_main_queue();
    //获取全局并发队列
    /*
     第一个参数是优先级,第二个无意义,填0
     */
    dispatch_queue_t queue5 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
}

3.1.2 任务

任务就是线程要执行的那段代码
执行任务有两种方式,同步和异步,同步和可以异步决定是否要开启新的线程,同步不开启,异步开启。

同步函数(sync):

  • 同步函数用来实现线程同步执行任务
  • 只能在当前线程中执行任务,不具备开启新线程的能力
  • 同步函数只能等block执行完成后才能返回

异步函数(async):

  • 异步函数用来实现线程异步执行任务
  • 异步函数不需要等block执行完成后就可以返回
  • 可以开启新线程

提交任务的函数很多都有两个版本,一个是传两个参数,传递作为任务的block代码,一个是传三个参数,传递作为任务的方法。

- (void)createTaskTest{
    /*
     参数一:队列
     参数二:作为任务的block代码
     */
     //在调度队列上提交异步执行的块并立即返回
    dispatch_async(dispatch_queue_t  _Nonnull queue, <#^(void)block#>);
    /*
     参数一:队列
     参数二:上下文
     参数三:作为任务的函数
     */
    dispatch_async_f(<#dispatch_queue_t  _Nonnull queue#>, <#void * _Nullable context#>, <#dispatch_function_t  _Nonnull work#>);
    
    /*
     参数一:队列
     参数二:作为任务的block代码
     */
     //提交块对象以执行,并在该块完成执行后返回。 
    dispatch_sync(dispatch_queue_t  _Nonnull queue, <#^(void)block#>);
    /*
     参数一:队列
     参数二:上下文
     参数三:作为任务的函数
     */
    dispatch_sync_f(<#dispatch_queue_t  _Nonnull queue#>, <#void * _Nullable context#>, <#dispatch_function_t  _Nonnull work#>);
}

注意:

  • 任务是线程执行的,不是队列执行,队列只是用来调度任务的
  • 任务的执行方式有同步和异步,同步不开辟新线程,异步会开辟新线程

3.1.3 主队列

主队列是一种特殊的串行队列,特殊在于只使用在主线程和主Runloop中,并且是在启动APP时自动创建的

3.1.4 全局并发队列

全局并发队列是系统提供的,开发者可以直接使用的一个并发队列,没有其他的特殊之处。

在获取时可以给定优先级

  • DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED
  • DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND

3.1.5 总结

任务队列线程的关系.png
  • 线程有两种方式执行任务,同步和异步,分别通过同步函数和异步函数来实现
  • 同步函数添加的任务,该线程执行完这个任务才可以执行其他任务,异步函数添加的任务,线程可以并发执行该任务
  • 任务存放在队列中,队列有两种调度方式,串行和并发,串行只能顺序处理任务,并发可以同时处理多个任务
  • 线程想要并发执行任务,需要异步函数添加任务给并发队列
  • 如果队列是串行的,即使线程可以并发执行任务也不行,因为队列是串行调度给线程的。
  • 同步函数需要等待block执行完成才可以返回
  • 异步函数不需要等待block执行完成就可以返回

3.2 队列和任务的搭配使用

任务和队列以及线程的执行关系.png

说明:

  • 同步不开启新线程,异步会开启新线程
  • 并发队列可以并发调度任务,串行队列只能顺序调度任务
  • 只有并发队列提交给线程异步执行的任务才可以异步执行

总结:

执行类型.png

3.3 常用API

3.3.1 dispatch_after延迟执行

使用很简单,只是需要知道一点,等待指定的时间后将任务块异步的添加到指定的队列中,并不是延迟执行,而是延迟入队

代码:

- (void)cjl_testAfter{
    /*
     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后输出");
    });
   
}

3.3.2 dispatch_once单例

dispatch_once的任务只执行一次

代码:

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

注意:

  • onceToken是静态变量,具有唯一性
  • dispatch_once的底层会进行加锁来保证block执行的唯一性
  • 如果任务没有执行过,会将任务进行加锁,如果在当前任务执行期间,有其他任务进来,会进入无限次等待,原因是当前任务已经获取了锁,进行了加锁,其他任务是无法获取锁的。

3.3.3 dispatch_apply重复执行

可以让任务重复添加到队列中重复执行

代码:

- (void)cjl_testApply{
    /*
     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_apply是同步函数,所以不能将其添加到主队列中,会造成死锁
  • 相当于是线程安全的for循环

3.3.4 dispatch_group_t调度组

通过调度组将任务分组实现,调度组的最直接作用是控制任务执行顺序

API:

dispatch_group_create 创建组 
dispatch_group_async 进组任务 
dispatch_group_notify 进组任务执行完毕通知 
dispatch_group_wait 进组任务执行等待时间

//进组和出组一般是成对使用的
dispatch_group_enter 进组 
dispatch_group_leave 出组

【方式一】使用dispatch_group_async + dispatch_group_notify

设定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_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"刷新页面");
    });
}

【方式二】使用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(@"刷新界面");
    });
}

执行结果:

2021-11-03 20:47:03.680798+0800 多线程使用[73188:1335127] 请求一完成
2021-11-03 20:47:03.680800+0800 多线程使用[73188:1335125] 请求二完成
2021-11-03 20:47:03.694075+0800 多线程使用[73188:1334877] 刷新界面
  • 在我们想要提前执行的任务执行后加上dispatch_group_leave,就可以让dispatch_group_notify中的任务执行
  • 只要有一对enter-leave执行完成就可以,不需要多对都执行完成
  • 如果enter比leave多,会一直等待
  • 如果enter比leave少,会崩溃

在方式二的基础上增加超时dispatch_group_wait
代码:

- (void)cjl_testGroup3{
    /*
     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(@"刷新界面");
    });
}

总结:

  • enter-leave只要成对就可以,不管远近,一旦有一对执行完成,就可以执行dispatch_group_notify中的任务
  • dispatch_group_async 等同于enter - leave,其底层的实现就是enter-leave
  • enter-leave必须成对出现

3.3.5 栅栏函数

栅栏函数有同步栅栏函数和异步栅栏函数,分别通过dispatch_barrier_sync & dispatch_barrier_async实现

栅栏函数的作用就是在自定义的并发队列中让某一部分任务按顺序执行,具有同步的效果。

同步栅栏函数dispatch_barrier_sync

//同步栅栏函数dispatch_barrier_sync
- (void)cjl_testBarrier1{
    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]);
    
    //栅栏函数的作用是将队列中的任务进行分组,所以我们只要关注任务1、任务2
    dispatch_barrier_sync(queue, ^{
        sleep(1);
        NSLog(@"------------延迟1s的栅栏任务------------%@", [NSThread currentThread]);
    });
    NSLog(@"栅栏结束 - %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"不延迟的任务2 - %@", [NSThread currentThread]);
    });
    NSLog(@"第二次结束 - %@", [NSThread currentThread]);
}

运行结果:

2021-11-03 21:40:32.581611+0800 多线程使用[75238:1382574] 开始 - <_NSMainThread: 0x600001a7c140>{number = 1, name = main}
2021-11-03 21:40:32.581733+0800 多线程使用[75238:1382574] 第一次结束 - <_NSMainThread: 0x600001a7c140>{number = 1, name = main}
2021-11-03 21:40:34.582062+0800 多线程使用[75238:1382843] 延迟2s的任务1 - {number = 6, name = (null)}
2021-11-03 21:40:35.582597+0800 多线程使用[75238:1382574] ------------延迟1s的栅栏任务------------<_NSMainThread: 0x600001a7c140>{number = 1, name = main}
2021-11-03 21:40:35.582839+0800 多线程使用[75238:1382574] 栅栏结束 - <_NSMainThread: 0x600001a7c140>{number = 1, name = main}
2021-11-03 21:40:35.583055+0800 多线程使用[75238:1382574] 第二次结束 - <_NSMainThread: 0x600001a7c140>{number = 1, name = main}
2021-11-03 21:40:35.583136+0800 多线程使用[75238:1382843] 不延迟的任务2 - {number = 6, name = (null)}

说明:

  • 同步栅栏函数提交任务以供当前线程执行,运行结果中可以看到是主线程执行的同步栅栏函数,这个很好理解,前面我们分析过了,同步函数不会开辟新线程。
  • 需要等待该任务的完成才能返回,所以会阻塞当前线程的执行,运行结果中可以看到执行完栅栏函数才会执行”栅栏结束“代码
  • 当栅栏任务到达自定义并发队列的顶端时,并不会立即执行,必须等待当前队列中正在执行的block完成执行才可以执行栅栏任务,所以运行结果中可以看出即时任务1需要等待2s,也必须等任务1执行完成才可以执行栅栏函数
  • 在栅栏任务完成之前,不会执行栅栏函数之后提交的任何任务,所以运行结果中可以看到栅栏函数需要等待1s,也必须等待栅栏函数执行完才可以执行任务2.
  • 也就是说栅栏函数作为栅栏,可以将它之前的任务、栅栏任务、之后的任务按顺序执行,使得在并发队列中没有串行的效果

异步栅栏函数dispatch_barrier_async:
代码:

//异步栅栏函数dispatch_barrier_async
- (void)cjl_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, ^{
        sleep(1);
        NSLog(@"------------延迟1s的栅栏任务------------%@", [NSThread currentThread]);
    });
    NSLog(@"栅栏结束 - %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"不延迟的任务2 - %@", [NSThread currentThread]);
    });
    NSLog(@"第二次结束 - %@", [NSThread currentThread]);
}

运行结果:

2021-11-03 21:44:03.482963+0800 多线程使用[75383:1386090] 开始 - <_NSMainThread: 0x600001874140>{number = 1, name = main}
2021-11-03 21:44:03.483082+0800 多线程使用[75383:1386090] 第一次结束 - <_NSMainThread: 0x600001874140>{number = 1, name = main}
2021-11-03 21:44:03.483157+0800 多线程使用[75383:1386090] 栅栏结束 - <_NSMainThread: 0x600001874140>{number = 1, name = main}
2021-11-03 21:44:03.483248+0800 多线程使用[75383:1386090] 第二次结束 - <_NSMainThread: 0x600001874140>{number = 1, name = main}
2021-11-03 21:44:05.485233+0800 多线程使用[75383:1386439] 延迟2s的任务1 - {number = 7, name = (null)}
2021-11-03 21:44:06.487452+0800 多线程使用[75383:1386439] ------------延迟1s的栅栏任务------------{number = 7, name = (null)}
2021-11-03 21:44:06.487667+0800 多线程使用[75383:1386439] 不延迟的任务2 - {number = 7, name = (null)}

说明:

  • 异步栅栏函数提交异步执行的代码块并立即返回,不需要等待block执行完成就可以返回,所以不会阻塞当前线程,在运行结果中可以看到主线程的任务全部执行完才会执行异步函数,并不会阻塞主线程。
  • 异步函数会开辟新线程,所以运行结果中执行栅栏任务的线程不是主线程
  • 这里也可以看到并发队列的执行顺序是任务1、栅栏任务、任务2,其原理与同步栅栏函数一样,不再赘述

总结:

  • 栅栏函数的作用是在自定义的并发队列中让某些任务按顺序执行,具有串行的效果
  • 栅栏函数只能作用于自定义的并发队列,如果作用于串行队列以及全局并发队列,它就相当于一个普通的同步函数或异步函数,无法起到栅栏的作用
  • 同步栅栏函数和异步栅栏函数的作用都是一样的,区别在于同步栅栏函数是当前线程执行的,而且必须等待栅栏任务完成才可以返回,有阻塞当前线程的效果。异步函数是新开辟线程执行的,而且无需等待任务块的完成就可以返回,没有阻塞当前线程的效果
  • 当我们想要在并发队列中控制某些任务的执行顺序时就可以使用栅栏函数

3.3.6 dispatch_semaphore_t信号量

信号量主要用作同步锁,实现线程安全,还可以用于控制GCD最大并发数。

代码:

- (void)cjl_testSemaphore{
    /*
     应用场景:同步当锁, 控制GCD最大并发数

     - dispatch_semaphore_create():创建信号量
     - dispatch_semaphore_wait():等待(减少)信号量,信号量减1。当信号量< 0时会阻塞当前线程,根据传入的等待时间决定接下来的操作——如果永久等待将等到有信号(信号量>=0)才可以执行下去
     - dispatch_semaphore_signal():递增信号量,信号量加1。当信号量>= 0 会执行dispatch_semaphore_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_semaphore_wait()会将信号量-1,当减小到<0时,该函数将无法返回,执行该任务的线程也将被阻塞,wait之后的代码将无法执行
  • dispatch_semaphore_signal会将信号量+1,当加到>=0时,可以执行wait中的任务。
  • 我们看到执行一次signal,就可以执行一次wait之后的代码

同步锁的使用:

  • wait之后的代码作为线程安全的代码
  • wait作为锁,当执行了wait后信号<0,此时就被上锁了,想要执行,需要通过signal解锁。

最大并发数的使用:

  • 最大并发数也就是线程最多可以同时执行多少任务
  • 将wait之后的代码作为任务
  • 我们调用n次signal,最大并发数就是n
  • 这是因为当执行了n次wait之后的任务后,信号<0,就无法再次执行了

3.3.7 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);
    
}

4、NSOperation

NSOperation和NSOperationQueue一起可以实现多线程,基于GCD的面向对象的封装,封装了很多实用的功能。

4.1 具体使用步骤

//基本使用
- (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]);
}

详细步骤:
1、先将执行的操作放到一个NSOperation对象中
2、将NSOperation对象添加到NSOperationQueue中
3、系统会自动将NSOperationQueue中的NSOperation对象取出来
4、将取出的NSOperation封装的操作放到一条新线程中执行

4.2 NSOperation的子类

NSOperation是一个抽象类,并不具备封装的能力,所以必须要使用子类,系统提供两种NSInvocationOperation、NSBlockOperation,我们还可以自定义类

4.2.1 NSInvocationOperation


//直接处理事务,不添加隐性队列
- (void)cjl_createNSOperation{
    //创建NSInvocationOperation对象并关联方法,之后start。
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doSomething:) object:@"CJL"];
    
    [invocationOperation start];
}

4.2.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];
}

4.2.3 自定义子类

继承自NSOperation,实现内部的方法

  1. 重写-(void)main方法,里面写任务
  2. 经常通过- (BOOL)isCancelled方法检测操作是否被取消,对取消做出响应
//*********自定义继承自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];
}

注意:自己创建自动释放池

4.3 NSOperationQueue的认识

NSOperationQueue添加事务
将NSOperation添加到NSOperationQueue,就会异步执行NSOperation的操作,操作队列不同于GCD的先进先出原则,它是添加到队列后先进入到就绪状态,之后的运行顺序由优先级决定。

有两种类型:
主队列:运行在主线程之上,可以直接获取
自定义队列:在后台执行,我们自己创建的都是自定义的

基本使用:

- (void)OperationQueue1
{
//创建一个队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];

//创建一个任务
NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download1) object:nil];

NSInvocationOperation *op2 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download1) object:nil];

NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block----%@",[NSThread currentThread]);
}];
[op3 addExecutionBlock:^{
NSLog(@"block----%@",[NSThread currentThread]);
}];

//把任务添加到队列当中去,相当于调用了任务的start方法
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
}

添加API

- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;

4.4 常用的功能

4.4.1 最大并发数

最大并发数表示最多同时执行的任务数,对于多个任务可以用来控制并发、串行。

API:
属性maxConcurrentOperationCount

注意:

  • 最大并发数的值并不代表线程的个数,仅仅代表线程的ID
  • 如果为1则表示串行执行

代码案例:

//设置并发数
- (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]);
        }];
    }
}

4.4.1 可以设置优先级

设置优先级只是抢先执行的可能性变大,并不是一定是会先执行

API:
属性qualityOfService

优先级:

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};

代码案例:

- (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];

}

4.4.1 任务状态控制

可以对任务进行控制,比如取消、暂停和恢复

API
取消:

//取消多个任务
- (void)cancelAllOperations;
//取消单个任务
- (void)cancel

暂停和恢复:

- (void)setSuspended:(BOOL)b; // YES代表暂停队列,NO代表恢复队列
- (BOOL)isSuspended;当前状态

注意:

  • 通过队列来暂停队列里的任务,注意暂停和恢复的是队列,而不是任务
  • 当前正在执行一个耗时操作,我们取消或者暂停时,会把当前任务执行完后再取消或暂停队列。而不是直接暂停或取消该任务

代码案例:

- (void)OperationQueue1
{
//创建一个队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];

//创建一个任务
NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download1) object:nil];

NSInvocationOperation *op2 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download1) object:nil];

NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block----%@",[NSThread currentThread]);
}];
[op3 addExecutionBlock:^{
NSLog(@"block----%@",[NSThread currentThread]);
}];

//把任务添加到队列当中去,相当于调用了任务的start方法
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];

//取消任务一
[op1 cancel];

//暂停队列
[queue setSuspended:YES];

sleep(10);

//恢复队列
[queue setSuspended:YES];

}

4.4.1 任务依赖

NSOperation可以通过设置依赖来保证执行顺序

API:

- (void)addDependency:(NSOperation *)op;//添加依赖
- (void)removeDependency:(NSOperation *)op;//移除依赖
//添加依赖
- (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(@"执行完了?我要干其他事");
}

注意:

  • 可以在不同的Queue的NSOperation之间建立依赖关系
  • 两个NSOperation不可以相互依赖
  • 设置依赖要放到任务添加到队列之前,而不能是后面

4.4.1 任务监听

监听一个操作是否执行完毕

API:

bo2.completionBlock = ^{
        <#code#>
    }

代码案例:

- (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");
}

注意:

  • NSOperation调用上面的方法或属性,并且实现block,就会监听到这个NSOperation执行完成后执行这个block

线程池的了解

线程池原理.png
  1. 判断核心线程池是否都正在执行任务
    1. 返回NO,创建新的工作线程去执行
    2. 返回YES,进入【第二步】
  2. 判断线程池工作队列是否已经饱满
    1. 返回NO,将任务存储到工作队列,等待CPU调度
    2. 返回YES,进入【第三步】
  3. 判断线程池中的线程是否都处于执行状态
    1. 返回NO,安排可调度线程池中空闲的线程去执行任务
    2. 返回YES,进入【第四步】
  4. 最后交给饱和策略去执行,主要有以下四种(在iOS中并没有找到以下4种策略)

你可能感兴趣的:(08 - OC多线程之认识和使用)