我试图在我的脑海中推断如何使用大致相同的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
,导致该值在保留调用发生时被解除分配。所以这种方法不会起作用。可能还有其他问题。
有没有人已经做过这样的事情并且想分享?或者指向野外类似的东西?