我写的由 GCD 代码支持的读写器锁导致并行测试中的死锁

Posted

技术标签:

【中文标题】我写的由 GCD 代码支持的读写器锁导致并行测试中的死锁【英文标题】:The reader writer lock I wrote backed by GCD code causes a deadlock in parallel test 【发布时间】:2019-08-02 04:05:46 【问题描述】:

我在 GCD 中实现了这个读写器锁,但在并行测试中失败了。我可以解释为什么它失败了吗?

这是用于 ios 开发的。该代码基于Objective C。我在GCD中编写了一个带有读写器锁的RWCache,用于数据保护。

@interface RWCache : NSObject

- (void)setObject:(id)object forKey:(id <NSCopying>)key;

- (id)objectForKey:(id <NSCopying>)key;

@end

@interface RWCache()
@property (nonatomic, strong) NSMutableDictionary *memoryStorage;
@property (nonatomic, strong) dispatch_queue_t storageQueue;
@end

@implementation RWCache

- (instancetype)init 
    self = [super init];
    if (self) 
        _memoryStorage = [NSMutableDictionary new];
        _storageQueue = dispatch_queue_create("Storage Queue", DISPATCH_QUEUE_CONCURRENT);
    
    return self;


- (void)setObject:(id)object forKey:(id <NSCopying>)key 
    dispatch_barrier_async(self.storageQueue, ^
        self.memoryStorage[key] = object;
    );


- (id)objectForKey:(id <NSCopying>)key 
    __block id object = nil;
    dispatch_sync(self.storageQueue, ^
        object = self.memoryStorage[key];
    );
    return object;


@end


int main(int argc, const char * argv[]) 
    @autoreleasepool 
        RWCache *cache = [RWCache new];
        dispatch_queue_t testQueue = dispatch_queue_create("Test Queue", DISPATCH_QUEUE_CONCURRENT);
        dispatch_group_t group = dispatch_group_create();
        for (int i = 0; i < 100; i++) 
            dispatch_group_async(group, testQueue, ^
                [cache setObject:@(i) forKey:@(i)];
            );
            dispatch_group_async(group, testQueue, ^
                [cache objectForKey:@(i)];
            );
        
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    return 0;


如果没有死锁,程序退出0,否则程序挂起不退出。

【问题讨论】:

我不完全确定您的代码为什么会死锁,但这似乎是一种非常复杂的共享字典访问方式。一个简单的NSLock 就可以解决问题......并且效率提高大约 100 倍。 @JamesBucanek 你是对的。这可能效率不高。我只是尝试在 GCD 中实现读/写锁,看看它是如何工作的,我也想知道为什么它会导致这个演示的死锁。 我不会认为它的效率会降低。对其进行基准测试。上次我对它进行基准测试时,读写器速度要快得多...... @Rob,很公平。但我想区分fastefficient。我承认,在某些极端情况下,这样的读写方案可能会为这样的O(1) 操作运行得更快——但它不可能更高效。我的证据是,读取操作将涉及至少两个线程切换,以及由(可能是多个)信号量和屏障操作控制的块的伴随排队和出队。这不可能击败单个(可能没有争议的)信号量。 (“高效”定义为每瓦所做的功) @Rob Sidebar:对于 OP,这是一个练习,很好。我的观点是关于现实世界的应用程序,尤其是针对移动设备的应用程序——尽管我认为我们桌面开发人员也应该尽自己的一份力量来提高软件的效率。 【参考方案1】:

问题不在于读取器/写入器模式,本身,,而是因为此代码中的一般线程爆炸。请参阅 WWDC 2015 视频 Building Responsive and Efficient Apps with GCD 中的“线程爆炸导致死锁”讨论。 WWDC 2016 Concurrent Programming With GCD in Swift 3 也是一个不错的视频。在这两个链接中,我会将您带到视频的相关部分,但都值得完整观看。

归根结底,您正在耗尽 GCD 线程池中数量非常有限的工作线程。只有 64 个。但是你有 100 个带屏障的写入,这意味着在该队列上的所有其他内容完成之前,调度的块无法运行。它们散布着 100 次读取,因为它们是同步的,所以会阻塞您调度它的工作线程,直到它返回。

让我们将其简化为能体现问题的更简单的东西:

dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 100; i++) 
    dispatch_async(queue2, ^
        dispatch_barrier_async(queue1, ^
            NSLog(@"barrier async %d", i);
        );
        dispatch_sync(queue1, ^
            NSLog(@"sync %d", i);
        );
    );

NSLog(@"done dispatching all blocks to queue1");

这会产生类似的东西:

开始 完成将所有块分派到 queue1 屏障异步 0 同步 0

它会死锁。

但如果我们将其限制为不超过 30 个项目可以在 queue2 上同时运行,那么问题就消失了:

dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(30);

for (int i = 0; i < 100; i++) 
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    dispatch_async(queue2, ^
        dispatch_barrier_async(queue1, ^
            NSLog(@"barrier async %d", i);
        );
        dispatch_sync(queue1, ^
            NSLog(@"sync %d", i);
        );
        dispatch_semaphore_signal(semaphore);
    );

NSLog(@"done dispatching all blocks to queue1");

或者,另一种方法是使用dispatch_apply,它实际上是一个并行化的for 循环,但在任何给定时刻将并发任务的数量限制为机器上的核心数量(使我们远低于阈值耗尽工作线程):

dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);

dispatch_apply(100, queue2, ^(size_t i) 
    dispatch_barrier_async(queue1, ^
        NSLog(@"barrier async %ld", i);
    );
    dispatch_sync(queue1, ^
        NSLog(@"sync %ld", i);
    );
);
NSLog(@"done dispatching all blocks to queue1");

【讨论】:

在看到你的答案之前,我有自己的猜测并写了here。这是太棒了。如果我的假设与您的相似,请告诉我。 是的,同样的想法,虽然我可能会狡辩,因为我认为如果不讨论“工作线程”和 GCD 的工作线程“池”,任何讨论都是不完整的。这是 GCD 施加的约束(尽管并非完全不合理)。我建议您观看Thread Explosion Causing Deadlock 视频。

以上是关于我写的由 GCD 代码支持的读写器锁导致并行测试中的死锁的主要内容,如果未能解决你的问题,请参考以下文章

Delphi 高效读写锁

读写锁ReentrantReadWriteLock源代码浅析

线程同步之读写锁

大神进,易语言sqlite3支持多线程读写吗

AQS系列- ReentrantReadWriteLock读写锁的加锁

读写锁(ReadWriteLock)