【译】用GCD构造线程安全的类(Writing Thread-Safe Classes with GCD)

【译】用GCD构造线程安全的类(Writing Thread-Safe Classes with GCD)_第1张图片

这是GCD介绍的第五篇文章。

到目前为止,我们已经了解到,在多线程的程序中,数据读写访问的操作必须被某种同步机制保护着。我们使用GCD同步队列来确保这个过程。

我们也讨论了如何使用并发队列实现一个读写锁。为了让这篇文章更简单易懂,这里我继续使用串行队列。

一个始终存在的问题:不论是谁访问了你的数据都必须使用dispatch_sync()来确保线程安全,否则就会得到错误的结果。尤其是当你的代码被某个不熟悉你的用意的人使用时(比如,你的代码是某个框架的一部分),这个问题将尤为明显。

如果我们能够将访问数据的同步操作封装起来是不是很好?这样使用者就不必再担心同步操作的问题了。

封装是类所擅长的东西。我们必须创建线程安全的类,而不是要求使用者在他们的代码中写那些同步代码。

线程安全的类

什么样的类是线程安全的呢?简单来说,如果一个类允许程序在任何线程中去实例化它,销毁它,访问它的属性,调用它的方法而不用担心会出现多线程相关的错误,那这个类就是线程安全的。

不是每一个类都需要线程安全!实现线程安全会操作性能的损失,在很多情况下都是不必要的。在你的设计中,你应该选定正确的“同步点”,这些同步点之外的对象就不用再考虑线程安全了。

我们试着让下面这个类变得线程安全。

@interface Warrior: NSObject

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

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

@end

在属性参数里,我们用了nonatomic,因为我们不想让编译器为我们的属性生成自动同步的版本。因为所有属性默认都是具有原子性的(atomic),所以我们需要去指明这个参数。

字符串类型的属性应该被声明为copy而不是strong的,这里我只是为了容易理解。

swapLeftAndRightHandEquippedItems方法的一种线程不安全的实现方式可能如下:

@implementation Warrior
- (void)swapLeftAndRightHandEquippedItems {
    NSString *oldLeftHandEquippedItem = self.leftHandEquippedItem;
    self.leftHandEquippedItem = self.rightHandEquippedItem;
    self.rightHandEquippedItem = oldLeftHandEquippedItem;
}
@end

很明显这这样做并不是线程安全的。如果在当前线程正在交换2个item时,有另一个线程给rightHandEquippedItem重新赋了一个新值,错误就出现了。

用队列来补救

我们需要用队列来串行化访问属性的操作。鉴于GCD队列还是相对廉价的,所以我们给每一个实例创建一个队列。

@interface Warrior()
@property (nonatomic, strong) dispatch_queue_t memberQueue;
@end

@implementation Warrior
- (id)init {
    self = [super init];
    if (self) {
        _memberQueue = dispatch_queue_create("Queue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

// ...

@end

这个匿名的分类(category)是一个声明私有属性和方法的好地方。它必须在.m文件中声明。

现在我们需要串行化访问leftHandEquippedItemrightHandEquippedItem属性的操作。我们可以重写它们的getter和setter方法来达到这个目的:

@implementation Warrior
- (NSString *)leftHandEquippedItem {
    __block NSString *retval;
    dispatch_sync(self.memberQueue, ^{
        retval = _leftHandEquippedItem;
    });
    return retval;
}

- (void)setLeftHandEquippedItem:(NSString *)item {
    dispatch_sync(self.memberQueue, ^{
        _leftHandEquippedItem = item;
    });
}
// Same for right hand...

你无法在block内给block外的变量重新赋值,除非它被__block修饰。

这解决了我们的同步问题,但是我再向前一步,为这些属性声明一个用队列名修饰的内部版本。

@interface Warrior()
@property (nonatomic, strong) NSString *memberQueueLeftHandEquippedItem;
@property (nonatomic, strong) NSString *memberQueueRightHandEquippedItem;
// ...
@end

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

- (void)setLeftHandEquippedItem:(NSString *)item {
    dispatch_sync(self.memberQueue, ^{
        self.memberQueueLeftHandEquippedItem = item;
    });
}
// Same for right hand...
@end

为什么要费力做这些?首先,这让swapLeftAndRightHandEquippedItems方法实现起来更容易:

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

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

看到了这种模式没有?如果你的类方法和属性变成更多,这种命名方法能够帮助你的类保持串行。没有它们,每当你要使用一个方法或者属性的时候,都会疑惑是否要将其dispatch_sync到一个队列。

这种命名规则目的很简单:以队列名开头的属性和方法能够确保它们已经被加入到这个队列中了。

除了在initdealloc,或者属性的getter和setter方法中,其他地方直接去访问一个属性(例如:_myProperty)是不好的做法。其他地方应该用 self.myProperty的形式来使得你的代码易读性更高。

这种命名方法可以使你没有疑虑得构建原子性的操作:

@implementation Warrior
- (NSString *)juggleNewItem:(NSString *)item {
    __block NSString *retval;
    dispatch_sync(self.memberQueue, ^{
        retval = self.memberQueueRightHandEquippedItem;
        self.memberQueueRightHandEquippedItem = item;
        [self memberQueueSwapLeftAndRightHandEquippedItems];
    });
    return retval;
}
// ...
@end

由于你已经在memberQueue队列中将block同步化了,所以使用这些以memberQueue开头的属性或方法是安全的。(实际上,你必须使用以memberQueue开头的属性或者方法,否则的话,有可能操作死锁!)

看看这种命名协议是怎么让操作串行的?

假如当item改变时你想打印出它的值:

@implementation Warrior
- (void)setMemberQueueLeftHandEquippedItem:(NSString *)item {
    NSLog(@"Left hand now holds %@", item);
    _memberQueueLeftHandEquippedItem = item;
}
// Same for right hand...

是不是很简单。

整体来看

类的全部实现如下:

// Header file

@interface Warrior: NSObject

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

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

@end

// Implementation file

@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;
}

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

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

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

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

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

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

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

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

- (NSString *)juggleNewItem:(NSString *)item {
    __block NSString *retval;
    dispatch_sync(self.memberQueue, ^{
        retval = self.memberQueueRightHandEquippedItem;
        self.memberQueueRightHandEquippedItem = item;
        [self memberQueueSwapLeftAndRightHandEquippedItems];
    });
    return retval;
}

@end

注意到相比于之前线程不安全的版本,类的公共接口并没有改变。这就说明你做的事是正确的:所有线程安全的代码都隐藏在类的内部,这就使得这个类的使用者可以完全不用知道关于线程同步的知识而去轻松的使用它。

读者练习:定义了个新的只读的NSArray属性,叫做bagOfCarrying。提供一个线程安全的方法去添加和删除这个数组里面的值。(提示:在内部声明一个bagOfCarrying类型的属性叫做memberQueueBagOfCarrying。)想想你该如何在避免线程问题的前提下,在bagOfCarrying的getter方法中返回正确的值。

希望这篇文章对你有用!下次我们将会探讨一下创建有回调事件的类。

你可能感兴趣的:(【译】用GCD构造线程安全的类(Writing Thread-Safe Classes with GCD))