首先,不要使用锁,而是使用原子递减和比较和交换来操作您的计数器。语法因您的编译器而异;在 GCC 中,您可能会执行以下操作:
long remaining_cache_slots;
void release() {
__sync_add_and_fetch(&remaining_cache_slots, 1);
}
// Returns false if we've hit our cache limit
bool acquire() {
long prev_value, new_value;
do {
prev_value = remaining_cache_slots;
if (prev_value <= 0) return false;
new_value = prev_value - 1;
} while(!__sync_bool_compare_and_swap(&remaining_cache_slots, prev_value, new_value));
return true;
}
这应该有助于减少争用窗口。但是,您仍然会在各处弹跳该缓存行,这在高请求率下会严重影响您的性能。
如果您愿意接受一定程度的浪费(即,允许缓存结果的数量 - 或者更确切地说,待处理的响应 - 略低于限制),您还有其他一些选择。一种是使缓存线程本地化(如果可能在您的设计中)。另一种方法是让每个线程保留一个“缓存令牌”池以供使用。
我所说的保留缓存令牌池的意思是每个线程都可以提前保留将 N 个条目插入缓存的权利。当该线程从缓存中删除一个条目时,它会将其添加到其令牌集中;如果它用完了令牌,它会尝试从全局池中获取它们,如果它有太多,它会将其中一些放回去。代码可能有点像这样:
long global_cache_token_pool;
__thread long thread_local_token_pool = 0;
// Release 10 tokens to the global pool when we go over 20
// The maximum waste for this scheme is 20 * nthreads
#define THREAD_TOKEN_POOL_HIGHWATER 20
#define THREAD_TOKEN_POOL_RELEASECT 10
// If we run out, acquire 5 tokens from the global pool
#define THREAD_TOKEN_POOL_ACQUIRECT 5
void release() {
thread_local_token_pool++;
if (thread_local_token_pool > THREAD_TOKEN_POOL_HIGHWATER) {
thread_local_token_pool -= THREAD_TOKEN_POOL_RELEASECT;
__sync_fetch_and_add(&global_token_pool, THREAD_TOKEN_POOL_RELEASECT);
}
}
bool acquire() {
if (thread_local_token_pool > 0) {
thread_local_token_pool--;
return true;
}
long prev_val, new_val, acquired;
do {
prev_val = global_token_pool;
acquired = std::min(THREAD_TOKEN_POOL_ACQUIRECT, prev_val);
if (acquired <= 0) return false;
new_val = prev_val - acquired;
} while (!__sync_bool_compare_and_swap(&remaining_cache_slots, prev_value, new_value));
thread_local_token_pool = acquired - 1;
return true;
}
像这样批量处理请求可以减少线程访问共享数据的频率,从而减少争用和缓存流失的数量。但是,如前所述,它会使您的限制不太精确,因此需要仔细调整以获得正确的平衡。