最近做了一个基于 LRU 策略的缓存 ZLRUCache
,自己玩着感觉还可以。
为了保证 Cache 内部的线程安全,在读取和设置缓存对象的接口内部进行了加锁处理,类似以下:
[_lock lock];
...
[self do_something];
...
[_lock unlock];
嗯,看起来没有问题,别人的代码也是这么写的。
外部使用缓存的时候,一般是定义一个内部成员变量 ZLRUCache *_cache;
,然后在内部拿着 _cache
对象来操作缓存。
这样就可能有问题了,在并发处理缓存的时候,缓存内部会使用 [_lock lock]
等待操作,如果使用者这个时候把缓存对象丢弃:_cache = nil;
,或者是使用者被释放导致 _cache
被析构。在接着 [_lock lock]
往下执行,当访问到内部的变量或者执行到末尾的 [_lock unlock]
时,就会触发非法访问而导致崩溃。
这种情况是使用者的问题吗?不是,是我们设计的库不够健壮。
要避免以上问题,在 LRUCache 内部需要保证 self
和 _lock
始终有效直到内部的操作执行完成。要达到这个目的,只需要将他们的引用计数加一就可以了,在函数返回的时候再减一。
还可以更方便一些,利用局部对象会在函数返回后析构的特点,制作两个工具类,SelfGuard
和 LockGuard
。
@interface ZSelfGuard () {
@private
id _self;
}
@end
@implementation ZSelfGuard
+ (instancetype)guardWithObject:(id)object {
return [[self alloc] initWithObject:object];
}
- (void)dealloc {
_self = nil;
}
- (instancetype)initWithObject:(id)object {
if (self = [super init]) {
_self = object;
}
return self;
}
@end
@interface ZLockGuard () {
@private
id<ZLocking> _lock;
}
@end
@implementation ZLockGuard
+ (instancetype)guardWithLock:(id<ZLocking>)lock {
return [[self alloc] initWithLock:lock];
}
- (void)dealloc {
[_lock unlock];
_lock = nil;
}
- (instancetype)initWithLock:(id<ZLocking>)lock {
if (self = [super init]) {
_lock = lock;
[lock lock];
}
return self;
}
@end
在使用的时候,需要使用 @autoreleasepool
包起来保证能及时释放
@autoreleasepool {
__unused ZSelfGuard *selfGuard = [ZSelfGuard guardWithObject:self];
ZLockGuard *lockGuard = [ZLockGuard guardWithLock:_lock];
[_lock lock];
...
[self do_something];
...
[_lock unlock];
}
现在 ZLRUCache
就足够健壮了。
但是,效率没了,和主流的缓存库对比:
===========================
Memory cache set 200000 key-value pairs
NSDictionary: 31.24
NSDict+Lock: 35.27
YYMemoryCache: 85.48
PINMemoryCache: 155.53
NSCache: 262.20
ZLRUCache: 230.57
数据太难看了,再针对性的优化一下。
不使用局部对象,改成使用局部变量增加引用计数:
__unused id selfGuard = self;
ZLock *lockGuard = _lock;
[lockGuard lock];
...
[self do_something];
...
[lockGuard unlock];
再测试一下性能,iPhone 8 Plus + iOS 13.4.1
===========================
Memory cache set 200000 key-value pairs
NSDictionary: 32.22
NSDict+Lock: 34.87
YYMemoryCache: 85.61
PINMemoryCache: 152.51
NSCache: 151.21
ZLRUCache: 157.02
===========================
Memory cache set 200000 key-value pairs without resize
NSDictionary: 18.12
NSDict+Lock: 26.85
YYMemoryCache: 92.38
PINMemoryCache: 132.85
NSCache: 151.47
ZLRUCache: 92.19
===========================
Memory cache get 200000 key-value pairs
NSDictionary: 16.41
NSDict+Lock: 22.96
YYMemoryCache: 53.99
PINMemoryCache: 85.54
NSCache: 15.02
ZLRUCache: 55.84
===========================
Memory cache get 100000 key-value pairs randomly
NSDictionary: 26.69
NSDict+Lock: 37.65
YYMemoryCache: 93.70
PINMemoryCache: 106.93
NSCache: 18.94
ZLRUCache: 94.93
===========================
Memory cache get 200000 key-value pairs none exist
NSDictionary: 26.49
NSDict+Lock: 42.57
YYMemoryCache: 80.79
PINMemoryCache: 86.07
NSCache: 18.90
ZLRUCache: 98.80