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

这是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),所以我们需要去指明这个参数。

<p>

字符串类型的属性应该被声明为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方法中返回正确的值。

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

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

推荐阅读更多精彩内容