可重复使用的Executor,ServiceExecutor等接口的目的

时间:2015-12-10 23:58:09

标签: java multithreading javafx concurrency executorservice

我正在使用javaFX构建一个GUI应用程序,该应用程序支持长时间运行的CPU密集型操作,如Prime95或Orthos。

我遇到的一个问题是试图让计数器很好地增加。如果您考虑具有毫秒分辨率的递增计数器的ElapsedTime字段,我需要的是UI线程上的作业,以便在相应的调用elapsedTimeTextField.setText("00:00:00.001")之前1ms调用elapsedTimeTextField.setText("00:00:00.002")。我还需要让UI线程在这两个调用之间执行更重要的工作。

构造代码来执行此操作非常繁琐,并且导致我们的许多控制器类创建的线程只是循环代码类似于:

Thread worker = new Thread(this::doUpdates);
worker.start();

//...

private void doUpdates(){
  while(true){
    String computedTime = computeTimeToDisplay();
    runLaterOnUI(() -> textField.setText(computedTime));
    sleep(DUTY_CYCLE_DOWNTIME);
  }
}

虽然完成了这项工作,但它不利,因为:

  • 难以进行单元测试:从测试环境开始,您必须修改此代码,以便在完成第一次传递时给出某种信号(通常是倒计时锁存器),或者您必须做傻非确定性的任意sleep() s
  • 它没有任何退避:如果UI线程充满了作业,这段代码会加剧问题。某种重新排队方案,其中停机时间考虑了作业的延迟和某种类型的硬编码睡眠是优选的,因为这意味着如果UI工作被淹没,我们不会问它过度地工作。
  • 没有线程默认处理程序的集中式异常处理。这意味着如果在computeTimeToDisplay()方法中引发异常(或者为此,在runLaterOnUI调用或sleep()调用中)文本字段将不再更新

我已经单独地解决了这些问题,但我没有任何明显和可重复使用的习惯来解决这三个问题。

怀疑 FutureTaskExecutorServiceExecutor等类(java.util.concurrent中的类不是锁或集合的包可以帮助我实现这个目标,但我不确定如何使用它们。

有人可以建议阅读一些文档和一些可以帮助我追求这些目标的习语吗?对于这种并发工作,是否存在一个同意的成语 - 不涉及匿名类并且包含最少的样板?

2 个答案:

答案 0 :(得分:1)

我建议使用核心池大小为1的{​​{3}},并选择线程优先级为Thread.NORM_PRIORITY + 1(使用ScheduledThreadPoolExecutor创建ThreadFactory UI线程的高于标准优先级的权限 - 这将允许您使用ScheduledThreadPoolExecutor#scheduleAtFixedRate计划诸如计数器增量之类的任务。不要在此执行程序上执行除UI任务之外的任何操作 - 在具有标准优先级的单独ThreadPoolExecutor上执行CPU任务;如果你有例如然后,16个逻辑核心创建一个具有16个核心线程的ThreadPoolExecutor,以便在UI线程空闲时充分利用您的计算机,并让虚拟机负责确保UI线程在其执行时执行其作业。应该是。

答案 1 :(得分:1)

你的问题是多方面的,我不会假装我理解所有这些。这个答案只涉及问题的一部分。

  

它没有任何退避:如果UI线程充满了作业,这段代码将加剧问题。某种重新排队方案,其中停机时间考虑了作业的延迟和某种类型的硬编码睡眠是优选的,因为这意味着如果UI工作被淹没,我们不会要求它过度地工作。

内置的java.util.concurrent类(例如Task,Service和ScheduledService)包括将非UI线程中的消息更新发送到UI线程的设施,其方式不会泛滥UI线程。你可以直接使用这些课程(这似乎是可取的,但也许这种感觉对我很幼稚,因为我不完全理解你的要求)。或者,如果您没有直接使用java.util.concurrent,则可以在代码中实现类似的自定义工具。

以下是Task实现的相关代码:

/**
 * Used to send message updates in a thread-safe manner from the subclass
 * to the FX application thread. AtomicReference is used so as to coalesce
 * updates such that we don't flood the event queue.
 */
private AtomicReference<String> messageUpdate = new AtomicReference<>();

private final StringProperty message = new SimpleStringProperty(this, "message", "");

/**
 * Updates the <code>message</code> property. Calls to updateMessage
 * are coalesced and run later on the FX application thread, so calls
 * to updateMessage, even from the FX Application thread, may not
 * necessarily result in immediate updates to this property, and
 * intermediate message values may be coalesced to save on event
 * notifications.
 * <p>
 *     <em>This method is safe to be called from any thread.</em>
 * </p>
 *
 * @param message the new message
 */
protected void updateMessage(String message) {
    if (isFxApplicationThread()) {
        this.message.set(message);
    } else {
        // As with the workDone, it might be that the background thread
        // will update this message quite frequently, and we need
        // to throttle the updates so as not to completely clobber
        // the event dispatching system.
        if (messageUpdate.getAndSet(message) == null) {
            runLater(new Runnable() {
                @Override public void run() {
                    final String message = messageUpdate.getAndSet(null);
                    Task.this.message.set(message);
                }
            });
        }
    }
}

该代码的工作原理是确保仅在UI处理(即呈现)上次更新时才进行runLater调用。

JavaFX 8系统内部在脉冲系统上运行。除非在UI线程或一般系统减速上存在异常耗时的操作,否则每个脉冲通常每秒发生60次,或大约每16-17毫秒发生一次。

你提到以下内容:

  

我需要的是在UI线程上调用elapsedTimeTextField.setText(&#34; 00:00:00.001&#34;)在相应的调用elatedTimeTextField.setText(&#34; 00: 00:00.002&#34)

但是,您可以从JavaFX体系结构描述中看到,每秒更新文本超过60次是没有意义的,因为永远不会呈现其他更新。来自Task的上述示例代码通过确保UI更新请求仅在UI更新线程实际反映UI中的新值时发布来处理此事。

一些一般性建议

这只是建议,它并没有直接解决你的问题,根据你的意愿去做,有些甚至可能与你的情况或问题无关。

  1. 明确问题中要解决的问题。这有时比描述您正在经历并试图解决的症状更重要。它还有助于防止XY questions
  2. 从一开始就明确你正在做什么来解决问题。 mcve有时可以提供帮助。
  3. 例如,您的初始问题陈述并未声明您可能拥有10,000个控制器或提供您称之为控制器的代码。没有太多关于任务的预期时间长度的信息,表示任务进度和结果的UI显示是什么,为什么毫秒精度级别可能对显示很重要,如果任务结果需要合并,如果任务可以拆分和运行同时,你正在使用多少线程等。
  4. 不要尝试从ConcurrentLinkedQueue这样的原语开发自己的更高级别的并发工具。
  5. 对于后端细分工作职位,请使用high level concurrency utilities from Java SE,例如ExecutorsForkJoinBlockingQueue
  6. 使用JavaFX concurrency utilitiesTask编排并同步后端作业的输出。
  7. 知道高级并发实用程序和JavaFX并发工具可以一起使用,例如this example。即,并发工具的选择并不一定是一种情况。
  8. immutable objects的广泛使用可以成为并行开发的救星。
  9. 如果您要进行大量并发开发,请花时间详细研究并发编程的高质量资源,例如Concurrency in Practice
  10. 一般来说,并发通常很难做到正确。