美文网首页
iOS多线程

iOS多线程

作者: 三十六_ | 来源:发表于2018-07-27 23:44 被阅读0次

    进程与线程

    进程:计算机操作系统分配资源的单位,是指系统中正在运行的应用程序,进程之间相互独立,运行在受保护的内存空间,比如同时打开XCode、QQ,系统就会启动两个进程;

    线程:进程的基本执行单元,一个进程中的任务都在线程中执行;

    并发与并行

    并发:并发的关键是具有处理多个任务的能力,不一定要同时;

    并行:并行的关键是你有同时处理多个任务的能力。

    你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
    你吃饭吃到一半,电话来了,你一手筷子,一手电话,说一句话,咽一口饭。这说明你支持并发。
    你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这就需要两张嘴,也就是多核CPU,这说明你支持并行。

    同步与异步

    同步方法就是我们平时调用的哪些方法。因为任何有编程经验的人都知道,比如在第一行调用foo()方法,那么程序运行到第二行的时候,foo方法肯定是执行完了。

    所谓的异步,就是允许在执行某一个任务时,函数立刻返回,但是真正要执行的任务稍后完成。比如我们在点击保存按钮之后,要先把数据写到磁盘,然后更新UI。同步方法就是等到数据保存完再更新UI,而异步则是立刻从保存数据的方法返回并向后执行代码,同时真正用来保存数据的指令将在稍后执行。

    多线程优缺点

    • 优点:能适当提高程序的执行效率,能适当提高资源利用率(CPU、内存利用率)
    • 缺点:创建线程是有开销的,iOS下主要成本包括:内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间
      如果开启大量的线程,会降低程序的性能,线程越多,CPU在调度线程上的开销就越大。
      程序设计更加复杂:比如线程之间的通信、多线程的数据共享等问题。

    iOS中多线程解决方案

    1. pthread

    pthread 是一套通用的多线程的 API,可以在Unix / Linux / Windows 等系统跨平台使用,使用 C 语言编写,需要程序员自己管理线程的生命周期,使用较为复杂,我们在 iOS 开发中几乎不使用 pthread,我们可以稍作了解。

    2. NSThread

    NSThread 是苹果官方提供的,使用起来比 pthread 更加面向对象,简单易用,可以直接操作线程对象。不过也需要需要程序员自己管理线程的生命周期(主要是创建),我们在开发的过程中偶尔使用 NSThread。比如我们会经常调用[NSThread currentThread]来显示当前的进程信息。

    创建方式

    • 先创建再启动

        NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething1:) object:@"NSThread1"];
        [thread1 start];
      
    • 创建线程后自动启动

        [NSThread detachNewThreadSelector:@selector(doSomething2:) toTarget:self withObject:@"NSThread2"];
      
    • 隐式创建线程,直接启动

        [self performSelectorInBackground:@selector(doSomething3:) withObject:@"NSThread3"];
      

    相关方法

    • 类方法

        // 当前线程
        [NSThread currentThread];
        // 打印结果:{number = 1, name = main}
        NSLog(@"%@",[NSThread currentThread]);
        //休眠多久
        [NSThread sleepForTimeInterval:2];
        //休眠到指定时间
        [NSThread sleepUntilDate:[NSDate date]];
        //退出线程
        [NSThread exit];
        //判断当前线程是否为主线程
        [NSThread isMainThread];
        //判断当前线程是否是多线程
        [NSThread isMultiThreaded];
        //主线程的对象
        NSThread *mainThread = [NSThread mainThread];
      
    • 实例方法

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

    线程间通信

    在开发中,线程往往不是孤立存在的,多个线程之间需要经常进行通信我们经常会在子线程进行耗时操作,操作结束后再回到主线程去刷新 UI。这就涉及到了子线程和主线程之间的通信。我们先来了解一下官方关于 NSThread 的线程间通信的方法。

    // 在主线程上执行操作
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
      // equivalent to the first method with kCFRunLoopCommonModes
    
    // 在指定线程上执行操作
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
    
    // 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法
    - (id)performSelector:(SEL)aSelector;
    - (id)performSelector:(SEL)aSelector withObject:(id)object;
    - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
    

    下面通过一个经典的下载图片 DEMO 来展示线程之间的通信。具体步骤如下:

    1.开启一个子线程,在子线程中下载图片。
    2.回到主线程刷新 UI,将图片展示在 UIImageView 中。
    代码如下:

    /**
     * 创建一个线程下载图片
     */
    - (void)downloadImageOnSubThread {
        // 在创建的子线程中调用downloadImage下载图片
        [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
    }
    
    /**
     * 下载图片,下载完之后回到主线程进行 UI 刷新
     */
    - (void)downloadImage {
        NSLog(@"current thread -- %@", [NSThread currentThread]);
        
        // 1. 获取图片 imageUrl
        NSURL *imageUrl = [NSURL URLWithString:@"https://ysc-demo-1254961422.file.myqcloud.com/YSC-phread-NSThread-demo-icon.jpg"];
        
        // 2. 从 imageUrl 中读取数据(下载图片) -- 耗时操作
        NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
        // 通过二进制 data 创建 image
        UIImage *image = [UIImage imageWithData:imageData];
        
        // 3. 回到主线程进行图片赋值和界面刷新
        [self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
    }
    
    /**
     * 回到主线程进行图片赋值和界面刷新
     */
    - (void)refreshOnMainThread:(UIImage *)image {
        NSLog(@"current thread -- %@", [NSThread currentThread]);
        
        // 赋值图片到imageview
        self.imageView.image = image;
    }
    
    线程状态

    线程安全

    多线程安全隐患的原因:一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源。当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。就好比几个人在同一时修改同一个表格,造成数据的错乱。
    解决方法:

    1. 添加互斥锁:

       @synchronized(锁对象) {
       // 需要锁定的代码
        }
      

    iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock等等各种方式。判断的时候锁对象要存在,如果代码中只有一个地方需要加锁,大多都使用self作为锁对象,这样可以避免单独再创建一个锁对象。加了互斥做的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠。

    1. 自旋锁:
      加了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方式,一直等待锁定的代码执行完成。相当于不停尝试执行代码,比较消耗性能。
      属性修饰atomic本身就有一把自旋锁:
    nonatomic 非原子属性,同一时间可以有很多线程读和写
    atomic 原子属性(线程安全),保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值),atomic 本身就有一把锁(自旋锁)
    atomic:线程安全,需要消耗大量的资源
    nonatomic:非线程安全,不过效率更高,一般使用nonatomic
    

    下面通过一个售票实例来看一下锁的作用:

    #import "ViewController.h"
    
    @interface ViewController ()
    
    @property(nonatomic,strong)NSThread *thread01;
    @property(nonatomic,strong)NSThread *thread02;
    @property(nonatomic,strong)NSThread *thread03;
    @property(nonatomic,assign)NSInteger numTicket;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // 总票数为30
        self.numTicket = 30;
        self.thread01 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
        self.thread01.name = @"售票员01";
        self.thread02 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
        self.thread02.name = @"售票员02";
        self.thread03 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
        self.thread03.name = @"售票员03";
    }
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        [self.thread01 start];
        [self.thread02 start];
        [self.thread03 start];
    }
    // 售票
    -(void)saleTicket
    {
        while (1) {
            // 锁对象,本身就是一个对象,所以self就可以了
            // 锁定的时候,其他线程没有办法访问这段代码
            @synchronized (self) {
                // 模拟售票时间,我们让线程休息0.05s 
                [NSThread sleepForTimeInterval:0.05];
                if (self.numTicket > 0) {
                    self.numTicket -= 1;
                    NSLog(@"%@卖出了一张票,还剩下%zd张票",[NSThread currentThread].name,self.numTicket);
                }else{
                    NSLog(@"票已经卖完了");
                    break;
                }
            }
        }
    }
    @end
    
    加锁前

    我们可以看到没有加锁时有的票被多卖了,显然不对,接下来看看加锁的结果:


    加锁后

    加上互斥锁后,就不会出现数据错乱的情况了。

    GCD

    GCD是苹果公司为多核的并行运算提出的解决方案,它可以自动管理线程的生命周期(创建线程、调度任务、销毁线程),我们只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。

    GCD中的任务与队列

    任务:GCD以block为基本单位,一个block中的代码可以为一个任务。
    任务有两种执行方式: 同步函数 和 异步函数,他们之间的区别是:

    • 同步:只能在当前线程中执行任务,不具备开启新线程的能力,任务立刻马上执行,会阻塞当前线程并等待 Block中的任务执行完毕,然后当前线程才会继续往下运行
    • 异步:可以在新的线程中执行任务,具备开启新线程的能力,但不一定会开新线程,当前线程会直接往下执行,不会阻塞当前线程

    队列:装载线程任务的队形结构。(系统以先进先出的方式调度队列中的任务执行)。在GCD中有两种队列:串行队列和并发队列。

    • 串行队列(Serial Dispatch Queue):让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
    • 并发队列(Concurrent Dispatch Queue):可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步(dispatch_async)函数下才有效。

    GCD的使用分为两步:

    1. 添加任务;
    2. 将任务放到指定的队列中,GCD自动将任务取出放到对应的线程中执行。
    GCD的创建
    1. 创建队列
      使用dispatch_queue_create来创建队列对象,传入两个参数,第一个参数表示队列的唯一标识符,可为空。第二个参数用来表示队列的类型,串行队列(DISPATCH_QUEUE_SERIAL)或并发队列(DISPATCH_QUEUE_CONCURRENT)。
      串行队列:
    dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
    

    并发队列:

    dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_CONCURRENT);
    

    全局并发队列:GCD默认已经提供了全局并发队列,供整个应用使用,可以无需手动创建:

     /** 
         第一个参数:优先级 也可直接填后面的数字
         #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
         #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认
         #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
         #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
         第二个参数: 预留参数  0
         */
        dispatch_queue_t quque1 =  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    

    主队列:GCD 提供了的一种特殊的串行队列,主队列负责在主线程上调度任务,如果在主线程上已经有任务正在执行,主队列会等到主线程空闲后再调度任务。通常是返回主线程更新UI的时候使用。dispatch_get_main_queue():

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
          // 耗时操作放在这里
    
          dispatch_async(dispatch_get_main_queue(), ^{
              // 回到主线程进行UI操作
    
          });
      });
    
    1. 执行任务
      同步(Synchronize)使用dispatch_sync;
    /*
         第一个参数:队列
         第二个参数:block,在里面封装任务
         */
        dispatch_sync(queue, ^{
            
        });
    

    异步(asynchronous)使用dispatch_async;

    dispatch_async(queue, ^{
    
        });
    

    GCD的使用:队列和任务的组合

    组合使用

    当在主队列中加入同步函数的时候,会造成死锁。

    //1.获得主队列
    dispatch_queue_t queue =  dispatch_get_main_queue();
    //2.同步函数
    dispatch_sync(queue, ^{
           NSLog(@"---download1---%@",[NSThread currentThread]);
    });
    

    主队列在执行dispatch_sync,随后队列中新增一个任务block。因为主队列是同步队列,所以block要等dispatch_sync执行完才能执行,但是dispatch_sync是同步派发,要等block执行完才算是结束。在主队列中的两个任务互相等待,导致了死锁。
    解决方案:其实在通常情况下我们不必要用dispatch_sync,因为dispatch_async能够更好的利用CPU,提升程序运行速度。只有当我们需要保证队列中的任务必须顺序执行时,才考虑使用dispatch_sync。在使用dispatch_sync的时候应该分析当前处于哪个队列,以及任务会提交到哪个队列。
    注意:GCD中开多少条线程是由系统根据CUP繁忙程度决定的,如果任务很多,GCD会开启适当的子线程,并不会让所有任务同时执行。

    • GCD线程间的通信非常简单,使用同步或异步函数,传入主队列即可(就像上面介绍主队列时那样):
    -(void)downloadImage
    {
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_async(queue, ^{
            // 获得图片URL
            NSURL *url = [NSURL URLWithString:@"https://img.haomeiwen.com/i2301429/d5cc0a007447e469.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
            // 将图片URL下载为二进制文件
            NSData *data = [NSData dataWithContentsOfURL:url];
            // 将二进制文件转化为image
            UIImage *image = [UIImage imageWithData:data];
            NSLog(@"%@",[NSThread currentThread]);
            // 返回主线程 这里用同步函数不会发生死锁,因为这个方法在子线程中被调用。
            // 也可以使用异步函数
            dispatch_sync(dispatch_get_main_queue(), ^{
                self.imageView.image = image;
                NSLog(@"%@",[NSThread currentThread]);
            });
        }); 
    }
    

    GCD其他常用方法

    1. 栅栏函数(控制任务的执行顺序)

    当任务需要异步进行,但是这些任务需要分成两组来执行,第一组完成之后才能进行第二组的操作。这时候就用了到GCD的栅栏方法dispatch_barrier_async:

    -(void)barrier
    {
        //1.创建队列(并发队列)
        dispatch_queue_t queue = dispatch_queue_create("com.xxccqueue", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(queue, ^{
            for (NSInteger i = 0; i<3; i++) {
                NSLog(@"%zd-download1--%@",i,[NSThread currentThread]);
            }
        });
        dispatch_async(queue, ^{
            for (NSInteger i = 0; i<3; i++) {
                NSLog(@"%zd-download2--%@",i,[NSThread currentThread]);
            }
        });
        //栅栏函数
        dispatch_barrier_async(queue, ^{
            NSLog(@"这是一个栅栏函数,34任务在12之后进行");
        });
        dispatch_async(queue, ^{
            for (NSInteger i = 0; i<3; i++) {
                NSLog(@"%zd-download3--%@",i,[NSThread currentThread]);
            }
        });
        dispatch_async(queue, ^{
            for (NSInteger i = 0; i<3; i++) {
                NSLog(@"%zd-download4--%@",i,[NSThread currentThread]);
            }
        });
    }
    
    输出结果
    2. 延迟执行
    /*
         第一个参数:延迟时间
         第二个参数:要执行的代码
         如果想让延迟的代码在子线程中执行,也可以更改在哪个队列中执行 dispatch_get_main_queue() -> dispatch_get_global_queue(0, 0)
         */
         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"---%@",[NSThread currentThread]);
        });
    

    当然,除了GCD以外我们还有其他的方法:

    [self performSelector:@selector(doSomething) withObject:nil afterDelay:2.0];
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
    
    3.使代码只执行一次
    //整个程序运行过程中只会执行一次
    //onceToken用来记录该部分的代码是否被执行过
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"-----");
    });
    

    这个用法一般用在单例模式中。

    4.dispatch_apply(快速迭代)

    dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API,该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等到全部的处理执行结束:

    - (void)dispatchApplyTest1 {
        //生成全局队列
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        /**
         *  @param 10    指定重复次数  指定10次
         *  @param queue 追加对象的Dispatch Queue
         *  @param index 带有参数的Block, index的作用是为了按执行的顺序区分各个Block
         *
         */
        dispatch_apply(10, queue, ^(size_t index) {
            NSLog(@"%zu-----%@", index, [NSThread currentThread]);
        });
        NSLog(@"finished");
    }
    
    打印结果

    可以看到该函数开启了多个线程执行block里的操作,我们可以利用这个特性模拟循环完成快速迭代遍历(无序):

    - (void)dispatchApplyTest2 {
        //1.创建NSArray类对象
        NSArray *array = @[@"a", @"b", @"c", @"d", @"e", @"f", @"g", @"h", @"i", @"j"];
        
        //2.创建一个全局队列
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        //3.通过dispatch_apply函数对NSArray中的全部元素进行处理,并等待处理完成,
        dispatch_apply([array count], queue, ^(size_t index) {
            NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
        });
        NSLog(@"finished");
    }
    
    队列组

    异步执行几个耗时操作,当这几个操作都完成之后再回到主线程进行操作,这是就要用到队列组了,队列组可以用来管理队列中任务的执行。

    // 创建队列组
        dispatch_group_t group = dispatch_group_create();
        // 创建并行队列 
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        // 执行队列组任务
        dispatch_group_async(group, queue, ^{   
        });
        //队列组中的任务执行完毕之后,执行该函数
        dispatch_group_notify(group, queue, ^{
        });
    

    将两张图片分别下载完成后,合成一张图片并显示的例子:

    -(void)GCDGroup
    {
        //下载图片1
        //创建队列组
        dispatch_group_t group =  dispatch_group_create();
        //1.开子线程下载图片
        //创建队列(并发)
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_group_async(group, queue, ^{
            //1.获取url地址
            NSURL *url = [NSURL URLWithString:@"https://img.haomeiwen.com/i1689172/61b8a20c108f539d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
            //2.下载图片
            NSData *data = [NSData dataWithContentsOfURL:url];
            //3.把二进制数据转换成图片
            self.image1 = [UIImage imageWithData:data];
            NSLog(@"1---%@",self.image1);
        });
        //下载图片2
        dispatch_group_async(group, queue, ^{
            //1.获取url地址
            NSURL *url = [NSURL URLWithString:@"https://img.haomeiwen.com/i1689172/2a0505c7992fd970.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
            //2.下载图片
            NSData *data = [NSData dataWithContentsOfURL:url];
            //3.把二进制数据转换成图片
            self.image2 = [UIImage imageWithData:data];
            NSLog(@"2---%@",self.image2);
        });
        //合成,队列组执行完毕之后执行
        dispatch_group_notify(group, queue, ^{
            //开启图形上下文
            UIGraphicsBeginImageContext(CGSizeMake(200, 200));
            //画1
            [self.image1 drawInRect:CGRectMake(0, 0, 200, 100)];
            //画2
            [self.image2 drawInRect:CGRectMake(0, 100, 200, 100)];
            //根据图形上下文拿到图片
            UIImage *image =  UIGraphicsGetImageFromCurrentImageContext();
            //关闭上下文
            UIGraphicsEndImageContext();
            //回到主线程刷新UI
            dispatch_async(dispatch_get_main_queue(), ^{
                self.imageView.image = image;
                NSLog(@"%@--刷新UI",[NSThread currentThread]);
            });
        });
    }
    
    GCD信号量(dispatch_semaphore)

    信号量:就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。其实,这有点类似锁机制了,只不过信号量都是系统帮助我们处理了,我们只需要在执行线程之前,设定一个信号量值,并且在使用时,加上信号量处理方法就行了。主要有三个方法:

    //创建信号量,参数:信号量的初值,如果小于0则会返回NULL
    dispatch_semaphore_create(long value)
     
    //等待,降低信号量
    dispatch_semaphore_wait(dispatch_semaphore_t semaphore, dispatch_time_t timeout)
     
    //提高信号量,这个函数会使传入的信号量dsema的值加1
    dispatch_semaphore_signal(dispatch_semaphore_t semaphore)
    

    关于信号量,可以用停车来比喻。
    停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
    信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait函数就相当于来了一辆车,dispatch_semaphore_signal
    就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,所以就一直等下去。
    我们看个例子,假设现在系统有两个空闲资源可以被利用,但同一时间却有三个线程要进行访问,这时利用GCD信号量代码如下:

    -(void)dispatchSignal{
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
        dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        //任务1
        dispatch_async(quene, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"run task 1");
            sleep(1);
            NSLog(@"complete task 1");
            dispatch_semaphore_signal(semaphore);
        });
        //任务2
        dispatch_async(quene, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"run task 2");
            sleep(1);
            NSLog(@"complete task 2");
            dispatch_semaphore_signal(semaphore);
        });
        //任务3
        dispatch_async(quene, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"run task 3");
            sleep(1);
            NSLog(@"complete task 3");
            dispatch_semaphore_signal(semaphore);
        });
    }
    
    结果

    我们可以看到任务1和任务3首先抢到了这两块资源,有任务完成后才轮到任务二。
    接下来我们还是以售卖车票为例,用信号量怎么实现加锁功能:

    dispatch_semaphore_t semaphore;
    - (void)viewDidLoad {
        [super viewDidLoad];
        // 总票数为30
        self.numTicket = 35;
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [NSThread currentThread].name = @"售票员1";
            [self saleTicket];
        });
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [NSThread currentThread].name = @"售票员2";
            [self saleTicket];
        });
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [NSThread currentThread].name = @"售票员3";
            [self saleTicket];
        });
        semaphore = dispatch_semaphore_create(1);
    }
    
    // 售票
    -(void)saleTicket
    {
        while (1) {
            
            [NSThread sleepForTimeInterval:0.05];
            if (self.numTicket > 0) {
                dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
                self.numTicket -= 1;
                NSLog(@"%@卖出了一张票,还剩下%zd张票",[NSThread currentThread].name,self.numTicket);
            }else{
                NSLog(@"票已经卖完了");
                break;
            }
            dispatch_semaphore_signal(semaphore);
        }
    }
    

    信号量属于底层工具。它非常强大,但在多数需要使用它的场合,最好从设计角度重新考虑,看是否可以不用。应该优先考虑是否可以使用诸如操作队列这样的高级工具。通常可以通过增加一个分派队列dispatch_suspend,或者通过其他方式分解操作来避免使用信号量。信号量并非不好,只是它本身是锁,能不用锁就不要用。尽量用cocoa框架中的高级抽象,信号量非常接近底层。但有时候,例如需要把异步任务转换为同步任务时,信号量是最合适的工具。

    NSOperation

    NSOperation 是苹果公司对 GCD 的封装,完全面向对象,并比GCD多了一些更简单实用的功能。NSOperation需要配合NSOperationQueue来实现多线程。NSOperation 和NSOperationQueue 分别对应 GCD 的 任务 和 队列。
    使用步骤:

    1. 将需要执行的操作封装到一个NSOperation对象中;
    2. 将NSOperation对象添加到NSOperationQueue中,系统会自动将NSOperationQueue中的NSOperation取出来,并将取出的NSOperation封装的操作放到一条新线程中执行。

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

    1. 使用子类NSInvocationOperation
    2. 使用子类NSBlockOperation
    3. 定义继承自NSOperation的子类,通过实现内部相应的方法来封装任务。
    NSOperation 的创建
    1. NSInvocationOperation
    /*
         第一个参数:目标对象
         第二个参数:选择器,要调用的方法
         第三个参数:方法要传递的参数
         */
    NSInvocationOperation *op  = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];
    //启动操作
    [op start];
    
    1. NSBlockOperation
    //1.封装操作
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
           //要执行的操作,在主线程中执行
           NSLog(@"1------%@",[NSThread currentThread]); 
    }];
    //2.追加操作,追加的操作在子线程中执行,可以追加多条操作
    [op addExecutionBlock:^{
            NSLog(@"---download2--%@",[NSThread currentThread]);
        }];
    [op start];
    

    NSBlockOperation 还提供了一个方法 addExecutionBlock:,通过 addExecutionBlock: 就可以为 NSBlockOperation 添加额外的操作。这些操作(包括 blockOperationWithBlock 中的操作)可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完成执行时,才视为完成。如果添加的操作多的话,blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock: 中的操作一定会在当前线程中执行。

    1. 自定义继承自 NSOperation 的子类

    如果使用子类 NSInvocationOperation、NSBlockOperation 不能满足日常需求,我们可以使用自定义继承自 NSOperation 的子类。可以通过重写 main 或者 start 方法 来定义自己的 NSOperation 对象。重写main方法比较简单,我们不需要管理操作的状态属性 isExecuting 和 isFinished。当 main 执行完返回的时候,这个操作就结束了。
    定义:

    // YSCOperation.h 文件
    #import <Foundation/Foundation.h>
    
    @interface JYHOperation : NSOperation
    
    @end
    
    //JYHOperation.m 文件
    #import "JYHOperation.h"
    
    @implementation JYHOperation
    
    - (void)main {
        if (!self.isCancelled) {
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:3];
                NSLog(@"%@", [NSThread currentThread]);
            }
        }
    }
    
    @end
    

    使用

    - (void)useCustomOperation {
        // 1.创建 Operation 对象
        JYHOperation *op = [[JYHOperation alloc] init];
        // 2.调用 start 方法开始执行
        [op start];
    }
    

    在没有使用 NSOperationQueue、在主线程单独使用自定义继承自 NSOperation 的子类以及使用NSInvocationOperation的情况下,是在主线程执行操作,并没有开启新线程,接下来看看怎么将操作添加到队列中去。

    创建NSOperationQueue

    一共有两种队列:

    1. 主队列:通过mainQueue获得,凡是放到主队列中的任务都将在主线程执行;
    2. 非主队列:通过 alloc init创建,非主队列同时具备了并发和串行的功能,通过设置最大并发数属性来控制任务是并发执行还是串行执行

    将操作添加到队列的方式也有两种:

    1. 先创建操作,再将创建好的操作加入到创建好的队列中去:
    -(void)addOperation:(NSOperation *)op;
    
    1. 无需先创建操作,在 block 中添加操作,直接将包含操作的 block 加入到队列中:
    - (void)addOperationWithBlock:(void (^)(void))block;
    

    将操作加入到操作队列后能够开启新线程,并发执行。并且将操作添加到NSOperationQueue中,就会自动启动,不需要再自己启动了。

    NSOperationQueue控制串行、并行

    NSOperationQueue有个关键属性 maxConcurrentOperationCount,叫做最大并发操作数,用来控制一个特定队列中可以有多少个操作同时参与并发执行。

    • maxConcurrentOperationCount默认为-1,直接并发执行,所以加入到‘非队列’中的任务默认就是并发,开启多线程。
    • 当maxConcurrentOperationCount为1时,则表示不开线程,也就是串行。
    • 当maxConcurrentOperationCount大于1时,进行并发执行。
    • 系统对最大并发数有一个限制,所以即使把maxConcurrentOperationCount设置的很大,系统也会自动调整。所以把最大并发数设置的很大是没有意义的。
    • maxConcurrentOperationCount 控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。
    NSOperation 操作依赖

    NSOperation能添加操作之间的依赖关系。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。
    NSOperation 提供管理依赖的接口:

    1. 添加依赖:
    - (void)addDependency:(NSOperation *)op;
    
    1. 移除依赖:
    - (void)removeDependency:(NSOperation *)op;
    

    比如说有 A、B 两个操作,其中 A 执行完操作,B 才能执行操作。
    如果使用依赖来处理的话,那么就需要让操作 B 依赖于操作 A:

    - (void)addDependency {
    
        // 1.创建队列
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
        // 2.创建操作
        NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
                NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
            }
        }];
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
                NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
            }
        }];
    
        // 3.添加依赖
        [op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
    
        // 4.添加操作到队列中
        [queue addOperation:op1];
        [queue addOperation:op2];
    }
    
    NSOperation、NSOperationQueue 常用属性和方法
    • NSOpreation
    // 开启线程
    - (void)start;
    - (void)main;
    // 判断线程是否被取消
    @property (readonly, getter=isCancelled) BOOL cancelled;
    // 取消当前线程
    - (void)cancel;
    //NSOperation任务是否在运行
    @property (readonly, getter=isExecuting) BOOL executing;
    //NSOperation任务是否已结束
    @property (readonly, getter=isFinished) BOOL finished;
    // 添加依赖
    - (void)addDependency:(NSOperation *)op;
    // 移除依赖
    - (void)removeDependency:(NSOperation *)op;
    // 优先级
    typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
        NSOperationQueuePriorityVeryLow = -8L,
        NSOperationQueuePriorityLow = -4L,
        NSOperationQueuePriorityNormal = 0,
        NSOperationQueuePriorityHigh = 4,
        NSOperationQueuePriorityVeryHigh = 8
    };
    // 操作监听
    @property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);
    // 阻塞当前线程,直到该NSOperation结束。可用于线程执行顺序的同步
    - (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);
    // 获取线程的优先级
    @property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);
    // 线程名称
    @property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);
    
    • NSOperationQueue
    // 获取队列中的操作
    @property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
    // 队列中的操作数
    @property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
    // 最大并发数,同一时间最多只能执行三个操作
    @property NSInteger maxConcurrentOperationCount;
    // 暂停 YES:暂停 NO:继续
    @property (getter=isSuspended) BOOL suspended;
    // 取消所有操作
    - (void)cancelAllOperations;
    // 阻塞当前线程直到此队列中的所有任务执行完毕
    - (void)waitUntilAllOperationsAreFinished;
    

    相关文章

      网友评论

          本文标题:iOS多线程

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