Multithreading - GCD


This chapter mainly introduces the most frequently used GCD in multithreading

  1. Introduction to GCD
  2. Four combinations of functions and queues (synchronous, asynchronous, serial, parallel)
  3. Performance scheduling energy consumption
  4. interview questions
  5. thread resource sharing
  6. barrier function barrier
  7. Scheduling Group
  8. GCD singleton
  9. semaphore

1 Introduction to GCD

GCD, the full name of Grand Central Dispatch (Central Dispatch Center), is developed in pure C language and provides many powerful functions.

  • Advantage:
    • GCD is Apple's solution for multi-core parallel computing;
    • GCD will automatically utilize more CPU cores;
    • GCD automatically manages the life cycle of threads (create threads, schedule tasks, destroy threads).
    • Programmers only need to tell GCD what tasks they want to perform, and do not need to write any thread management related code (regardless of scheduling and destruction)
  • Core: Add a task to the queue and specify a function to execute the task

Here are three contents: tasks, queues, and functions that execute tasks. We analyze one by one

// 任务(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 Tasks

The task of GCD is to use block-encapsulated functions without input and return parameters. After the task is created, the function waiting to execute the task puts it in the queue.

1.2 Queue

GCD queues include serial queues and parallel queues. Serial Queue: Only one task is allowed to execute at a time. Parallel Queue: Allows multiple tasks to execute at the same time.

1.3 Functions that perform tasks

There are two types of functions that perform tasks: synchronous functions and asynchronous functions:

  • dispatch_sync synchronization function:
    • You must wait for the current statement to complete before executing the next statement
    • The thread will not be opened, and the block task will be executed on the current thread
  • dispatch_async asynchronous function:
    • Execute the next statement without waiting for the current statement to complete
    • Will start the thread to execute the block task

2 Four combinations of functions and queues (synchronous, asynchronous, serial, parallel)

  • Main queue dispatch_get_main_queue:

    • A serial queue dedicated to scheduling tasks on the main thread
    • thread will not be started
    • If the current main thread is executing a task, it needs to wait for the current task to complete before continuing to schedule other tasks.
  • Global concurrent queue dispatch_get_global_queue:

    • For the convenience of programmers, Apple provides a global queue (concurrent queue, a shortcut to achieve multi-threading needs).
    • When using multi-threaded development, if there is no special requirement for the queue, you can directly use the global queue to execute asynchronous tasks

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);
    });
复制代码

Guess you like

Origin juejin.im/post/6992132403841990692
gcd