低成本实现线程安全的可变集合

514 阅读4分钟

本文使用 运行时和信号量加锁 的方式,低成本地实现了 线程安全的可变集合 ,并使用 CocoaPods 封装了 YCThreadSafeMutableCollection 库供大家把玩和吐槽。

1. atomic

属性声明为atomic时,在该属性在调用getter和setter方法时,会加上同步锁。即在属性在调用getter和setter方法时,保证同一时刻只能有一个线程调用属性的读/写方法。保证了读和写的过程是可靠的。

但是对于可变集合,我们操作的是atomic指针指向的对象,并不是直接调用其getter/setter,所以还是不可靠的。

2. 加锁

对于可变集合,我们应该在操作集合对象时加锁,以保证其线程安全。

「不再安全的 OSSpinLock」 一文中,我们看到了几种锁的性能对比。由于 OSSpinLock 已不再安全,所以我们采用信号量 dispatch_semaphore 来进行加锁操作,以获得较好的性能。

3. 低成本实现线程安全的可变集合

3.1 方案1

将可变集合封装到另一个对象中,提供操作接口,并在操作接口中加锁,保证其线程安全。

@interface YCThreadSafeMutableArray : NSObject
//NSArray、NSMutableArray APIs
@end
  
@interface YCThreadSafeMutableArray ()

@property (nonatomic, strong) dispatch_semaphore_t lock;
@property (nonatomic, strong) NSMutableArray *mutableArray;

@end
  
@implementation YCThreadSafeMutableArray
  
- (void)addObject:(id)anObject
{
    dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
    [self.mutableArray addObject:anObject];
    dispatch_semaphore_signal(self.lock);
}

... // 其他API
@end
  • 缺点
    • 需要复写大量API,工作量很大。
    • 后期系统版本升级,需要额外的维护成本。

3.2 方案2

采用 继承 + Runtime 方案。

  • 首先,继承可变集合类型。
  • 通过 **Runtime **,将父类实现的API在子类中动态实现,绑定到 _objc_msgForward_objc_msgForward_stret 上,直接进行消息转发。
  • 子类实现必要的,或者不需要消息转发的方法。在遍历父类API时,如果发现子类已实现,可将其排除。
    • 这里为了使用安全,除了必要的 init 方法,我还复写了一些API,以判断传入对象是否为空,索引值是否越界等。
  • 消息转发过程最后 forwardInvocation: 时加锁来保证线程安全。

下面以 Set 为例:

@interface YCThreadSafeMutableSet ()

@property (nonatomic, strong) dispatch_semaphore_t lock;
@property (nonatomic, strong) NSMutableSet *mutableSet;

@end

@implementation YCThreadSafeMutableSet

+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [NSObject tsm_enumrateInstanceMethodsOfClasses:@[NSMutableSet.class, NSSet.class] addToClass:YCThreadSafeMutableSet.class];
    });
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *result = [super methodSignatureForSelector:aSelector];
    if (result) {
        return result;
    }
    result = [self.mutableSet methodSignatureForSelector:aSelector];
    if (result && [self.mutableSet respondsToSelector:aSelector]) {
        return result;
    }
    return [NSMethodSignature threadSafeMutableAvoidExceptionSignature];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL selector = invocation.selector;
    if ([self.mutableSet respondsToSelector:selector]) {
         YC_SEMAPHORE_LOCKPAIR(self.lock, [invocation invokeWithTarget:self.mutableSet]);
    }
}

#pragma mark - must override

- (void)dealloc
{
//    NSLog(@"%s", __func__);
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.lock = dispatch_semaphore_create(1);
        self.mutableSet = [[NSMutableSet alloc] init];
    }
    return self;
}

- (instancetype)initWithCapacity:(NSUInteger)numItems
{
    self = [super init];
    if (self) {
        self.lock = dispatch_semaphore_create(1);
        self.mutableSet = [[NSMutableSet alloc] initWithCapacity:numItems];
    }
    return self;
}

- (instancetype)initWithObjects:(id  _Nonnull const [])objects count:(NSUInteger)cnt
{
    self = [super init];
    if (self) {
        self.lock = dispatch_semaphore_create(1);
        self.mutableSet = [[NSMutableSet alloc] initWithObjects:objects count:cnt];
    }
    return self;
}

- (void)addObject:(id)object
{
    if (object) {
        YC_SEMAPHORE_LOCKPAIR(self.lock, [self.mutableSet addObject:object]);
    }
}

- (void)removeObject:(id)object
{
    if (object) {
        YC_SEMAPHORE_LOCKPAIR(self.lock, [self.mutableSet removeObject:object]);
    }
}

@end
  • 宏定义
#ifndef YC_SEMAPHORE_LOCK
#define YC_SEMAPHORE_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER)
#endif

#ifndef YC_SEMAPHORE_UNLOCK
#define YC_SEMAPHORE_UNLOCK(lock) dispatch_semaphore_signal(lock)
#endif

#define YC_SEMAPHORE_LOCKPAIR(lock, ...) YC_SEMAPHORE_LOCK(lock); \
__VA_ARGS__; \
YC_SEMAPHORE_UNLOCK(lock)
  • 初始化方法没什么好说的,按接口初始化实例对象即可。

  • 消息转发 forwardInvocation: 时,通过 invokeWithTarget: 将响应对象换为内部的可变对象,并加锁保证其线程安全。

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL selector = invocation.selector;
    if ([self.mutableSet respondsToSelector:selector]) {
         YC_SEMAPHORE_LOCKPAIR(self.lock, [invocation invokeWithTarget:self.mutableSet]);
    }
}
  • initialize ,即对象第一次接收到消息时,子类动态实现父类的主要API。
+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [NSObject tsm_enumrateInstanceMethodsOfClasses:@[NSMutableSet.class, NSSet.class] addToClass:YCThreadSafeMutableSet.class];
    });
}
  • tsm_enumrateInstanceMethodsOfClasses: 的实现。
    • 子类动态实现父类的主要API,并将实现 IMP 绑定到 _objc_msgForward_objc_msgForward_stret 上。
@implementation NSObject(ThreadSafeMutable)

CG_INLINE IMP tsm_getMsgForwardIMP(Class cls, SEL selector) {
    IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
    Method method = class_getInstanceMethod(cls, selector);
    const char *typeDescription = method_getTypeEncoding(method);
    if (typeDescription[0] == '{') {
        // 以下代码参考 JSPatch 的实现:
        //In some cases that returns struct, we should use the '_stret' API:
        //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
        //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
        NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription];
        if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
            msgForwardIMP = (IMP)_objc_msgForward_stret;
        }
    }
#endif
    return msgForwardIMP;
}

+ (void)tsm_enumrateInstanceMethodsOfClass:(Class)aClass usingBlock:(void (^)(Method, SEL))block {
    if (!block) return;
    unsigned int methodCount = 0;
    Method *methods = class_copyMethodList(aClass, &methodCount);
    for (unsigned int i = 0; i < methodCount; i++) {
        Method method = methods[i];
        SEL selector = method_getName(method);
        if (block) block(method, selector);
    }
    free(methods);
}

+ (void)tsm_enumrateInstanceMethodsOfClasses:(NSArray<Class> *)classes addToClass:(Class)toClass {
    if (toClass) {
        [classes enumerateObjectsUsingBlock:^(Class  _Nonnull hookClass, NSUInteger idx, BOOL * _Nonnull stop) {
            [self tsm_enumrateInstanceMethodsOfClass:hookClass usingBlock:^(Method  _Nonnull method, SEL  _Nonnull selector) {
                // 如果已经实现了该方法,则不需要消息转发
                if (class_getInstanceMethod(toClass, selector) != method) return;
                const char * typeDescription = (char *)method_getTypeEncoding(method);
                if (typeDescription) {
                    class_addMethod(toClass, selector, tsm_getMsgForwardIMP(hookClass, selector), typeDescription);
                }
            }];
        }];
    }
}

@end

方案2相比方案1:

  • 优点
    • 需要复习的API较少。
    • 使用 Runtime 获取父类方法,父类API变更时动态获取,后期基本不需要维护。
  • 缺点
    • 采用 消息转发 ,会有一定性能损失,但大部分场景下损失较小,可以不用考虑。
    • 采用 NSKeyedArchiverNSKeyedUnarchiver 后,还原的对象为 NSMutableArray
      • 这个问题暂时没有找到好的办法,使用 NSKeyedUnarchiver 根本不走 initWithCoder:
- (void)archiveLimitation
{
    YCThreadSafeMutableArray *array = [[YCThreadSafeMutableArray alloc] init];
    [array addObject:@0];
    [array addObject:@1];
    [array addObject:@2];
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array];
    YCThreadSafeMutableArray *unarchiveArray = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    NSLog(@"%@", NSStringFromClass(unarchiveArray.class));
    NSLog(@"%@", unarchiveArray);
}
// 输出
__NSArrayM
(
    0,
    1,
    2
)
  • 局限性
    • 对于对象直接调用的API可以实现加锁保护,但对 for in这种遍历不能实现保护。
      • for in 底层会调用 countByEnumeratingWithState:objects:count: ,但这个方法只有调用时被锁保护,遍历过程中其实是没有保护的。
    • 对于 enumerateObjectsUsingBlock:objectAtIndex: ,API调用都是被锁保护的,所以是线程安全的。
- (void)badCase
{
    YCThreadSafeMutableArray *array = [[YCThreadSafeMutableArray alloc] init];
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i < 1000; ++i) {
            [array addObject:@(i)];
        }
    });
    // crash
    dispatch_async(queue, ^{
        for (NSNumber *number in array) {
            NSLog(@"%@", number);
        }
        NSLog(@"finished");
    });
    // safe
//    dispatch_async(queue, ^{
//        [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//            NSLog(@"%@", obj);
//        }];
//        NSLog(@"finished");
//    });
    // safe
//    dispatch_async(queue, ^{
//        for (int i = 0; i < 1000; ++i) {
//            NSLog(@"%@", [array objectAtIndex:i]);
//        }
//        NSLog(@"finished");
//    });
}

4. 总结

通过采用 继承 + Runtime ,我们可以低成本实现线程安全的可变集合。

但这种实现还是 有一定缺点和局限性 ,另外这个方案只在 demo 场景下经受了一些考验,暂时没有应用在线上。


如果觉得本文对你有所帮助,给我点个赞吧~ 👍🏻

字节内推二维码:

社招
校招