阅读 23

实现一个线程安全的缓存库

最近做了一个基于 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 始终有效直到内部的操作执行完成。要达到这个目的,只需要将他们的引用计数加一就可以了,在函数返回的时候再减一。

还可以更方便一些,利用局部对象会在函数返回后析构的特点,制作两个工具类,SelfGuardLockGuard

@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
复制代码

还行,能看。

github.com/cntrump/ZLR…