iOS多线程编程介绍

作者: THU_Yu | 来源:发表于2020-08-26 18:45 被阅读0次

    一、进程与线程

       进程是系统中正在运行的应用程序,是CPU分配资源和调度的单位,线程是CPU调度(执行任务)的最小单位,一个进程内必须至少包含一个线程。
       不同进程拥有不同的内存资源,而在同一进程的不同线程则是共享进程的资源(这也就导致可能出现线程安全问题)。

    二、多线程编程

       为什么需要使用多线程?在一个APP的项目中,用户所看到的UI界面都是在主线程进行改动,如果我们在主线程的某一个方法中执行了一项耗时操作(比如大量for循环、大批量网络请求等),由于同一线程内的程序是串行顺序执行,因此会导致主线程堵塞,界面卡死。如果我们将这些耗时操作放到一个新的线程中处理,透过线程的并行执行,可以避免主线程阻塞,等到子线程处理完成后,可以利用线程间的通信方法通知主线程。
       线程的运行方式有两种:线程同步线程异步。线程同步是指线程按顺序执行,通常发生在不同线程需要访问一个加锁的数据时;线程异步就是线程并行,通过CPU的快速调度,实现同时运行的效果。

    • iOS中线程的实现方案

    技术方案 简介 语言 线程生命周期
    pthread 通用的多线程API
    适用于Unix\Linux\Windows等系统
    跨平台\可移植
    使用难度大
    C 程序员管理
    NSThread 面向对象
    简单易用,可以直接操作线程对象
    OC 程序员管理
    GCD 为了替代NSThread,更底层
    有效利用系统多核
    C 自动管理
    NSOperation 基于GCD
    面向对象
    比GCD多了一些简单易用的功能
    OC 自动管理
    • 线程的状态

       线程可以分为几个状态:新建、就绪、运行、阻塞、死亡。新建线程的时候,会在内存中创建一个线程,这时的线程还不可以被调度。当进入就绪阶段后,线程会被放入可调度线程池,接受cpu的调度。当cpu调度时,线程进入运行阶段。在运行阶段调用sleep或等待同步锁时,会进入阻塞状态,此时线程被移出可调度线程池,直到阻塞条件结束回到就绪状态。当线程内的方法执行结束后或其他异常导致强制退出时,进入死亡状态,进行线程的销毁。

    • 线程安全

       线程安全问题是指当不同线程同时对一个数据进行请求并修改时,会造成数据处理错误的情况。下面一个存钱取钱的例子便是一个线程安全问题:

       现在有两个人(线程):A(线程A)和B(线程B),当A和B在不同的地方同时向银行卡(进程的内存资源)请求余额信息(数据),此时银行卡会告诉他们余额是1000元,接着A先进行了存钱操作,并修改银行卡余额为2000(修改数据),但对于B而言,B所知道的银行卡余额是1000元,此时B再进行取钱操作,并修改银行卡余额为500元,这样银行卡的余额会被覆盖,变成只有500元,很明显得到了一个错误的数据。
       为了解决上面提到的数据错误问题,我们需要对数据上锁,当一个线程要访问某个数据且进行修改时,把这个数据锁住,等到该线程修改完毕再解锁让其他线程取用,这个就是互斥锁

       从上面的流程来看,互斥锁使得这些线程按照访问顺序执行,也就是前面提到的线程同步。下面我们用代码实现一个互斥锁(使用NSThread)。
    ViewController.h

    #import <UIKit/UIKit.h>
    
    @interface ViewController:UIViewController
    @end
    

    ViewController.m

    #import  "ViewController.h"
    
    @interface ViewController:UIViewController
    @property (nonatomic, strong)NSThread *thread1;
    @property (nonatomic, strong)NSThread *thread2;
    @property (nonatomic, strong)NSThread *thread3;
    @property (nonatomic, assign)NSInteger num;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        // 设置初始数据
        self.num = 100;
        // 开辟多线程,三个线程都执行func,func内会对self.num进行修改,因而产生线程安全问题
        self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(func)  object:nil];
        self.thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(func)  object:nil];
        self.thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(func)  object:nil];
        // 设置线程名称
        self.thread1.name = @"线程A";
        self.thread2.name = @"线程B";
        self.thread3.name = @"线程C";
        // 启动线程
        [self.thread1 start];
        [self.thread2 start];
        [self.thread3 start];
    }
    
    - (void)func
    {
        while (1)
        {
            @synchronize(self)  // 加上互斥锁,self是锁对象(要使用全局的对象),通常用self即可
            {
                NSInteger count = self.num;
                if (count > 0) {
                    self.num = count - 1;
                    for (int i; i < 1000000; i ++) // 耗时操作
                    {}
                    // 打印当前线程名称和num
                    NSLog(@"%@-----%zd", [NSThread currentThread].name, self.num);
                } else {
                    // 打印当前线程名称和num`
                    NSLog(@"%@-----%zd", [NSThread currentThread].name, self.num);
                    break;
                }
            }
        }
    }
    @end
    
    • NSThread

       线程创建

    /*
    * 参数说明:
    *   第一个参数:目标对象
    *   第二个参数:方法选择器(希望线程执行的方法)
    *   第三个参数:前一个参数(方法)中要传入参数
    * 特点:需要启动线程,可以获得线程对象进行详细设置
    */
    NSThread *thread = [NSThread alloc] initWithTarget:self selector:@selector(func)  object:nil];
    
    // 直接分离出一条子线程
    /*
    * 参数说明:
    *   第一个参数:方法选择器(希望线程执行的方法)
    *   第二个参数:目标对象
    *   第三个参数:第一个参数(方法)中要传入参数
    * 特点:不需要启动线程,无法得到线程对象进行详细设置
    */
    [NSThread detachNewThreadSelector:@selector(func) toTarget:self withObject:nil];
    
    // 开启后台线程
    /*
    * 参数说明:
    *   第一个参数:方法选择器(希望线程执行的方法)
    *   第二个参数:第一个参数(方法)中要传入参数
    * 特点:不需要启动线程,无法得到线程对象进行详细设置
    */
    [self performSelectorInBackground:@selector(func) withObject:nil];
    

       线程启动

    [thread start];
    

       得到主线程和当前线程

    // 得到主线程
    NSThread *mainThread = [NSThread mainThread];
    // 得到当前线程
    NSThread *currentThread = [NSThread currentThread];
    
    // 判断线程是否为主线程
    /*
    * 1.取得当前线程的number,主线程的number == 1
    * 2.透过isMainThread方法
    */
    // 判断number == 1
    [NSThread currentThread].number == 1;
    // isMainThread方法
    [thread isMainThread];
    

       设置线程名称

    thread.name = @"线程1";
    

       设置线程优先级

    // 优先级是介于0~1的数,0代表低优先级,1代表高优先级,默认是0.5
    // 优先级也意味着cpu调用线程的概率,优先级越高,cpu调用的概率就越大
    thread.threadPriority = 1;
    

       线程通信

    // 子线程向主线程通信
    /*
    * 参数说明:
    *   第一个参数:方法选择器(回到主线程要执行什么方法)
    *   第二个参数:前一个参数(方法)中要传递的参数
    *   第三个参数:是否等待该方法执行结束才继续往下执行,YES代表等待
    */
    [self performSelectorOnMainThread:@selector(func) withObject:nil  waitUntilDone:YES];
    
    // 两个线程间通信(不限主线程)
    /*
    * 参数说明:
    *   第一个参数:方法选择器(切换到新线程要执行什么方法)
    *   第二个参数:想要回到的线程
    *   第三个参数:前一个参数(方法)中要传递的参数
    *   第四个参数:是否等待该方法执行结束才继续往下执行,YES代表等待
    */
    [self performSelector:@selector(func) onThread:[NSThread  mainThread] withObject:nil  waitUntilDone:YES];
    

       使用NSThread创建的线程,其生命周期仅限于执行的方法,当方法执行结束后,该线程会被释放。

    • GCD

       GCD的核心概念
          1. 任务:要做什么
          2. 队列:存放任务
       使用步骤
          1. 创建队列

    // 并发队列
    /*
    * 参数说明:
    *   第一个参数:队列的名字,C语言字符串
    *   第二个参数:队列类型,这里是并发队列
    * 自动开启多线程,同时执行任务
    * 开多少条线程不是由任务的数量决定,是GCD内部自己决定的
    * 仅在异步函数才有效
    */
    dispatch_queue_t  queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
    
    //全局并发队列
    /*
    * 参数说明:
    *   第一个参数:优先级,一般传入默认的优先级即可
    *   第二个参数:未来接口预留参数,现在传0即可
    */
    dispatch_queue_t  queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 串行队列
    /*
    * 参数说明:
    *   第一个参数:队列的名字,C语言字符串
    *   第二个参数:队列类型,这里是串行队列,默认是串行,如果写NULL也是串行
    * 任务必须一个接着一个执行
    */
    dispatch_queue_t  queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
    
    // 主队列,特殊的串行队列
    /*
    * 与主线程相关联,凡是放在主队列里的任务都要在主线程中串行执行
    * 当同步函数和主队列一起使用时会发生死锁,因为同步函数要等到dispatch_sync函数执行结束才往下运行,而被压进主队列的任务有会马上被拿出来给主线程执行,此时主线程就会出现死锁的情况。
    */
    dispatch_queue_t  queue = dispatch_get_main_queue();
    

          2. 封装任务

    // 使用函数来封装任务
    // 同步
    /*
    * 参数说明:
    *   第一个参数:队列
    *   第二个参数:想要封装的任务
    * 只能在当前线程中执行任务,不具备开启新线程的能力
    * 必须等待当前任务完成,才能执行下面的任务
    */
    dispatch_sync(dispatch_queue_t  queue, dispatch_block_t block);
    
    // 异步
    /*
    * 参数说明:
    *   第一个参数:队列
    *   第二个参数:想要封装的任务
    * 可以在新的线程中执行任务,具备开启新线程的能力
    * 不必等待当前任务完成,可以直接执行下面的任务
    */
    dispatch_async(dispatch_queue_t  queue, dispatch_block_t block);
    

       线程间通信(透过嵌套执行)

    dispatch_queue_t  queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        // 在子线程进行某些操作
        NSLog(@"%@",[NSThread currentThread]);
        ...
        dispatch_async(dispatch_get_main_queue(), ^{
           // 传递数据到主线程处理
           ...
           NSLog(@"%@",[NSThread currentThread]);
        });
    });
    

       其他常用函数

    // 一次性代码:整个程序运行过程中只会执行一次,线程安全的(内部已加锁)
    // 可以应用在单例模式
    // 实现原理:透过判断onceToken的值来决定是否执行,onceToken==0代表没有执行过
    static  dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"Once----");
    });
    
    // 延迟执行
    /*
    * 参数说明:
    *   第一个参数:设置延迟时间(GCD的时间单位是ns)
    *   第二个参数:队列(决定任务在哪个队列执行)
    *   第三个参数:设置任务
    */
    // 实现原理:先等两秒,再将任务放到队列
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@", [NSThread currentThread]);
    });
    
    // 快速迭代
    /*
    * 参数说明:
    *   第一个参数:遍历的次数
    *   第二个参数:队列(不可以使用主队列,会发生死锁;如果使用普通的串行队列,则只会在主线程执行)
    *   第三个参数:设置任务,这个block需要接受一个参数,类似于for循环的int i
    * 会开启多条子线程和主线程并发的执行任务。
    */
    dispatch_queue_t  queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(10, queue, ^(size_t i){
        NSLog(@"%zd-----%@", i, [NSThread currentThread]);
    });
    
    // 栅栏函数
    // 由于异步执行时的顺序不能保证,有时候我们希望先执行某些异步操作后,先执行某个任务再执行后续任务,这时候需要栅栏函数来进行拦截。
    // 下面的代码段能够保证先并发执行打印1、2(1、2顺序不保证),然后打印stop,再并发执行打印3、4(3、4顺序不保证)
    // 不能使用全局并发队列(不能拦截)
    dispatch_queue_t  queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"1-------%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"2-------%@",[NSThread currentThread]);
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"---stop---%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"3-------%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"4-------%@",[NSThread currentThread]);
    });
    

       队列组

    // 使用队列组可以监听任务的执行情况
    // 下面代码实现所有打印数字结束后(打印数字的任务是并发执行,顺序不保证),再打印stop
    // 创建队列组和队列
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue1 = dispatch_queue_create("test1.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("test2.queue", DISPATCH_QUEUE_CONCURRENT);
    
    // 封装任务,添加到队列并监听任务的执行情况
    // dispatch_group_async函数是对dispatch_group_enter、dispatch_async、dispatch_group_leave三个函数的封装,需要注意的是dispatch_group_leave必须写在dispatch_async的block中,具体封装如下:
    //dispatch_group_enter(group);
    //dispatch_async(queue1, ^{
    //    NSLog(@"1-------%@",[NSThread currentThread]);
    //    dispatch_group_leave(group);
    //});
    dispatch_group_async(group, queue1, ^{
        NSLog(@"1-------%@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue1, ^{
        NSLog(@"2-------%@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue1, ^{
        NSLog(@"3-------%@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue2, ^{
        NSLog(@"4-------%@",[NSThread currentThread]);
    });
    dispatch_group_async(group, queue2, ^{
        NSLog(@"5-------%@",[NSThread currentThread]);
    });
    
    // 拦截通知,等待所有任务执行完毕才执行,这个函数的queue1只是决定block里任务放在哪个队列中
    // dispatch_group_notify内部是异步执行
    dispatch_group_notify(group, queue1, ^{
        NSLog(@"---stop---%@",[NSThread currentThread]);
    });
    
    • NSOperation

       NSOperation的核心概念
          1. NSOperation:操作,抽象类,只能将操作封装到其子类中。
          2. NSOperationQueue:队列
       使用步骤
          1. 创建队列

    // 自定义队列:并发队列,可以设定成串行队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 主队列:串行队列,和主线程相关
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
    

          2. 将要执行的操作封装到一个NSOperation对象中

    // NSInvocationOperation
    // 封装操作对象
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(func)  object:nil];
    // 执行操作,压入队列不需要执行操作,由队列自动调用,如果直接执行会在当前线程执行,不会开启新的线程
    [operation start];
    
    // NSBlockOperation
    // 封装操作对象
    NSBlockOperation *operation = [NSBlockOperation blockWithOperation:^{
        NSLog(@"1-----%@", [NSThread currentThread]);
    }];
    // 追加任务,当一个操作对象中的任务数量大于1的时候,会开启新的线程来执行`
    [operation addExecutionBlock:^{
        NSLog(@"2----%@", [NSThread currentThread]);
    }];
    
    // 执行操作,压入队列不需要执行操作,由队列自动调用,如果直接执行会在当前线程执行,不会开启新的线程
    [operation start];
    

          3. 将NSOperation对象添加到NSOperationQueue中

    [queue addOperation:operation];
    // 使用NSBlockOperation有较为简便的写法,不需要前面的封装操作
    // 实现原理:先将block封装成NSBlockOperation,再将NSBlockOperation压入队列
    [queue addObjectWithBlock:^{
        NSLog(@"1-----%@", [NSThread currentThread]);
    }];
    

             设置队列的最大并发数量

    // num为数字,当设置成1的时候相当于串行队列,但线程数可能不为1,只是代表同时只有一个线程在运行
    // 默认为-1,代表系统认为要开多少就开多少,不受限制
    // 设置最大并发数==1时,只能控制单任务的操作顺序执行,如果一个操作中有追加的任务,会不受最大并发数的影响
    queue.maxConcurrentOperationCount = num;
    

             队列的挂起和取消

    // 暂停队列,要等到当前操作结束才能暂停,当前操作不可分割
    [queue setSuspended:YES];
    // 恢复队列
    [queue setSuspended:NO];
    // 取消队列,只能取消队列中等待执行的操作,正在执行的操作不能中断
    // 如果想要取消当前操作,可以在操作中判断isCancelled属性,因为cancelAllOperations方法会修改所有队列中操作的的isCancelled属性
    [queue cancelAllOperations];
    

             自定义NSOperation子类

    // 继承自NSOperation
    // 重写main方法
    // 使用的时候可以直接用alloc
    // 好处:可以复用大量操作
    // 需要注意的是,当main中执行了一段耗时任务时,建议判断一下isCancelled属性
    - (void)main
    {
        NSLog(@"main----%@", [NSThread currentThread]);
    }
    

             操作队列的依赖和监听

    // operation1依赖于operation2,代表operation2先于operation1执行
    // 设置依赖必须在添加到队列前设置
    // 不能设置循环依赖
    // 可以设置跨队列依赖
    [operation1 addDependency:operation2];
    
    // 监听任务执行完毕
    operation.completionBlock = ^{
        NSLog(@"---Finish---");
    };
    

             线程中通信

    NSBlockOperation *operation = [NSBlockOperation blockWithOperation:^{
        // 执行某些任务
        NSLog(@"1-----%@", [NSThread currentThread]);
        ...
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // 主线程内执行某些任务`
            NSLog(@"main-----%@", [NSThread currentThread]);
            ...
        }];
    }];
    
    // 把操作添加到队列
    [queue addOperation:operation];
    
    • GCD和NSOperation的比较

    1. GCD是C语言的API,NSOperation是Objective-C的对象。
    2. 在GCD中,任务用block封装,是一个轻量级的数据结构;NSOperation的操作用NSOperation封装,是一个重量级的数据结构。
    3. NSOperationQueue可以取消操作,GCD则无法。
    4. NSOperation可以指定依赖关系。
    5. NSOperation可以通过KVO对NSOperation对象进行控制。
    6. NSOperation可以制定操作的优先级。
    7. 可以自定义NSOperation来实现操作复用。

    相关文章

      网友评论

        本文标题:iOS多线程编程介绍

        本文链接:https://www.haomeiwen.com/subject/sokfsktx.html