限制多个线程上的对象分配

时间:2012-06-23 19:17:54

标签: multithreading concurrency

我有一个应用程序,它检索并缓存客户端查询的结果,并将结果从缓存发送到客户端。

我对可以在任何时间缓存的项目数量有限制,并且在处理大量并发请求时,跟踪此限制已大大降低了应用程序性能。有没有更好的方法来解决这个问题,而不经常锁定,这可能会提高性能?

编辑:我已经采用了CAS方法,它看起来效果很好。

2 个答案:

答案 0 :(得分:3)

首先,不是使用锁,而是使用原子减量和比较和交换来操纵你的计数器。这种语法因编译器而异;在海湾合作委员会中你可以做类似的事情:

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;
}

对这样的请求进行批处理可以降低线程访问共享数据的频率,从而降低争用和缓存流失的数量。但是,如前所述,它会使您的限制不那么精确,因此需要仔细调整以获得正确的平衡。

答案 1 :(得分:1)

SendResults中,尝试在处理结果后仅更新totalResultsCached一次。这将最大限度地减少获取/释放锁定所花费的时间。

void SendResults( int resultsToSend, Request *request )
{
    for (int i=0; i<resultsToSend; ++i)
    {
        send(request.remove())
    }

    lock totalResultsCached 
    totalResultsCached -= resultsToSend;
    unlock totalResultsCached 
}

如果resultsToSend通常为1,那么我的建议不会产生太大影响。

此外,在达到缓存限制后,可能会在ResultCallback中删除一些额外请求,因为SendResults在发送每个请求后没有立即更新totalResultsCached