《a series about Grand Central Di

2019-01-30  本文已影响2人  我才是臭吉吉

本篇是a series about Grand Central Dispatch的学习记录。
原文共6篇,循序渐进地介绍了GCD的基本使用及相关规范。

1. Why GCD?

1.1 在GCD出现之前,解决数据竞争的方式

1.2 GCD队列

分为串行队列、并发队列和主队列三种。

1.3 Blocks

Blocks可以捕获上下文中的变量(保存当时的值)。

1.4 你好,GCD

// 1.
dispatch_main(); 

// 2.
[[NSRunLoop currentRunLoop] run];

以上两者均可以启动主线程运行(在main函数中可以使队列不返回,程序不结束。)。但是建议用后者,因为NSRunLoop对象中可以支持NSTimer等资源执行,前者不可以。需要退出当前队列,在对应队列中执行exit(0)即可。

2. Using GCD Queues For Synchronization

传统情况下,数据竞争(多线程环境下更新同一份数据)需要使用排它锁进行数据更新,使用GCD可以通过串行队列直接解决。

3. GCD Concurrent Queues

4. GCD Target Queues

4.1 使用目标队列规范执行时机

直接看栗子:

#import <Foundation/Foundation.h>

void makeCall(dispatch_queue_t queue, NSString *caller, NSArray *callees) {
    // 获取随机接通对象
    NSInteger targetIndex = arc4random() % callees.count;
    NSString *callee = callees[targetIndex];
    
    NSLog(@"%@ 正在呼叫 %@...", caller, callee);
    sleep(1);
    NSLog(@"%@ 与 %@ 呼叫完毕!", caller, callee);
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(arc4random() % 1000 * NSEC_PER_SEC)), queue, ^{
        makeCall(queue, caller, callees);
    });
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray *house1Folks = @[@"小明", @"小华", @"小希"];
        NSArray *house2Folks = @[@"大明", @"大华", @"大希"];
        
        // 创建并发队列,用于执行block
        dispatch_queue_t house1Queue = dispatch_queue_create("com.jiji.concurrent", DISPATCH_QUEUE_CONCURRENT);
        
        // 创建串行队列,作为并发队列的目标队列
        dispatch_queue_t targetQueue = dispatch_queue_create("com.jiji.targetQueue", DISPATCH_QUEUE_SERIAL);
        dispatch_set_target_queue(house1Queue, targetQueue);
        
        for (NSString *caller in house1Folks) {
            dispatch_async(house1Queue, ^{
                makeCall(house1Queue, caller, house2Folks);
            });
        }
        
        // 使主队列不返回
        [[NSRunLoop currentRunLoop] run];
//        dispatch_main(); // Xcode都无法结束调试
    }
    return 0;
}

执行结果为:

2019-01-28 11:09:09.259488+0800 GCDTest[92057:9310107] 小明 正在呼叫 大明...
2019-01-28 11:09:10.260182+0800 GCDTest[92057:9310107] 小明 与 大明 呼叫完毕!
2019-01-28 11:09:10.260306+0800 GCDTest[92057:9310107] 小华 正在呼叫 大希...
2019-01-28 11:09:11.262624+0800 GCDTest[92057:9310107] 小华 与 大希 呼叫完毕!
2019-01-28 11:09:11.262751+0800 GCDTest[92057:9310107] 小希 正在呼叫 大希...
2019-01-28 11:09:12.264263+0800 GCDTest[92057:9310107] 小希 与 大希 呼叫完毕!

使用目标队列,在本例中,通过将串行队列设置为house1Queue的目标队列,将本来在并发队列中对于block的无序执行变为了串行执行。故dispatch_set_target_queue可以将原队列的优先级设置为与目标队列相同,在这里即变为了串行执行。而由于自定义的串行队列默认的目标为全局并发队列,故实际上block是在全局队列中重进进队并执行。

4.2 使指定串行队列作为多个队列的共同目标

这样设置,可以将子队列设置为同级(实质优先级相同,且执行block时都在目标队列中)。

4.3 现实当中的应用

注意:

5. Writing Thread-Safe Classes with GCD

线程安全,一般来说,在GCD中都需要使用串行队列或者在并发队列中的派发执行栅栏任务。故一定需要记住一点:线程安全是以牺牲性能为代价的,一定不要滥用。

使用GCD实现线程安全的类,需要在内部设置上述二者之一的队列。作者以串行队列为例,并以优雅的封装方式进行了阐述。直接贴完整代码:

// 头文件(公共API中没有任何需要调用者额外注意的事件)

@interface Warrior: NSObject

@property (nonatomic, strong) NSString *leftHandEquippedItem;
@property (nonatomic, strong) NSString *rightHandEquippedItem;

- (void)swapLeftAndRightHandEquippedItems;
- (NSString *)juggleNewItem:(NSString *)item; // return dropped item

@end

// 实现文件

@interface Warrior()

/** 串行队列,线程安全的基础 */
@property (nonatomic, strong) dispatch_queue_t memberQueue;
/** 线程安全的内部版本,该命名方式表明需要在队列中使用 */
@property (nonatomic, strong) NSString *memberQueueLeftHandEquippedItem;
/** 线程安全的内部版本,该命名方式表明需要在队列中使用 */
@property (nonatomic, strong) NSString *memberQueueRightHandEquippedItem;

@end

@implementation Warrior

- (id)init {
    self = [super init];
    if (self) {
        // 由于队列对象非常“轻”,实例化时可放心创建
        _memberQueue = dispatch_queue_create("Queue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

// private的setter(由于内部版本需要在队列中使用,故直接赋值即可)

- (void)setMemberQueueLeftHandEquippedItem:(NSString *)item {
    NSLog(@"Left hand now holds %@", item);
    _memberQueueLeftHandEquippedItem = item;
}

- (void)setMemberQueueRightHandEquippedItem:(NSString *)item {
    NSLog(@"Right hand now holds %@", item);
    _memberQueueRightHandEquippedItem = item;
}

// public的getter(外部API的实现是调用的内部版本,需要在队列中使用,保证线程安全)

- (NSString *)leftHandEquippedItem {
    __block NSString *retval;
    dispatch_sync(self.memberQueue, ^{
        retval = self.memberQueueLeftHandEquippedItem;
    });
    return retval;
}

- (NSString *)rightHandEquippedItem {
    __block NSString *retval;
    dispatch_sync(self.memberQueue, ^{
        retval = self.memberQueueRightHandEquippedItem;
    });
    return retval;
}

// public的setter(外部API的实现是调用的内部版本,需要在队列中使用,保证线程安全)

- (void)setLeftHandEquippedItem:(NSString *)item {
    dispatch_sync(self.memberQueue, ^{
        self.memberQueueLeftHandEquippedItem = item;
    });
}

- (void)setRightHandEquippedItem:(NSString *)item {
    dispatch_sync(self.memberQueue, ^{
        self.memberQueueRightHandEquippedItem = item;
    });
}

// private的method(由于内部版本需要在队列中使用,故直接赋值即可)

- (void)memberQueueSwapLeftAndRightHandEquippedItems {
    NSString *oldLeftHandEquippedItem = self.memberQueueLeftHandEquippedItem;
    self.memberQueueLeftHandEquippedItem = self.memberQueueRightHandEquippedItem;
    self.memberQueueRightHandEquippedItem = oldLeftHandEquippedItem;
}

// public的method(外部API的实现是调用的内部版本,需要在队列中使用,保证线程安全)

- (void)swapLeftAndRightHandEquippedItems {
    dispatch_sync(self.memberQueue, ^{
        [self memberQueueSwapLeftAndRightHandEquippedItems];
    });
}

- (NSString *)juggleNewItem:(NSString *)item {
    __block NSString *retval;
    // 这里再说一遍,由于在队列中执行,故block内直接使用内部版本的变量和方法
    dispatch_sync(self.memberQueue, ^{
        retval = self.memberQueueRightHandEquippedItem;
        self.memberQueueRightHandEquippedItem = item;
        // 这里调用外部方法会导致死锁
        [self memberQueueSwapLeftAndRightHandEquippedItems];
    });
    return retval;
}

@end

以上便实现了一个“线程安全”类。其编码方式需要我们学习:

6. Keeping Things Straight with GCD

6.1 设计线程安全的类或库
// Bank_Private.h
dispatch_queue_t memberQueue();

// Bank.m
#import "Bank_Private.h"
dispatch_queue_t memberQueue() {
    static dispatch_queue_t queue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        queue = dispatch_queue_create("member queue", DISPATCH_QUEUE_SERIAL);
    });
    return queue;
}
6.2 使用单个队列的简易性
6.2.1 创建“读写锁”

“读写锁”,即“随意读取,写入保护”。在创建时,可以设置两个带有前缀的方法,如“memberQueue_”和“memberQueueMutating_”。前者只能读取变量的值或调用无修改功能的方法,后者可以对变量进行读写或调用任意方法(包括可修改数据的方法)。

6.2.2 不要使用多个,嵌套的队列
上一篇下一篇

猜你喜欢

热点阅读