iOS多线程

进程与线程

进程:计算机操作系统分配资源的单位,是指系统中正在运行的应用程序,进程之间相互独立,运行在受保护的内存空间,比如同时打开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:@"//upload-images.jianshu.io/upload_images/2301429-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://upload-images.jianshu.io/upload_images/1689172-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://upload-images.jianshu.io/upload_images/1689172-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;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容