使用无锁读取实现线程安全的无效缓存?

时间:2013-06-08 16:24:51

标签: objective-c thread-safety lock-free memory-barriers

我试图在我的脑海中推断如何使用大致相同的API实现引用计数值的线程安全缓存机制:(注意:我使用的是Objective-C语法,但问题不在于语言特定的)

typedef id (^InvalidatingLazyGenerator)();

@interface InvalidatingLazyObject : NSObject

- (id)initWithGenerator: (InvalidatingLazyGenerator)generator;

@property (readonly) id value;

- (void)invalidate;

@end

当有人请求-value时,如果它有现有的缓存值,则应返回该值的-retain/-autoreleased版本。如果它没有值,或者该值无效,它应该使用在初始时传入的生成块生成一个,然后它应该为将来的任何读取缓存该值,直到有人调用-invalidate

假设我们不关心是否多次调用生成器块(即第二个读取器在第一个读取器位于生成器块中时到达),只要它返回的对象在发生时不会泄漏。第一遍, -wait-free实现这可能看起来像:

- (id)value
{
    id retVal = nil;
    @synchronized(self)
    {
        retVal = [mValue retain];
    }
    if (!retVal)
    {
        retVal = [[mGenerator() retain] retain]; // Once for the ivar and once for the return value
        id oldVal = nil;
        @synchronized(self)
        {
            oldVal = mValue;
            mValue = retVal;
        }
        [oldVal release];
    }
    return [retVal autorelease];
}

- (void)invalidate
{
    id val = nil;
    @synchronized(self)
    {
        val = mValue;
        mValue = nil;
    }
    [val release];
}

当然,这会导致糟糕的读取性能,因为并发读取是通过锁序列化的。读取器/写入器锁可以改善这一点,但在读取路径中仍然很慢。这里的性能目标是尽可能快地缓存读取(希望无锁)。如果我们必须计算一个新值,那么读取速度很慢,-invalidate速度很慢。

所以...我试图找出一种方法来使读取锁定/等待。我的第一个(有缺陷的 - 见下文)思想涉及添加一个失效计数器,其值是原子的,单调递增的并使用记忆障碍读取。它看起来像这样:

- (id)value
{
    // I think we don't need a memory barrier before this first read, because
    // a stale read of the count can only cause us to generate a value unnecessarily,
    // but should never cause us to return a stale value.
    const int64_t startCount = mWriteCount;

    id retVal = [mValue retain];

    OSMemoryBarrier(); // But we definitely want a "fresh" read here.
    const int64_t endCount = mWriteCount;

    if (retVal && startCount == endCount)
    {
        return [retVal autorelease];
    }

    // Now we're in the slow path
    retVal = [mGenerator() retain]; // we assume generator has given us an autoreleased object

    @synchronized(self)
    {
        mValue = retVal;
        OSAtomicIncrement64Barrier(&mWriteCount);
    }
    return retVal;
}

- (void)invalidate
{
    id value = nil;
    @synchronized(self)
    {
        value = mValue;
        mValue = nil;
        OSAtomicIncrement64Barrier(&mWriteCount);
    }
    [value release];
}

但我已经可以在这里看到问题。例如,读取路径中的[mValue retain]:我们需要保留值,但是在读取mValue和调用-retain之间的时间内,另一个线程可能是-invalidate,导致该值在保留调用发生时被解除分配。所以这种方法不会起作用。可能还有其他问题。

有没有人已经做过这样的事情并且想分享?或者指向野外类似的东西?

1 个答案:

答案 0 :(得分:0)

我最终采用这种方法解决了这个问题:Read-Copy-Update。它似乎工作得很好 - 读取性能比基于锁定的方法快50倍(但这并不奇怪。)