【问题标题】:Limiting object allocation over multiple threads限制多线程上的对象分配
【发布时间】:2012-06-24 10:29:05
【问题描述】:

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

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

编辑:我采用了 CAS 方法,它似乎工作得很好。

【问题讨论】:

  • 您应该使用适当的语言标签。
  • 谢谢斧头;我已经更新了标签。
  • 我不是最新的 c++ 线程,所以我将其作为评论而不是答案。在 c# 中,使用 Interlocked 至少可以避免您使用的一些锁(更改 SendResults 是最简单的)。 “原子”和“比较和交换”是该主题的关键术语。有关 c++ 的一些链接,请参阅此链接:stackoverflow.com/questions/54188/… 和 stackoverflow.com/questions/930897/…
  • 另见 a reference 的新 C++11 原子库和 compiler support
  • 您确定性能下降不是由新的缓存限制引起的吗?您是否尝试过将缓存限制配置得非常高?

标签: multithreading concurrency


【解决方案1】:

首先,不要使用锁,而是使用原子递减和比较和交换来操作您的计数器。语法因您的编译器而异;在 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;
}

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

【讨论】:

  • 在这种情况下,原子是要走的路。 +1
【解决方案2】:

在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。

【讨论】:

  • 在您上面的代码中,如果 resultsToSend 在输入时 >= 0,那么在您执行 totalResultsCached -= resultsToSend 时它的值不是总是 0 吗?
  • 这样会更有效率。但这取决于他的解决方案是如何构建的。如果他的问题中的两种方法可以在不同的线程上运行,那么在他的原始代码中,一种方法可能是将结果放入缓存中,同时发送结果。在您的代码中,它会更加“矮胖”。如果 totalResultsCached 处于最大值,则在 SendResults 发送它必须处理的所有结果之前不会放入更多结果。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-04-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-28
  • 1970-01-01
相关资源
最近更新 更多