计算地图:提前计算价值

时间:2010-07-08 15:26:57

标签: java concurrency guava memoization

我有一个computing mapsoft values),我用它来缓存昂贵计算的结果。

现在我有一种情况,我知道在接下来的几秒钟内可能会查找某个特定的密钥。这个密钥的计算成本也比大多数都贵。

我想在最小优先级线程中预先计算该值,以便在最终请求该值时,它已经被缓存,从而缩短了响应时间。

这样做的好方法是:

  1. 我可以控制执行计算的线程(特别是它的优先级)。
  2. 避免重复工作,即计算仅进行一次。如果计算任务已经在运行,那么调用线程将等待该任务,而不是再次计算该值(FutureTask实现了这一点。使用Guava的计算映射,如果你只调用get,这是真的,但如果你将其与put的调用混合。)
  3. “预先计算值”方法是异步和幂等的。如果计算已在进行中,则应立即返回,而不等待该计算完成。
  4. 避免优先级倒置,例如如果高优先级线程在中等优先级线程执行某些不相关的操作但是计算任务在低优先级线程上排队时请求该值,则高优先级线程不得缺乏。也许这可以通过暂时提高计算线程的优先级和/或在调用线程上运行计算来实现。
  5. 如何协调所涉及的所有线程?


    其他信息
    我的应用程序中的计算是图像过滤操作,这意味着它们都是CPU绑定的。这些操作包括仿射变换(范围从50μs到1ms)和卷积(最多10ms)。当然,不同线程优先级的有效性取决于操作系统抢占较大任务的能力。

4 个答案:

答案 0 :(得分:8)

您可以使用带有ComputedMap的Future安排“仅执行一次”后台计算。未来代表了计算价值的任务。未来由ComputedMap创建,同时传递给ExecutorService以进行后台执行。执行程序可以配置您自己的ThreadFactory实现,该实现创建低优先级线程,例如

class LowPriorityThreadFactory implements ThreadFactory
{
   public Thread newThread(Runnable r) {
     Tread t = new Thread(r);
     t.setPriority(MIN_PRIORITY);
     return t;
   }
}

当需要该值时,您的高优先级线程然后从地图中获取未来,并调用get()方法来检索结果,等待必要时计算结果。为了避免priority inversion,您需要在任务中添加一些额外的代码:

class HandlePriorityInversionTask extends FutureTask<ResultType>
{
   Integer priority;  // non null if set
   Integer originalPriority;
   Thread thread;
   public ResultType get() {
      if (!isDone()) 
         setPriority(Thread.currentThread().getPriority());
      return super.get();
   }
   public void run() {
      synchronized (this) {
         thread = Thread.currentThread();
         originalPriority = thread.getPriority();
         if (priority!=null) setPriority(priority);
      } 
      super.run();
   }
   protected synchronized void done() {
         if (originalPriority!=null) setPriority(originalPriority);
         thread = null;
   }

   void synchronized setPriority(int priority) {
       this.priority = Integer.valueOf(priority);
       if (thread!=null)
          thread.setPriority(priority);
   }
}

如果任务尚未完成,则负责将任务的优先级提高到调用get()的线程的优先级,并在任务完成时(正常或其他方式)将优先级返回到原始优先级。 (为了保持简洁,代码不会检查优先级是否确实更高,但是很容易添加。)

当高优先级任务调用get()时,未来可能尚未开始执行。您可能想通过在执行程序服务使用的线程数上设置一个较大的上限来避免这种情况,但这可能是一个坏主意,因为每个线程可能以高优先级运行,消耗尽可能多的CPU操作系统将其切换出来。该池的大小可能与硬件线程的数量相同,例如:将池的大小调整为Runtime.availableProcessors()。如果任务尚未开始执行,而不是等待执行程序安排它(这是一种优先级倒置形式,因为您的高优先级线程正在等待低优先级线程完成),那么您可以选择从当前执行程序并在仅运行高优先级线程的执行程序上重新提交。

答案 1 :(得分:2)

协调此类情况的一种常见方法是使用其值为FutureTask对象的映射。因此,作为一个例子,我从我的Web服务器中编写了一些代码,其基本思想是,对于给定的参数,我们看看是否已经存在FutureTask(意味着已经调度了该参数的计算),以及如果是这样我们等待它。在这个例子中,我们以其他方式安排查找,但如果需要,可以通过单独的调用在其他地方完成:

  private final ConcurrentMap<WordLookupJob, Future<CharSequence>> cache = ...

  private Future<CharSequence> getOrScheduleLookup(final WordLookupJob word) {
    Future<CharSequence> f = cache.get(word);
    if (f == null) {
      Callable<CharSequence> ex = new Callable<CharSequence>() {
        public CharSequence call() throws Exception {
          return doCalculation(word);
        }
      };
      Future<CharSequence> ft = executor.submit(ex);
      f = cache.putIfAbsent(word, ft);
      if (f != null) {
        // somebody slipped in with the same word -- cancel the
        // lookup we've just started and return the previous one
        ft.cancel(true);
      } else {
        f = ft;
      }
    }
    return f;
  }

就线程优先级而言:我想知道这是否会实现您的想法?我不太明白你在等待线程上面提高查找优先级的观点:如果线程正在等待,那么它正在等待,无论其他线程的相对优先级是什么......(你可能想看看一些我在thread prioritiesthread scheduling上写过的文章,但长话短说,我不确定改变优先级肯定会给你带来你想要的东西。)

答案 2 :(得分:2)

我怀疑你是通过专注于线程优先级来走错路。通常,由于I / O(内存不足数据)与CPU绑定(逻辑计算)相比,缓存所保存的数据的计算成本很高。如果您正在预测猜测用户未来的操作,例如查看未读电子邮件,则表明您的工作可能受I / O限制。这意味着只要不发生线程饥饿(哪些调度程序不允许),使用线程优先级玩游戏将无法提供太多的性能提升。

如果成本是I / O调用,则后台线程被阻塞,等待数据到达并处理该数据应该相当便宜(例如反序列化)。由于线程优先级的更改不会提供很多加速,因此在后台线程池上异步执行工作应该就足够了。如果缓存未命中损失过高,那么使用多层缓存往往有助于进一步减少用户感知延迟。

答案 3 :(得分:1)

作为线程优先级的替代方法,只有在没有高优先级任务正在进行时,才能执行低优先级任务。这是一个简单的方法:

AtomicInteger highPriorityCount = new AtomicInteger();

void highPriorityTask() {
  highPriorityCount.incrementAndGet();
  try {
    highPriorityImpl();
  } finally {
    highPriorityCount.decrementAndGet();  
  }
}

void lowPriorityTask() {
  if (highPriorityCount.get() == 0) {
    lowPriorityImpl();
  }
}

在您的用例中,两个Impl()方法都会在计算映射上调用get(),在同一个线程中调用highPriorityImpl(),在不同的线程中调用lowPriorityImpl()。

您可以编写一个更复杂的版本,推迟低优先级任务,直到高优先级任务完成,并限制并发低优先级任务的数量。