iOS 中使用的那些多线程与同步机制

这里分析的开源库及版本
AFNetworking:3.0
SDWebImage:4.0

一. AFURLSessionManager 中的 completionQueuecompletionGroup
在使用时,可以通过外部传入,也可以直接使用框架提供的group私有单例或者主队列。

dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
    if (self.completionHandler) {
        self.completionHandler(task.response, responseObject, error);
    }
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];
    });
});

这段代码是在 URLSession:task:didCompleteWithError 中执行的,意思是将自定义的完成回调放在 completionQueue 上执行。由于要发已完成的通知,所以放在主线程上执行发通知的操作。
这里并没有使用 dispatch_group_notifydispatch_group_wait 来同步,那么使用 group 的意义在哪呢?一开始我也不太明白,这里有人给出了见解,但是外部使用者如果认为 completionGroup 属性可读,而直接使用它来通知也会出现问题,因为内部使用的是私有单例,所以这块为啥用 group 还是有些困惑的。

二. AFURLSessionManager 中的 url_session_manager_processing_queue

dispatch_async(url_session_manager_processing_queue(), ^{
    NSError *serializationError = nil;
    responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:data error:&serializationError];
    ...
});

url_session_manager_processing_queue 是框架提供的自定义并发队列单例,这段代码也是在 URLSession:task:didCompleteWithError 中执行的,意思是将反序列化的操作放在并发队列上执行。

三. dispatch_semaphore_t

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }

        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return tasks;
}

使用信号量来同步读取。其实也可以用 dispatch_sync

四. NSLock

- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
            forTask:(NSURLSessionTask *)task
{
    NSParameterAssert(task);
    NSParameterAssert(delegate);

    [self.lock lock];
    self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
    [self addNotificationObserverForTask:task];
    [self.lock unlock];
}

由于 mutableTaskDelegatesKeyedByTaskIdentifier 是字典,多线程读取字典要加锁。其实也可以用 dispatch_barrier_async

五. synchronized
SDWebImageManager 中的 failedURLs 是一个集合。

@synchronized (self.failedURLs) {
    isFailedUrl = [self.failedURLs containsObject:url];
}
@synchronized (self.failedURLs) {
    [self.failedURLs addObject:url];
}
@synchronized (self.failedURLs) {
    [self.failedURLs removeObject:url];
}

这个同步机制没什么可说的。

六. 读写操作
AFHTTPRequestSerializer 中的 requestHeaderModificationQueue 是个并发队列,mutableHTTPRequestHeaders 是个字典,针对字典同时读是可以的,但是同时写或者同时读写是不可以的。

- (NSDictionary *)HTTPRequestHeaders {
    NSDictionary __block *value;
    dispatch_sync(self.requestHeaderModificationQueue, ^{
        value = [NSDictionary dictionaryWithDictionary:self.mutableHTTPRequestHeaders];
    });
    return value;
}
- (NSString *)valueForHTTPHeaderField:(NSString *)field {
    NSString __block *value;
    dispatch_sync(self.requestHeaderModificationQueue, ^{
        value = [self.mutableHTTPRequestHeaders valueForKey:field];
    });
    return value;
}

这两个操作都是字典的读操作,是可以同时读的。

- (void)setValue:(NSString *)value
forHTTPHeaderField:(NSString *)field
{
    dispatch_barrier_async(self.requestHeaderModificationQueue, ^{
        [self.mutableHTTPRequestHeaders setValue:value forKey:field];
    });
}
- (void)clearAuthorizationHeader {
    dispatch_barrier_async(self.requestHeaderModificationQueue, ^{
        [self.mutableHTTPRequestHeaders removeObjectForKey:@"Authorization"];
    });
}

这两个操作都是字典的写操作,必须与其他操作互斥。

再来看 AFAutoPurgingImageCache 中的例子,synchronizationQueue 是一个并发队列,cachedImages 是字典,currentMemoryUsage 是整数。

- (UInt64)memoryUsage {
    __block UInt64 result = 0;
    dispatch_sync(self.synchronizationQueue, ^{
        result = self.currentMemoryUsage;
    });
    return result;
}
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
    __block UIImage *image = nil;
    dispatch_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        image = [cachedImage accessImage];
    });
    return image;
}

这两个操作都是读操作,是可以同时读的。

- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
    dispatch_barrier_async(self.synchronizationQueue, ^{
        AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

        AFCachedImage *previousCachedImage = self.cachedImages[identifier];
        if (previousCachedImage != nil) {
            self.currentMemoryUsage -= previousCachedImage.totalBytes;
        }

        self.cachedImages[identifier] = cacheImage;
        self.currentMemoryUsage += cacheImage.totalBytes;
    });
    ...
}
- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
    __block BOOL removed = NO;
    dispatch_barrier_sync(self.synchronizationQueue, ^{
        AFCachedImage *cachedImage = self.cachedImages[identifier];
        if (cachedImage != nil) {
            [self.cachedImages removeObjectForKey:identifier];
            self.currentMemoryUsage -= cachedImage.totalBytes;
            removed = YES;
        }
    });
    return removed;
}

这两个操作都是字典写操作,必须与其他操作互斥。注意这里如果当前线程需要使用 block 中返回的数据,需要使用 dispatch_barrier_sync

在看 SDWebImageDownloaderOperation 中的例子,同样,barrierQueue 是并发队列,callbackBlocks 是数组。

- (nullable NSArray *)callbacksForKey:(NSString *)key {
    __block NSMutableArray *callbacks = nil;
    dispatch_sync(self.barrierQueue, ^{
        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
        [callbacks removeObjectIdenticalTo:[NSNull null]];
    });
    return [callbacks copy];    // strip mutability here
}

这个操作是字典的读操作。

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    dispatch_barrier_async(self.barrierQueue, ^{
        [self.callbackBlocks addObject:callbacks];
    });
    return callbacks;
}
- (BOOL)cancel:(nullable id)token {
    __block BOOL shouldCancel = NO;
    dispatch_barrier_sync(self.barrierQueue, ^{
        [self.callbackBlocks removeObjectIdenticalTo:token];
        if (self.callbackBlocks.count == 0) {
            shouldCancel = YES;
        }
    });
    if (shouldCancel) {
        [self cancel];
    }
    return shouldCancel;
}

这两个操作都是字典写操作,必须与其他操作互斥。

总结

  1. 什么时候用sync、什么时候用async?
    如果当前线程后续的操作需要用到 block 中的数据时,就用sync,否则用async。

  2. 什么时候用串行队列、什么时候用并发队列?
    如果 block 中的操作需要互斥的进行(比如写字典、文件IO),就用串行队列,否则用并发队列。

PS:
同步机制还包括 NSRecursiveLockNSConditionLockNSConditionOSSpinLockpthread_mutex等,后续会单独出一篇分析锁的文章。

你可能感兴趣的:(iOS 中使用的那些多线程与同步机制)