老谭笔记

线程安全的可变容器类

稍稍有点儿Objective-C编程经验的人都明白NSMutableArray和NSMutableDictionary此类可变的容器都是线程不安全的。但即便大家都知晓这个准则,但在使用多线程编程时,我们仍然会通过可变容器来达到线程间的数据共享,当然作为富有经验的程序员,你肯定会想法设法让这种不安全变得安全,比如用锁、或者将容器的操作切换到同一个线程或串行的dispatch_queue中。但有时原本很简单的逻辑却因为这严谨的安全让这一切变得复杂起来,这时我便希望有一个安全的容器类让代码显得更加优雅。

好吧,我的想法就是继承原可变容器构建一个安全的子类,虽然前辈们都不推荐这样做。Cocoa的可变容器有一个高端特性被称之为Class Clusters,关于这个特性的具体解释我摘抄了lianxu博客中的文字:

在 Cocoa 中有一种奇葩的类存在,有程序员抨击它是 OOP 模式的破坏者,这就是 Class Clusters。面向对象的编程教育我们:“类可以继承,子类具有父类的方法”。而 Cocoa 中的 Class Clusters 虽然平时表现的像普通类一样,但子类却没法继承父类的方法。而 NSMutableArray, NSMutableDictionary 就是这样一个玩意。为何如此?因为 Class Clusters 内部其实是由多个私有的类和方法组成。虽然它有这样的弊端,但是好处还是不言而喻的。例如,NSNumber 其实也是这种类,这样一个类可以把各种不同的原始类型封装到一个类下面,提供统一的接口。这正设计模式中的抽象工厂模式。

查看Apple的文档,要继承这样的类需要必须实现其primitive methods方法,实现了这些方法,其它方法便都能通过这些方法组合而成。比如需要继承NSMutableArray就需要实现它的以下primitive methods:

1
2
3
4
5
- (void)addObject:(id)anObject;
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index;
- (void)removeLastObject;
- (void)removeObjectAtIndex:(NSUInteger)index;
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;

和NSArray的primitive methods:

1
2
- (NSUInteger)count;
- (id)objectAtIndex:(NSUInteger)index;

而NSMutableDictionary也类似,在Apple文档中都有详细的描述。当然除此之外你都可以选择性的实现其它方法,以达到更高效率,比如NSMutableArray的removeAllObjects方法,因为默认他将循环调用removeLastObject方法达到目的,而你则可以选择更高效的实现方式。而要达到线程安全,不外乎就是在这些方法内部都加上锁,简化多线程情景下的容器使用,不必手动逐一添加锁。

上面已经详细描述了派生线程安全的可变容器,实现其实已经很简单了。但既然要实现线程安全的容器类,并且是Apple和前辈都不推荐的做法,这必须有一定的原因,而这原因就是程序的公敌————性能问题。经过简单的测试,性能会发生倍数的影响,不过因为容器性能好,基数小,所以性能尚且还能接收。但当你使用容器来频繁的处理大量数据则不推荐这样选择,仅当线程间的同步成了数据共享的瓶颈时,一个安全的容器类才有存在的价值。

相关的代码及测试Demo:https://github.com/tanhaogg/SafeContainer