iOS 多线程 浅述

什么是进程?

  • 进程是指在系统中正在运行的一个应用程序。
  • 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。

什么是线程?


  • 1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程)。
  • 线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行。

小拓展

- 线程的串行(就像烤串一样)
    - 1个线程中任务的执行是串行的。
    - 如果要在1个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务。
    - 在`同一时间内`,1个线程只能执行1个任务。

什么是多线程?

  • 1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务。

  • 线程的并行(同时执行)

    • 比如同时开启3条线程分别下载3个文件(分别是文件A、文件B、文件C。
  • 多线程并发执行的原理:

    • 在同一时间里,CPU只能处理1条线程,只有1条线程在工作(执行)。
    • 多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。(如下图)
    CPU调用线程

多线程优缺点:

  • 优点
    • 能适当提高程序的执行效率。
    • 能适当提高资源利用率(CPU、内存利用率)
  • 缺点
    • 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能。
    • 线程越多,CPU在调度线程上的开销就越大。
    • 程序设计更加复杂:比如线程之间的通信、多线程的数据共享

多线程在iOS开发中的应用

- 主线程
    - 一个iOS程序运行后,默认会在自己的进程中开启1条线程,称为“主线程”也叫“UI线程”。
    - 作用:刷新显示UI,处理UI事件。
- 使用注意
    - 不要将耗时操作放到主线程中去处理,因为会卡住主线程,造成UI卡顿(用户体验差)。
    - 和UI相关的刷新操作`必须`放到主线程中进行处理。

线程的状态

  • 线程的各种状态:新建-就绪-运行-阻塞-死亡
  • 常用的控制线程状态的方法
        [NSThread exit];//退出当前线程
        [NSThread sleepForTimeInterval:7.0];//阻塞线程
        [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:7.0]];//阻塞线程
    
    

    注意:线程死亡后不能复生


线程安全:

  • 前提:多个线程同时访问同一块资源会发生数据安全问题 解决方案:加互斥锁
  • 相关代码:@synchronized(self){}
  • 专业术语-线程同步
  • 原子和非原子属性(是否对setter方法加锁)

IOS中多线程的实现方案

方案 简介 语言 线程生命周期 使用频率
pthread 一套通用的多线程API
(跨平台\可移植)
C语言 程序员管理 几乎不用
NSThread 使用更加面向对象
(简单易用,可直接操作线程对象)
OC语言 程序员管理 偶尔使用
GCD 为了替代NSThread为生
充分利用设备多核
C语言 系统自动管理 经常使用
NSOperation 基于GCD
更加面向对象 更方便地设置线程之间的依赖 监听线程状态KVO
OC语言 系统自动管理 经常使用

pthread简单使用

1.包含头文件(必须)

#import <pthread.h>

2.创建线程

//  创建线程
/**
     *
     * 参数一:线程对象(传地址)
     * 参数二:线程的属性(名称\优先级)
     * 参数三:只想函数的指针
     * 参数四:函数需要接受的字符串参数,可以不传递(注:由于我们创建的是OC的字符串,所以在传值的时候需要将其转换成C的字符串)
     */
    pthread_t thread;
    NSString *num = @"123";
    pthread_create(&thread, NULL, task, (__bridge void *)(num));

3.定义参数所需要的函数指针


void *task(void *num)
{
    NSLog(@"当前线程 -- %@,传入的参数:-- %@", [NSThread currentThread], num);

    return NULL;
}

如果需要退出线程的话只需调用下面代码

pthread_exit(NULL);

运行结果:


pthread线程使用截图

NSThread简单使用

这边介绍NSThread创建线程的4种方式:

  • 第一种 (alloc nitWithTarget:selector:object:)
    • 特点:需要手动开启线程,可以拿到线程对象进行详细设置
    • 优缺点:
      • 缺点:需要手动开启线程执行任务
      • 优点:可以拿到线程对象
//  创建线程
    /**
     * 参数一:目标对象
     * 参数二:方法选择器(线程启动后调用的方法)
     * 参数三:调用方法需要接受的参数
     */
    NSThread *thread = [[NSThread alloc] initWithTarget:self
                                               selector:@selector(task)
                                                 object:nil];

    //  开始执行
    [thread start];
  • 第二种(分离出一条子线程)
    • 特点:自动启动线程,无法对线程进行更详细的设置
    • 优缺点:
      • 缺点:无法拿到线程对象 进行更详细设置
      • 优点:代码简单且自动开启线程执行
//  创建线程
    /**
     * 参数一:要调用的方法
     * 参数二:目标对象 self
     * 参数三:调用方法需传递的参数
     */
    [NSThread detachNewThreadSelector:@selector(task)
                             toTarget:self
                           withObject:nil];
  • 第三种(后台线程)
    • 特点:自动启动线程,无法进行更详细设置
    • 优缺点:
      • 缺点:无法拿到线程对象 进行更详细设置
      • 优点:代码简单且自动开启线程执行

/**
 *  NSThread创建一条后台线程
 */
- (void)nsthreadTest3
{
    //  创建线程
    /**
     * 参数一:要调用的方法
     * 参数二:调用方法需传递的参数
     */
    [self performSelectorInBackground:@selector(run:) withObject:@"后台线程"];

}

- (void)run:(NSString *)str
{
    NSLog(@"当前线程:%@ -- 接收到的参数:%@", [NSThread currentThread], str);
}

  • 第四种(自定义NSThread类并重写内部的方法实现)
    • 特点:可以不暴露一些实现细节,使代码增加隐蔽性。(一般出现在第三方框架内)
    • 优缺点:
      • 缺点:繁琐,且需要手动开启线程执行
      • 优点:增加代码隐蔽性

1.创建自定义类继承自NSThread
2.重写NSThread类中的main方法

- (void)main
{
    NSLog(@"当前线程--%@", [NSThread currentThread]);
}

3.创建线程对象

/**
 *  NSThread创建一条后台线程
 */
- (void)nsthreadTest4
{
    //  创建线程
    SJThread *thread = [[SJThread alloc] init];

    //  开启执行
    [thread start];
}

线程间通信

有时候我们会从服务器上下载图片然后再展示出来,下载的操作我们会放到子线程,而UI刷新的操作只能在主线程中执行。这样就涉及到线程间的通信。接下来我们分三种方式来简单实现一下:

  • 方式一:
- (void)viewDidLoad {
    [super viewDidLoad];

    // 开启一条线程下载图片
    [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];

}


- (void)downloadImage
{
    //  网络图片url
    NSURL *url = [NSURL URLWithString:@"http://img3.imgtn.bdimg.com/it/u=3841157212,2135341815&fm=206&gp=0.jpg"];
    //  根据url下载图片数据到本地
    NSData *imageData = [NSData dataWithContentsOfURL:url];
    //  把下载到本地的二进制数据转成图片
    UIImage *image = [UIImage imageWithData:imageData];
    //  回到主线程刷新UI
    //  第一种方式
    [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];

    //  第二种方式
    //  直接调用iconView里面的setImage:方法就可以实现刷新
//    [self.iconView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];

    //  第三种方式
    //  此方法可以方便自由在主线程和其它线程切换
//    [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
}

- (void)showImage:(UIImage *)image
{
    self.iconView.image = image;
}


GCD简单使用

什么是GCD

  • GCD全称是Grand Central Dispatch(牛逼的中枢调度器)
  • 纯C语言,提供了非常多强大的函数

GCD优势

  • GCD是苹果公司为多核的并行运算提出的解决方案
  • GCD会自动利用更多的CPU内核
  • GCD会自动关了线程生命周期(创建、调度、销毁线程)
  • GCD性能很好(接近底层)

GCD的组合方式

  • 异步函数+并发队列:开启多条线程,并发执行任务
  • 异步函数+串行队列:开启一条线程,串行执行任务
  • 同步函数+并发队列:不开线程,串行执行任务
  • 同步函数+串行队列:不开线程,串行执行任务
  • 异步函数+主队列:不开线程,在主线程中串行执行任务
  • 同步函数+主队列:不开线程,串行执行任务(注意死锁发生

注意同步函数和异步函数在执行顺序上面的差异

GCD的任务和队列

  • 任务:执行什么操作
  • 队列:用来存放任务(GCD中提供了2种队列)
    • 串行队列
    • 并发队列

GCD的使用

  • 定制任务 —— 确定需要做的操作
  • 将任务添加到队列中
  • GCD会自动将队列中的任务取出,存放到线程中执行
  • 任务的取出遵循队列的FIFO原则(先进先出,后进后出)
FIFO原则图片

GCD创建线程

  • 接下来看看同步函数和异步函数有什么区别:

1.先来看看异步并发队列

- (void)test
{
    /**
     * 参数一:C语言的字符串,给队列起一个名字或标识
     * 参数二:队列类型
        DISPATCH_QUEUE_CONCURRENT   并发
        DISPATCH_QUEUE_SERIAL   串行
     */
    dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);

    /**
     *  使用函数封装任务
     * 参数一:获取队列
     * 参数二:需要执行的任务
     */
    dispatch_async(queue, ^{
        NSLog(@"在:%@线程执行了任务",[NSThread currentThread]);
    });

    NSLog(@"结束");
}

执行结果:


异步并发队列截图

2.再来看看同步并发队列

- (void)test
{
    /**
     * 参数一:C语言的字符串,给队列起一个名字或标识
     * 参数二:队列类型
        DISPATCH_QUEUE_CONCURRENT   并发
        DISPATCH_QUEUE_SERIAL   串行(串行队列可以用NULL表示)
     */
    dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);

    /**
     *  使用函数封装任务
     * 参数一:获取队列
     * 参数二:需要执行的任务
     */
    dispatch_sync(queue, ^{
        NSLog(@"在:%@线程执行了任务",[NSThread currentThread]);
    });

    NSLog(@"结束");
}

执行结果:


同步并发队列截图

结论:

从上面的2个运行结果的时间可以看出
1.异步并发队列,会开启一条子线程来处理任务,以达到主线程和子线程同时执行的并发效果。
2.同步并发队列,不会开线程,必须等block块中的代码先执行完毕才会继续执行以外的任务,所以并发队列对于同步函数来说等同于“无效”

  • 再看看并发队列对异步函数和同步函数的影响:

1.同步函数+并发队列

dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);

    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });

执行结果:同步函数+并发队列没有开启子线程的能力

并发队列对同步函数的影响

2.异步函数+并发队列

- (void)test2
{
    dispatch_queue_t queue = dispatch_queue_create("并发队列", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
}

执行结果:异步函数+并发队列会自动开启3条子线程执行任务

并发队列对异步函数的影响

结论:

从上面可以看出,异步函数拥有开启子线程的能力,而同步函数没有开启子线程的能力。


  • GCD中,除了并发队列外,还有串行队列,我们来看看如果把并发队列换成串行队列会有怎样的变化

1.同步函数+串行队列

- (void)test2
{
    dispatch_queue_t queue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
}

执行结果:进一步证明同步函数没有开启子线程的能力,他的所有任务都在主线程中执行

同步函数+串行队列

2.异步函数+串行队列

- (void)test2
{
    dispatch_queue_t queue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"当前线程:%@",[NSThread currentThread]);
    });
}

执行结果:开启了一条子线程,在子线程中依次执行任务

异步函数+串行队列

结论

1.在同步函数+串行队列中,任务依旧是在主线程中执行。

2.在异步函数+串行队列中,会自动开启一条子线程,在子线程中依次执行任务

3.再一次证明同步函数没有开启子线程的能力


系统提供的4个全局并发队列

  • 在iOS中系统默认给我们提供了4个全局并发队列
- (void)test3
{
    //  获取全局并发队列
    //  系统内部默认提供4个全局并发队列
    /**
     * 参数一:优先级
     * 参数二:时间(传0即可)
     */
//优先级:DISPATCH_QUEUE_PRIORITY_HIGH 2
//      DISPATCH_QUEUE_PRIORITY_DEFAULT 0
//      DISPATCH_QUEUE_PRIORITY_LOW (-2)
//      DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN    级别最低

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_async(queue, ^{
        NSLog(@"1当前线程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"2当前线程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"3当前线程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"4当前线程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"5当前线程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"6当前线程:%@", [NSThread currentThread]);
    });
}

执行结果:在结果中我们看到GCD创建了6条线程,但是实际上GCD创建多少条线程完全由系统当前情况而定,我们是无法控制的。

获取全局队列

特殊的串行队列 —— 主队列(与主线程相关联的队列)

  • 主队列是GCD自带的一种特殊的串行队列
  • 放在主队列中的人物,都会放到主线程中执行
  • 使用dispatch_get_main_queue()的方式可获取主队列
    • 特点
      • 1.放在主队列中的任务,必须在主线程中执行
      • 2.主队列执行任务的时候,在调度任务的时候,会先调用主线程的状态,如果当前有任务在做,则会等待主线程执行完任务再执行自己的任务

1.主队列+异步函数

- (void)test4
{
    //  获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();

    //  添加任务
    dispatch_async(queue, ^{
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    });
}

执行结果:任务都在主线程中执行

主队列+异步函数

2.同步函数+主队列

- (void)test4
{
    //  获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();

    //  添加任务
    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    });
}

执行结果:
进入死锁状态,因为主队列执行任务的时候,在调度任务的时候,会先调用主线程的状态,如果当前有任务在做,则会等待主线程执行完任务再执行自己的任务

如果要解决以上的情况,那么可以将任务添加到子线程中,这样就不会出现死锁的情况,程序也就能够正常执行了


[self performSelectorInBackground:@selector(test4) withObject:nil];

- (void)test4
{
    //  获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();

    //  添加任务
    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    });
}

执行结果:

死锁解决方式

总结

函数类型 并发队列 手动创建的串行队列 主队列
同步 (sync) 1.没有开启新线程
2.串行执行任务
1.有开启新线程
2.串行执行任务
死锁
异步(async) 1.有开启新线程
2.并发执行任务
1.有开启新线程
2.串行执行任务
1.没有开启新线程
2.串行执行任务

注意

使用sync函数往当前串行队列中添加任务,会卡主当前的串行队列。

GCD线程间的通信

  • 有时候我们需要在子线程进行一些耗时操作,等耗时操作完成后再回到主线程进行相应的UI刷新,那么就可以使用下面的方式在子线程和主线程之间进行通信

- (void)test5
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    dispatch_async(queue, ^{

        NSLog(@"在%@线程中执行任务", [NSThread currentThread]);
        //  回到主线程
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"在%@线程中执行任务", [NSThread currentThread]);
        });
    });
}

执行结果:


线程间通信

GCD延迟执行

  • 特点:可以选择在哪个线程中执行任务
- (void)test6
{
    NSLog(@"方法开始运行");
    /**
     *  GCD延迟执行方法
     *
     *  参数一: 要延迟的时间 (以秒为单位)
     *  参数二: 在哪个线程中执行
     */
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"GCD定时器");
    });
}

执行结果:

GCD延迟执行

一次性代码

  • 特点:
    • 能保证整个程序运行过程中,block内的代码块只会被执行一次
    • 线程是安全的
    • 应用:简单的单例模式(单例模式实现点我)
    • 注意点:不可放在懒加载中

- (void)test8
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"一次性代码运行");
    });
}


栅栏函数

  • 作用:能够控制并发队列里面任务的执行顺序
  • 注意:不能使用全局并发队列(会没有任何区别,文档中有注释——只对自己创建的并发队列有效)
- (void)test7
{
    //  创建队列
    dispatch_queue_t queue = dispatch_queue_create(0, 0);

    dispatch_async(queue, ^{

        for (int i = 0; i<5; i++) {
            NSLog(@"1");
        }
    });
    dispatch_async(queue, ^{

        for (int i = 0; i<5; i++) {
            NSLog(@"2");
        }
    });

    dispatch_barrier_async(queue, ^{
        NSLog(@"进入栅栏函数");
    });

    dispatch_async(queue, ^{
        for (int i = 0; i<5; i++) {
            NSLog(@"3");
        }
    });

    dispatch_async(queue, ^{
        for (int i = 0; i<5; i++) {
            NSLog(@"4");
        }
    });
}

执行结果:

栅栏函数执行结果

GCD迭代开发(遍历)

  • 一般我们传统的遍历方式如下,它的缺点就是在处理比较耗时的操作时效率较低,因为只在一个线程中执行任务。
    //  传统的遍历方式
    for (int i ; i< 10; i++) {
        NSLog(@"%d -- 当前线程%@", i, [NSThread currentThread]);
    }

执行结果:


这里写图片描述
  • 在GCD中,为我们提供了一个迭代函数,可以开启子线程快速进行遍历,这样就可以大大提高效率,而且使用非常简单。接下来使用迭代函数来进行文件复制的操作:
- (void)test9
{
    //  获得文件原始路径(上层文件夹得路径)
    NSString *fromPath = @"/Users/yeshaojian/Desktop/test";

    //  获得文件的目标路径
    NSString *toPath = @"/Users/yeshaojian/Desktop/test2";

    //  得到文件路径下面的所有文件
    NSArray *subpaths =  [[NSFileManager defaultManager] subpathsAtPath:fromPath];
    NSLog(@"文件名:%@",subpaths);

    //  获取数组中文件的个数
    NSInteger count = subpaths.count;

    //  将要迭代的操作放到迭代函数内
    dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t index){
            //  拼接需要复制的文件的全路径
            NSString *fromFullpath = [fromPath stringByAppendingPathComponent:subpaths[index]];
            //  拼接目标目录的全路径
            NSString *toFullpath = [toPath stringByAppendingPathComponent:subpaths[index]];
            //  执行文件剪切操作
            /*
             * 参数一:文件在哪里的全路径
             * 参数二:文件要被剪切到哪里的全路径
             */
            [[NSFileManager defaultManager] moveItemAtPath:fromFullpath toPath:toFullpath error:nil];

           NSLog(@"拼接需要复制的文件的全路径:%@ -- 拼接目标目录的全路径:%@ -- 当前线程:%@",fromFullpath,toFullpath,[NSThread currentThread]);
       });

}

执行结果:


GCD迭代截图

队列组

  • 假如开发中有多个任务,要求在所有任务都在子线程中并发执行,且不能使用栅栏函数,当所有任务都执行完成后打印“完成”。这样的需求就需要用到GCD中的队列组。
  • 应用场合:
    • 对多个任务有强制依赖性,缺一不可时使用

1.队列组的基本使用

- (void)test10
{
    // 获取队列组,用来管理队列
    dispatch_group_t group = dispatch_group_create();

    //  获取并发队列
    dispatch_queue_t queue = dispatch_queue_create("cs", DISPATCH_QUEUE_CONCURRENT);

    //  添加任务
    dispatch_group_async(group, queue, ^{
        NSLog(@"cs1---%@", [NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"cs2---%@", [NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"cs3---%@", [NSThread currentThread]);
    });

    //  拦截通知:当队列组中所有的任务都执行完毕后,会调用下面方法的block块
    dispatch_group_notify(group, queue, ^{
        NSLog(@"完成");
    });
}

执行结果:


队列组截图

队列组函数内部操作简要流程

处理流程:
1.封装任务
2.把任务提交到队列
3.把当前任务的执行情况纳入到队列注的监听范围

注意:下面方法本身是异步的
dispatch_group_notify(group, queue, ^{

    });

拓展:
在一些框架或者早期项目中,可能会见到下面2种队列组的使用方法,在这边顺带提及一下,但不推荐使用,因为太过繁琐。

第一种

- (void)test11
{
    //  获得队列组,管理队列
    dispatch_group_t group = dispatch_group_create();

    //  获得并发队列
    dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);

    //  表示开始把后面的异步任务纳入到监听范围
    //dispatch_group_enter & dispatch_group_leave
    dispatch_group_enter(group);

    //  使用异步函数封装任务
    dispatch_async(queue, ^{
        NSLog(@"1---%@",[NSThread currentThread]);

        //  通知队列组该任务已经执行完毕
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"2---%@",[NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"3---%@",[NSThread currentThread]);
        dispatch_group_leave(group);
    });

    //  拦截通知
    dispatch_group_notify(group, queue, ^{
        NSLog(@"--完成---");
    });
}

第二种


- (void)test11
{
    //  获得队列组,管理队列
    dispatch_group_t group = dispatch_group_create();

    //  获得并发队列
    dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);

    //  表示开始把后面的异步任务纳入到监听范围
    //dispatch_group_enter & dispatch_group_leave
    dispatch_group_enter(group);

    //  使用异步函数封装任务
    dispatch_async(queue, ^{
        NSLog(@"1---%@",[NSThread currentThread]);

        //  通知队列组该任务已经执行完毕
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"2---%@",[NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"3---%@",[NSThread currentThread]);
        dispatch_group_leave(group);
    });

    //  等待DISPATCH_TIME_FOREVER 死等,一直要等到所有的任务都执行完毕之后才会继续往下执行
    //  同步执行
    dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, 0.00001 * NSEC_PER_SEC);

    //  等待timer m的时间 不管队列中的任务有没有执行完毕都继续往下执行,如果在该时间内所有事任务都执行完毕了那么会返回一个0,否则是非0值
    long n =  dispatch_group_wait(group, timer);
    NSLog(@"%ld",n);

    NSLog(@"--完成---");
}


补充:同步\异步函数另一种创建方式

  • 其实同步函数和异步函数还有另外的创建方式,但是使用起来比较不方便,所以上面就没提及,想想还是补充一下好了

1.异步函数(创建一个使用函数封装代码的异步函数)

- (void)test12
{
    /**
     *  参数一:队列
     *  参数二:要传给函数的参数
     *  参数三:函数
     */
    dispatch_async_f(dispatch_get_global_queue(0, 0), NULL, testTask);
}

void testTask(void *param)
{
    NSLog(@"%@", [NSThread currentThread]);
}

2.同步函数(创建一个使用函数封装代码的同步函数)

- (void)test12
{
    /**
     *  参数一:队列
     *  参数二:要传给函数的参数
     *  参数三:函数
     */
    dispatch_sync_f(dispatch_get_global_queue(0, 0), NULL, testTask);
}

void testTask(void *param)
{
    NSLog(@"%@", [NSThread currentThread]);
}

上面使用的是函数来封装要处理的代码,使用比较不方便,且block是轻量级的数据结构,更推荐使用block封装代码的形式创建同步\异步函数。

GCD一些需要注意的细节

  • 全局并发队列是默认存在的(在我们程序运行的时候就存在)
  • 全局队列根据队列的优先级分为 (高,默认,低,后台优先级)4个并发队列
  • iOS 6之前,我们通过创建的线程,是要自己手动施放的
    • 施放的方式 —— dispatch_release()
  • 使用栅栏函数,苹果官方文档明确规定栅栏函数只有在和使用create函数创建的笔法队列一起使用才有效
  • 暂时就想到这么多O(∩_∩)O,因为GCD已经开源,想研究的朋友可以到网上搜索一下,有哪里不对的可以联系我,谢谢!

NSOperation简单使用

NSOperation作用

  • 配合使用NSOperation和NSOperationQueue也能实现多线程编程

NSOperation和NSOperationQueue实现多线程的具体步骤

  • 先将需要执行的操作封装到一个NSOperation对象中
  • 然后将NSOperation对象添加到NSOperation对象中
  • 系统会自动将NSOperationQueue中的NSOperation取出来
  • 将取出来的NSOperation封装的操作放到一条新线程中执行

NSOperation的子类

  • NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类
  • 使用NSOperation子类的方式有3种
    • NSInvocationOperation
    • NSBlockOperation
    • 自定义子类继承NSOperation,实现内部相应的方法

NSOperation封装操作

  • 第一种方式 —— NSInvocationOperation
- (void)invocationTest
{
    /**
     * 参数一:目标对象
     * 参数二:调用方法
     */
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
    
    //  开启任务
    [op1 start];
}

- (void)download
{
    NSLog(@"下载:%@",[NSThread currentThread]);
}

执行结果:需要和队列并用才会开启子线程执行任务


NSInvocationOperation
  • 第二种方式 —— Block
- (void)blockTest
{
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下载:%@",[NSThread currentThread]);
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下载:%@",[NSThread currentThread]);
    }];
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下载:%@",[NSThread currentThread]);
    }];
    
    [op1 addExecutionBlock:^{
        NSLog(@"增加的下载:%@", [NSThread currentThread]);
    }];
    
    //  开启任务
    [op1 start];
    [op2 start];
    [op3 start];
}

- (void)download
{
    NSLog(@"下载:%@",[NSThread currentThread]);
}

执行结果:如果一条线程中执行的操作大于1就会开启新线程并发执行


NSBlockOperation
  • 方式三 —— 自定义NSOperation

1.先创建一个继承自NSOperation的类并重写main方法


- (void)main
{
    NSLog(@"当前线程:%@", [NSThread currentThread]);
}

2.在需要使用的类中引用自定义的类,并创建开启任务

- (void)custom
{
    SJOperation *op1 = [[SJOperation alloc] init];
    
    [op1 start];
}

执行结果:需要手动开启线程或者与队列并用才会开启子线程


自定义NSOperation

NSOperation中的队列

  • 主队列 (获取方式:+mainQueue)
    • 所有在主队列中的任务都在主线程中执行
    • 本质上是串行队列
- (void)invocationQueue
{
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
    
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
    
    NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download3) object:nil];
    
    //  获取主队列
    NSOperationQueue *queue = [NSOperationQueue mainQueue];
    
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];
    
}

执行结果:所有任务都在主队列中执行,且是串行队列

NSInvocationOperation在主队列使用情况
  • 非主队列(获取方式:alloc init)
    • 同时具备并发和串行功能
    • 默认下是并发的
- (void)invocationQueue
{
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
    
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
    
    NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download3) object:nil];
    
    //  获取非主队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];
    
}

执行结果:所有任务在子线程中并发执行

NSInvocationOperation在非主队列使用情况

注意:addOperation:内部已经帮我们执行了开启任务方法,所有不需要另外实现。


NSBlockOperation与队列并用的简单写法

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下载:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下载:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下载:%@",[NSThread currentThread]);
    }];

执行结果:所有任务都在子线程中并发执行

NSBlockOperation在队列中的简单写法

设置最大并发数

  • 在NSOperation中,我们要想控制串行队列或者并发队列,只需要设置maxConcurrentOperationCount属性即可
    • 一般我们要使用串行队列,只需设置值为1即可
    • 如果值大于1,则为并发队列

1.串行队列示例

- (void)blockQueue
{
    //  创建非主队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //  设置最大并发数为1,则队列为串行队列
    queue.maxConcurrentOperationCount = 1;
    
    [queue addOperationWithBlock:^{
        NSLog(@"下载1:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下载2:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下载3:%@",[NSThread currentThread]);
    }];
    
}

执行结果:按照任务添加顺序执行,所以是串行队列


NSOperationQueue串行执行

2.并发队列示例

- (void)blockQueue
{
    //  创建非主队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //  设置最大并发数为6,一般子线程控制在6以内,太多线程会使设备压力过大
    queue.maxConcurrentOperationCount = 6;
    
    [queue addOperationWithBlock:^{
        NSLog(@"下载1:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下载2:%@",[NSThread currentThread]);
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"下载3:%@",[NSThread currentThread]);
    }];
    
}

执行结果:程序并没有按照添加顺序完成任务,所以是并发执行

NSOperationQueue并发执行

注意:

  1. 一般子线程控制在6以内,太多线程会使设备压力过大
  2. maxConcurrentOperationCount默认值为-1(在计算机中,-1一般指最大值)
  3. 如果将maxConcurrentOperationCount设置为0,说明同一时间内执行0个任务,所以任务将不会执行。
maxConcurrentOperationCount系统默认值

NSOperation暂停、恢复和取消功能

  • 在NSOperation中,已经为我们提供了暂停、恢复和取消的功能,我们只需调用相应的方法即可。

1.暂停

    //  暂停
    [queue setSuspended:YES];

2.恢复

    //  取消
    [queue setSuspended:NO];

3.取消

    //  取消队列中所有操作,且取消后的任务不可恢复
    [queue cancelAllOperations];

注意:

1.队列中的的任务是有状态的,分别是 —— 等待;执行;完成三种状态,且暂停、恢复和取消操作并不能作用于当前正处于执行状态的任务,只能作用于等待状态的任务。
2.如果是自定义的NSOperation,会发现暂停、恢复操作对其无效,对于这种情况,可以用以下方式解决 —— 使用取消操作

- (void)main
{
    //  模拟耗时操作
    for (int i = 0; i< 200; i++) {
        NSLog(@"1当前线程:%@", [NSThread currentThread]);
    }
    //  判断当前状态,如果已经取消,直接返回
    if (self.cancelled) return;
    
    //  模拟耗时操作
    for (int i = 0; i< 200; i++) {
        NSLog(@"2当前线程:%@", [NSThread currentThread]);
    }
    //  判断当前状态,如果已经取消,直接返回
    if (self.cancelled) return;
    
    //  模拟耗时操作
    for (int i = 0; i< 200; i++) {
        NSLog(@"3当前线程:%@", [NSThread currentThread]);
    }
    //  判断当前状态,如果已经取消,直接返回
    if (self.cancelled) return;
}

解决问题思路:其实这是苹果官方文档中的建议 —— 因为,当我们调用cancelAllOperations:方法的时候,他内部的cancelled属性就会为真,每执行完一个耗时操作后都进行一次判断,如果发现已经取消,则退出执行。如果想更精确操控的话,也可以将判断操作放到耗时操作中,但是不建议这样做,因为这样性能极差。


NSOperation中的依赖操作

  • NSOperation提供了一套非常便捷好用的操作依赖方式,比起GCD,那种酸爽简直不敢相信
- (void)blockQueue
{
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下载1:%@",[NSThread currentThread]);
    }];
    
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下载2:%@",[NSThread currentThread]);
    }];
    
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下载3:%@",[NSThread currentThread]);
    }];
    
    //  设置依赖关系
    //  op1依赖op2,只有当op2执行完毕后,才会执行op1
    [op1 addDependency:op2];
    //  op2依赖op3,只有当op3执行完毕后,才会执行op2
    [op2 addDependency:op3];
    
    //  获取主队列
    NSOperationQueue *queue = [NSOperationQueue mainQueue];
    
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];

执行结果:先执行完op3,等op3执行完成后才执行op2,当op2执行完毕后,才执行op1

NSOperation操作依赖

注意

  • NSOperation提供的操作依赖功能特别强大,可以设置不同队列的依赖
  • 但是不能循环依赖,比如op1依赖op2,op2又依赖op1,而且并不会报错,但会发生死锁,且有关任务都不执行。

NSOperation的监听

  • 我们经常有这样的需要:在某些任务执行完成后,再执行指定的某些操作,那么NSOperation中的监听功能就派上用场了,使用非常简单
    NSOperation *op = [[NSOperation alloc] init];
    
    op.completionBlock = ^{
        NSLog(@"下载完成");
    };

    [op start];

NSOperation线程间通信

  • NSOperation线程间的通信类似于GCD,所以就不多叙述了,直接上代码
- (void)downloadPhoto
{
    //  获取非主队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    //  创建下载任务
    [queue addOperationWithBlock:^{
       
        //  图片地址
        NSURL *url = [NSURL URLWithString:@"http://cdn.duitang.com/uploads/item/201512/05/20151205092106_aksZU.jpeg"];
        //  下载图片
        NSData *imageData = [NSData dataWithContentsOfURL:url];
        //  转换图片
        UIImage *image = [UIImage imageWithData:imageData];
        //  回到主线程刷新
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            
            NSLog(@"回%@线程刷新UI", [NSThread currentThread]);
            
            self.imageView.image = image;
        }];
        
    }];
}

执行结果:

NSOperation线程间通信.gif

GCD和NSOperation区别

在开发中最常用的就是GCD和NSOperation来进行多线程开发,NSThread更多是在测试时辅助使用,pthread则很少看见,这里为大家简单整理一下他们之间的区别

  • GCD和NSOperation的对比

    • GCD是纯C语言的API,而操作队列(NSOperation)则是Object-C的对象
    • 在GCD中,任务用Block块来表示,而块是轻量级的数据结构,相反,操作队列(NSOperation)中的操作NSOperation是比较重量级的Object-C对象
  • 那么在开发中如何选择呢?

    • 一般如果任务之间有依赖关系或者需要监听任务执行的过程(KVO),首选NSOperation
    • 单纯进行一些耗时操作则选用GCD,因为相比NSOperation,GCD效率更高,性能更好
  • NSOperation和NSOperationQueue好处

    • NSOperation可以方便设置操作优先级(表示操作在队列中与其它操作之间的优先关系,级别越高越先执行)
    • NSOperation可以通过KVO的方式对NSOperation对象进行监听控制(监听当前操作是处于完成,取消还是执行状态)
    • NSOperation可以方便设置操作之间的依赖关系
    • 通过自定义NSOperation子类可以实现操作复用
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,504评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,898评论 1 300
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,218评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,322评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,693评论 3 290
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,812评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,747评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,476评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,700评论 2 251
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,190评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,541评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,206评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,129评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,903评论 0 199
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,894评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,748评论 2 274

推荐阅读更多精彩内容