多线程 - GCD

1,058 阅读11分钟

本章节主要介绍多线程中使用最频繁的GCD

  1. GCD简介
  2. 函数与队列四大组合(同步、异步、串行、并行)
  3. 性能调度耗能
  4. 面试题
  5. 线程资源共享
  6. 栅栏函数barrier
  7. 调度组 Group
  8. GCD单例
  9. 信号量 semaphore

1 GCD简介

GCD,全称Grand Central Dispatch(中央调度中心),纯C语言开发,提供了很多强大的函数。

  • 优势:
    • GCD是苹果公司为多核并行运算提出的解决方案;
    • GCD会自动利用更多的CPU内核;
    • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。
    • 程序员只需要告诉GCD想要执行的任务,不需要编写任何线程管理相关代码(调度、销毁都不用管)
  • 核心: 将任务添加到队列,并指定执行任务的函数

这里引申出任务、队列、执行任务的函数三个内容。我们一一进行分析

// 任务(block)
    dispatch_block_t block = ^{
        NSLog(@"hello GCD");
    };

    // 队列(此处串行队列)
    dispatch_queue_t queue = dispatch_queue_create("syncTest", DISPATCH_QUEUE_SERIAL);

    // 执行任务的函数(此处异步函数)
    dispatch_async(queue, block);

1.1 任务

GCD的任务是使用block封装的函数,没有入参和返参。 任务创建好后,等待执行任务的函数将其放入队列中。

1.2 队列

GCD的队列包含串行队列和并行队列两种。 串行队列: 同一时刻只允许一个任务执行。 并行队列: 同一时刻允许多个任务执行。

1.3 执行任务的函数

执行任务的函数包括同步函数和异步函数两种:

  • dispatch_sync同步函数:
    • 必须等待当前语句执行完毕,才会执行下一条语句
    • 不会开启线程,就在当前线程执行block任务
  • dispatch_async异步函数:
    • 不用等待当前语句执行完毕,就可以执行下一条语句
    • 会开启线程执行block任务

2 函数与队列四大组合(同步、异步、串行、并行)

  • 主队列dispatch_get_main_queue:

    • 专门用来在主线程上调度任务的串行队列
    • 不会开启线程
    • 如果当前主线程正在执行任务,需要等当前任务执行完,才会继续调度其他任务。
  • 全局并发队列dispatch_get_global_queue:

    • 为了方便程序员的使用,苹果提供了全局队列 (并发队列,实现多线程需求的快捷方式)。
    • 使用多线程开发时,如果对队列没有特殊要求,可直接使用全局队列来执行异步任务

Q & A:

Q: 队列有几种?

 // 串行队列
   dispatch_queue_t serial = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
   // 并行队列
   dispatch_queue_t concurrent = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
   // 主队列(串行队列)
   dispatch_queue_t mainQueue = dispatch_get_main_queue();
   // 全局队列 (并行队列)
   dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

   NSLog(@"\n%@ \n%@ \n%@ \n%@", serial, concurrent, mainQueue, globalQueue);

A:只有串行队列和并行队列两种。

2.1 同步 + 串行 死锁

- (void)mainSyncTest{
    NSLog(@"0 %@", [NSThread currentThread]);
    // 等
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"1 %@", [NSThread currentThread]);
    });
    NSLog(@"2 %@", [NSThread currentThread]);
}

分析:

  • 主队列(main)是串行队列,函数是sync同步函数。属于同步函数&串行队列的情况
  • 在打印0之后,dispatch_sync同步函数将block排队插入mainSyncTest函数最后,等待mainSyncTest函数执行完后再执行。
  • 但是block没有执行,dispatch_sync函数就等于没有完成。程序无法往下执行。
  • 所以造成了dispatch_sync等mainSyncTest执行完后执行block,而mainSyncTest却说dispatch_sync没有执行完.

Q1 :如果上述代码,将同步 + 主队列执行改为同步 + 自定义串行队列,是否会堵塞?

   dispatch_queue_t serial = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
    NSLog(@"0 %@", [NSThread currentThread]);

    dispatch_sync(serial, ^{
        NSLog(@"1 %@", [NSThread currentThread]);
    });
    NSLog(@"2 %@", [NSThread currentThread]);

此处输入图片的描述

总结 虽然将主队列执行改为自定义串行队列,解决了堵塞问题,但是否和syncTest代码本身运行的队列和线程有关呢? [之前堵塞代码] 函数代码和block代码都在主队列+主线程 [新代码]函数代码在自定义队列+主线程

Q2: 将syncTest代码也放在这个自定义队列中执行,此时堵塞是否会出现?

    dispatch_queue_t serial = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serial, ^{
        [self mainSyncTest];
    });
    
- (void)mainSyncTest{
    NSLog(@"0 %@", [NSThread currentThread]);
    // 等
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"1 %@", [NSThread currentThread]);
    });
    NSLog(@"2 %@", [NSThread currentThread]);
}

此处输入图片的描述 总结 我们注意到线程还是在main主线程。所以:同一队列同一线程进行sync同步操作,会阻塞,Crash 主队列是个特殊队列,APP启动时就与主线程完成了线程绑定。不会切换线程。

Q3 :自定义队列切其他自定义队列,是否也可以阻止阻塞呢? 此处输入图片的描述 Q4 :是否和主线程有关?如果当前函数在子线程执行,任务回归主线程操作,是否也可以阻止阻塞呢? 此处输入图片的描述

总结 我们注意到线程还是在main主线程。所以:同一队列同一线程进行sync同步操作,会阻塞,Crash

继续探索,按照上面说的,自定义队列切主队列,是否也可以阻止阻塞呢? 总结: 主队列是个特殊队列,APP启动时就与主线程完成了线程绑定,不会切换线程。

  1. 当前环境:主队列+主线程,执行sync同步 + main_queue主队列任务,会阻塞
  2. 当前环境:主队列+主线程,切换到自定义串行队列,不会开辟线程(block在main线程执行),不会阻塞
  3. 当前环境:自定义串行队列+主线程,切换到主队列,会阻塞
  4. 当前环境:自定义串行队列+主线程,切换到新自定义串行队列,不会开辟线程(block在main线程执行),不会阻塞
  5. 当前环境:自定义串行队列+子线程,切换到主队列,主队列是绑定main线程的,所以会切换回main线程执行block任务,执行完后回到子线程,执行后续任务。不会阻塞

2.2 同步 + 并行

 for (int i = 0; i<20; i++) {
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");

总结 不会阻塞线程,但是一次只通过一个。是耗时操作。

2.3 异步 + 串行

- (void)mainAsyncTest{
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"1 %@", [NSThread currentThread]);
    });
    NSLog(@"2 %@", [NSThread currentThread]);
}

可以发现,异步+串行时,异步函数内的Block(打印1)是在mainAsyncTest函数全部执行完后(打印了2),再在新线程中执行block,打印了1。

可以对比上面2.1 同步 + 串行 阻塞死锁的现象,两者的区别是: 同步 + 串行:

  • dispatch_sync必须 等 mainSyncTest执行完,才将block任务插入尾部。
  • dispatch_sync必须 等 block执行完,才算完成。

异步 + 串行:

  • 1.dispatch_async 不用等 mainAsyncTest执行完,直接将block任务插入尾部。
  • dispatch_async 不用等 block执行完,只要将block插入尾部,就算完成了。

2.4 异步 + 并行

for (int i = 0; i<20; i++) {
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d-%@",i,[NSThread currentThread]);
        });
    }
    NSLog(@"hello queue");

总结 会开启多个线程,执行顺序不确定。

3. 性能调度耗能

 CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
 dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_SERIAL);
//    dispatch_async(queue, ^{
//        NSLog(@"异步执行");
//    });
    dispatch_sync(queue, ^{
        NSLog(@"同步执行");
    });
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);

对比无任何操作、创建线程、创建线程调用同步函数、创建线程调用异步函数四种情况的耗时:

  • 无任何操作时,基本无耗时
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
  • 创建线程: 耗时0.00009秒
 CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
 dispatch_queue_t queue = dispatch_queue_create("thread", 
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
  • 创建线程且调用异步函数: 耗时0.00040秒
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    NSLog(@"异步执行");
});
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
  • 创建线程且调用同步函数: 耗时0.000232秒
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
        NSLog(@"同步执行");
});
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);

总结:

  • 每次创建线程,都会有时间上的损耗
  • 线程创建后,同步执行比异步执行更耗时

4. 面试题

面试题1:

  // 串行队列
    dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    // 异步函数
    dispatch_async(queue, ^{
        NSLog(@"2");
        // 同步
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
    });
    NSLog(@"5");

答案: 打印 1、5、2后,崩溃

面试题2 :

  // 并行队列
    dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1 %@",[NSThread currentThread]);
    // 异步
    dispatch_async(queue, ^{
        NSLog(@"2 %@",[NSThread currentThread]);
        // 同步
        dispatch_sync(queue, ^{
            NSLog(@"3 %@",[NSThread currentThread]);
        });
        NSLog(@"4 %@",[NSThread currentThread]);
    });
    NSLog(@"5 %@",[NSThread currentThread]);

答案

  • 打印结果: 1 -> 5 -> 2 -> 3 -> 4
  • 与面试题一不同,这里是DISPATCH_QUEUE_CONCURRENT并行队列。
  • 参考2.2 同步+并行分析,并发队列中的dispatch_sync同步函数不会阻塞线程,但是一次只通过一个任务。

面试题3:

 // 并行队列
    dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    // 耗时
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");

答案: 打印结果: 1 -> 5 -> 2 -> 4 -> 3

面试题4:

选出打印顺序可能出现的选项: A: 1230789 B: 1237890 C: 3120798 D: 2137890

 // 并行队列
    dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    
    // 同步
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    
    NSLog(@"0");

    dispatch_async(queue, ^{
        NSLog(@"7");
    });
    dispatch_async(queue, ^{
        NSLog(@"8");
    });
    dispatch_async(queue, ^{
        NSLog(@"9");
    });

答案 是 A 和 C 分析:

  • 是并发队列;
  • 异步 & 并发是无序的,所以1和2的打印是无序的, 7、8、9的打印是无序的;
  • 同步 & 并发是排队一个个任务执行,所以0一定在3后面打印,7、8、9一定在0后面打印。
  • 满足0在3后打印,7、8、9在0后打印。只有选项 A 和 C。

5 线程资源共享

  • 多读单写: 利用串行队列,异步函数支持多人买票,同步函数限制同一时刻仅出一张票。
@interface ViewController ()
@property (nonatomic, assign) NSInteger tickets;      // 票数
@property (nonatomic, strong) dispatch_queue_t queue; // 队列
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 准备票数
    _tickets = 20;
    // 创建串行队列
    _queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    // 第一个线程卖票
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self saleTickes];
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 第二个线程卖票
        [self saleTickes];
    });

}

- (void)saleTickes {
    
    while (self.tickets > 0) {
        // 模拟延时
        [NSThread sleepForTimeInterval:1.0];
        // 苹果不推荐程序员使用互斥锁,串行队列同步任务可以达到同样的效果!
        // @synchronized
        // 使用串行队列,同步任务卖票
        dispatch_sync(_queue, ^{
            // 检查票数
            if (self.tickets > 0) {
                self.tickets--;
                NSLog(@"还剩 %zd %@", self.tickets, [NSThread currentThread]);
            } else {
                NSLog(@"没有票了");
            }
        });
    }
}
@end

6. 栅栏函数barrier

控制任务执行顺序,同步。

  • dispatch_barrier_async:前面任务都执行完毕,才会到这里(不会堵塞线程)
  • dispatch_barrier_sync:堵塞线程,等待前面任务都执行完毕,才放开堵塞。堵塞期间,后面的任务都被挂起等待。

重点:栅栏函数只能控制同一并发队列

  • 栅栏函数只应用在并行队列&异步函数中,它的作用就是在监听多个信号(任务)是否都完成。

( 串行或同步内的信号(任务)本身就是按顺序执行,不需要使用到栅栏函数。)

坑点:栅栏函数为何不能使用dispatch_get_global_queue队列? 因为global队列中有很多系统任务也在执行。 我们需要dispatch_queue_create手动创建一个纯净的队列,放置自己需要执行的任务,再使用栅栏函数监听任务的执行结果。

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    __block CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
    
    // 请求token
    [self requestToken:^(id value) {
        // 带token
        [weakSelf requestDataWithToken:value handle:^(BOOL success) {
            success ? NSLog(@"成功") : NSLog(@"失败");
            NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
        }];
    }];
}

/** 获取token请求 */
- (void)requestToken:(void(^)(id value))successBlock{
    NSLog(@"开始请求token");
    [NSThread sleepForTimeInterval:1];
    if (successBlock) {
        successBlock(@"b2a8f8523ab41f8b4b9b2a79ff47c3f1");
    }
}

/** 请求所有数据 */
- (void)requestDataWithToken: (NSString *)token handle: (void(^)(BOOL success))successBlock {
    dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        [self requestHeadDataWithToken: token handle:^(id value) { NSLog(@"%@", value); }];
    });
    
    dispatch_async(queue, ^{
        [self requestListDataWithToken:token handle:^(id value) { NSLog(@"%@", value); }];
    });
    
    dispatch_barrier_async(queue, ^{  successBlock(true); });
    
}

/** 头部数据的请求 */
 - (void)requestHeadDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
    if (token.length == 0) {
        NSLog(@"没有token,因为安全性无法请求数据");
        return;
    }
    [NSThread sleepForTimeInterval:2];
    if (successBlock) {
        successBlock(@"我是头,都听我的");
    }
}
/** 列表数据的请求 */
 - (void)requestListDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
    if (token.length == 0) {
        NSLog(@"没有token,因为安全性无法请求数据");
        return;
    }
    [NSThread sleepForTimeInterval:1];
    if (successBlock) {
        successBlock(@"我是列表数据");
    }
}
@end

7 调度组 Group

与栅栏函数类似,也是控制任务的执行顺序。

  • dispatch_group_create 创建组
  • dispatch_group_async 进组任务 (自动管理进组和出组)
  • dispatch_group_notify 进组任务执行完毕通知
  • dispatch_group_wait 进组任务执行等待时间
  • dispatch_group_enter 进组
  • dispatch_group_leave 出组 进组和出组需要成对搭配使用
@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    __block CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
    
    // 1. 【手动入组和出组】
    [self requestToken:^(id value) {        dispatch_group_t group = dispatch_group_create();        dispatch_queue_t concurrent = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);        dispatch_group_enter(group);        dispatch_async(concurrent, ^{            [weakSelf requestHeadDataWithToken:value handle:^(id value) {                NSLog(@"%@",value);                dispatch_group_leave(group);            }];
        });

        dispatch_group_enter(group);
        dispatch_async(concurrent, ^{
            [weakSelf requestListDataWithToken:value handle:^(id value) {
                NSLog(@"%@",value);
                dispatch_group_leave(group);
            }];
        });

        dispatch_group_notify(group, concurrent, ^{
            NSLog(@"成功了");
            NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
        });
    }];
}

/** 获取token请求 */
- (void)requestToken:(void(^)(id value))successBlock{
    NSLog(@"开始请求token");
    [NSThread sleepForTimeInterval:1];
    if (successBlock) {
        successBlock(@"b2a8f8523ab41f8b4b9b2a79ff47c3f1");
    }
}

/** 头部数据的请求 */
- (void)requestHeadDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
    if (token.length == 0) {
        NSLog(@"没有token,因为安全性无法请求数据");
        return;
    }
    [NSThread sleepForTimeInterval:2];
    if (successBlock) {
        successBlock(@"我是头,都听我的");
    }
}
/** 列表数据的请求 */
- (void)requestListDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
    if (token.length == 0) {
        NSLog(@"没有token,因为安全性无法请求数据");
        return;
    }
    [NSThread sleepForTimeInterval:1];
    if (successBlock) {
        successBlock(@"我是列表数据");
    }
}
@end
  1. GCD单例 单例: 利用static在内存中仅一份的特性,保证了对象的唯一性。 重写allocWithZone的实现,让外界使用alloc创建时,永远返回的是static声明的对象。 以下是KCImageManger的核心代码:
// 保存在常量区
static id instance;
@implementation KCImageManger
/**
 每次类初始化的时候进行调用
 1、+load它不遵循那套继承规则。如果某个类本身没有实现+load方法,那么不管其它各级超类是否实现此方法,系统都不会调用。+load方法调用顺序是:SuperClass -->SubClass --> CategaryClass。
 3、+initialize是在类或者它的子类接受第一条消息前被调用,但是在它的超类接收到initialize之后。也就是说+initialize是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的+initialize方法是不会被调用的。
 4、只有执行+initialize的那个线程可以操作类或类实例,其他线程都要阻塞等着+initialize执行完。
 5、+initialize  本身类的调用都会执行父类和分类实现 initialize方法都会被调多次
 */
+ (void)initialize{
    NSLog(@"父类");
    if (instance == nil) {
        instance = [[self alloc] init];
    }
}
/**
 配合上面 也能进行单利
 */
+ (instancetype)manager{
    return instance;
}

/**
 * 所有为类的对象分配空间的方法,最终都会调用到 allovWithZone 方法
 * 下面这样的操作相当于锁死 该类的所有初始化方法
 */
+(instancetype)allocWithZone:(struct _NSZone *)zone{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [super allocWithZone:zone];
    });
    return instance;
}

/**
 单利
 */
+(instancetype)shareManager{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

@end

9 信号量 semaphore

控制GCD的最大并发数。(同一时刻可进行的信号(任务)最大个数。)

  • dispatch_semaphore_create: 创建信号量
  • dispatch_semaphore_wait: 信号量等待
  • dispatch_semaphore_signal: 信号量释放

加入了信号量的等待dispatch_semaphore_wait后,一定需要配对加入信号量释放dispatch_semaphore_signal,不然会crash

// 创建全局队列(并行)
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
   // 设置信号量
    dispatch_semaphore_t sem = dispatch_semaphore_create(2); // 最多同时执行2个任务
    
    //任务1
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        sleep(1);
        NSLog(@"执行任务1");
        sleep(1);
        NSLog(@"任务1完成");
        dispatch_semaphore_signal(sem);
    });
    
    //任务2
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        sleep(1);
        NSLog(@"执行任务2");
        sleep(1);
        NSLog(@"任务2完成");
        dispatch_semaphore_signal(sem);
    });
    
    //任务3
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        sleep(1);
        NSLog(@"执行任务3");
        sleep(1);
        NSLog(@"任务3完成");
        dispatch_semaphore_signal(sem);
    });