Scala期货:不确定性输出

时间:2019-01-27 13:25:51

标签: scala future

我是Scala的新手,我正在通过创建一些重试方案来练习Future库。这样做,我得到了以下代码:

import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._

object Retries extends App {

  var retries = 0

  def resetRetries(): Unit = retries = 0

  def calc() = if (retries > 3) 10 else {
    retries += 1
    println(s"I am thread ${Thread.currentThread().getId} This is going to fail. Retry count $retries")
    throw new IllegalArgumentException("This failed")
  }

  def fCalc(): Future[Int] = Future(calc())

  resetRetries()

  val ff = fCalc() // 0 - should fail
    .fallbackTo(fCalc()) // 1 - should fail
    .fallbackTo(fCalc()) // 2 - should fail
    .fallbackTo(fCalc()) // 3 - should fail
    .fallbackTo(fCalc()) // 4 - should be a success

  Await.ready(ff, 10.second)

  println(ff.isCompleted)
  println(ff.value)
}

每次运行此代码时,都会得到不同的结果。我得到的结果样本如下

输出1

I am thread 12 This is going to fail. Retry count 1
I am thread 14 This is going to fail. Retry count 3
I am thread 13 This is going to fail. Retry count 2
I am thread 11 This is going to fail. Retry count 1
I am thread 12 This is going to fail. Retry count 4
true
Some(Failure(java.lang.IllegalArgumentException: This failed))

输出2

I am thread 12 This is going to fail. Retry count 2
I am thread 11 This is going to fail. Retry count 1
I am thread 13 This is going to fail. Retry count 3
I am thread 14 This is going to fail. Retry count 4
true
Some(Success(10))

输出3

I am thread 12 This is going to fail. Retry count 1
I am thread 11 This is going to fail. Retry count 1
I am thread 12 This is going to fail. Retry count 2
I am thread 12 This is going to fail. Retry count 3
I am thread 12 This is going to fail. Retry count 4
true
Some(Failure(java.lang.IllegalArgumentException: This failed))

结果并不总是在成功和失败之间交替出现。在成功运行之前,可能会有不止两次失败。

据我了解,应该只有4条日志“我是线程x这将失败。请重试计数x”,这些日志应为以下内容:

I am thread a This is going to fail. Retry count 1
I am thread b This is going to fail. Retry count 2
I am thread c This is going to fail. Retry count 3
I am thread d This is going to fail. Retry count 4

不一定按此顺序进行-因为我不知道Scala线程模型是如何工作的-但您明白我的意思。但是,我得到了这个不确定的输出,我无法处理。 我的问题是:这种不确定性输出来自何处?

我想提到以下重试机制始终产生相同的结果:

import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._

object Retries extends App {

  var retries = 0

  def resetRetries(): Unit = retries = 0

  def calc() = if (retries > 3) 10 else {
    retries += 1
    println(s"I am thread ${Thread.currentThread().getId} This is going to fail. Retry count $retries")
    throw new IllegalArgumentException("This failed")
  }

  def retry[T](op: => T)(retries: Int): Future[T] = Future(op) recoverWith { case _ if retries > 0 => retry(op)(retries - 1) }

  resetRetries()
  val retriableFuture: Future[Future[Int]] = retry(calc())(5)
  Await.ready(retriableFuture, 10 second)

  println(retriableFuture.isCompleted)
  println(retriableFuture.value)
}

输出

I am thread 11 This is going to fail. Retry count 1
I am thread 12 This is going to fail. Retry count 2
I am thread 11 This is going to fail. Retry count 3
I am thread 12 This is going to fail. Retry count 4
true
Some(Success(10))

尽管我减少了重试次数(retry(calc())(3)),但结果却是预期的失败

I am thread 11 This is going to fail. Retry count 1
I am thread 12 This is going to fail. Retry count 2
I am thread 11 This is going to fail. Retry count 3
I am thread 12 This is going to fail. Retry count 4
true
Some(Failure(java.lang.IllegalArgumentException: This failed))

3 个答案:

答案 0 :(得分:4)

虽然从技术上讲@Tim是正确的,但我认为他并没有真正回答这个问题。

我相信,造成混淆的真正原因是您对结构的误解:

f.fallbackTo(Future(calc()))

确实。以及它与

有何不同
f.recoverWith({ case _ => Future(calc())})

有两个重要区别:

  1. fallbackTo情况下,Future(calc())立即创建,因此(几乎)立即开始执行calc()。因此,原始的未来和后备的未来会同时运行。对于recoverWith,仅在原始将来失败后才创建后备将来。这种差异会影响记录顺序。同样,这意味着对var retries的访问是并发的,因此您可能会看到所有线程实际上由于丢失了对retries的某些更新而失败的情况。

  2. 另一个棘手的问题是fallbackTodocumented,因为(突出显示是我的)

  

创建一个新的future,如果成功完成,则保存该future的结果;如果成功完成,则保存该future的结果。 如果两个期货都失败,则产生的期货将持有第一个期货的可抛对象。

这种差异不会真正影响您的示例,因为在所有失败的尝试中抛出的异常都是相同的,但是如果它们不同,则可能会影响结果。例如,如果您将代码修改为:

  def calc(attempt: Int) = if (retries > 3) 10 else {
    retries += 1
    println(s"I am thread ${Thread.currentThread().getId} This is going to fail. Retry count $retries")
    throw new IllegalArgumentException(s"This failed $attempt")
  }

  def fCalc(attempt: Int): Future[Int] = Future(calc(attempt))

  val ff = fCalc(1) // 0 - should fail
      .fallbackTo(fCalc(2)) // 1 - should fail
      .fallbackTo(fCalc(3)) // 2 - should fail
      .fallbackTo(fCalc(4)) // 3 - should fail
      .fallbackTo(fCalc(5)) // 4 - should be a success

那么您应该获得这两个结果之一

Some(Failure(java.lang.IllegalArgumentException: This failed 1))
Some(Success(10))

,绝没有其他任何“失败”的值。

请注意,这里我明确地传递了attempt,以不达到retries上的竞争条件。


回答更多评论(1月28日)

我在上一个示例中明确传递attempt的原因是,这是确保由逻辑上第一个IllegalArgumentException创建的calc会获得1的最简单方法作为它在所有(甚至不是很现实)线程计划中的值。

如果您只想让所有日志具有不同的值,则有一种更简单的方法:使用局部变量!

  def calc() = {
    val retries = atomicRetries.getAndIncrement()
    if (retries > 3) 10 
    else {
      println(s"I am thread ${Thread.currentThread().getId} This is going to fail. Retry count $retries")
      throw new IllegalArgumentException(s"This failed $retries")
    }
  }

通过这种方式可以避免经典的TOCTOU问题。

答案 1 :(得分:2)

这不是Scala问题,而是一个更通用的多线程问题,其值为retries。您有多个线程在没有任何同步的情况下读写此值,因此您无法预测每个线程何时运行或它将看到什么值。

具体问题似乎在于您正在测试retries,然后稍后对其进行更新。可能所有四个线程都会在它们中的任何一个更新值之前测试该值。在这种情况下,他们都会看到0并抛出错误。

解决方案是将retries转换为AtomicInteger并使用getAndIncrement。这将自动获取该值并将其递增,因此每个线程将看到适当的值。


更新以下评论:另一个答案说明了为什么同时启动多个线程的原因,因此在此不再赘述。在多个线程并行运行的情况下,日志记录的顺序始终是不确定的。

答案 2 :(得分:0)

这最终对我有用:

(以下用于calc()方法的代码充分解决了有关日志记录重复和期货的不确定性结果的问题)

var time = 0
  var resetTries = time = 0

  def calc() = this.synchronized {
    if (time > 3) 10 else {
      time += 1
      println(s"I am thread ${Thread.currentThread().getId} This is going to fail. Retry count $time") // For debugging purposes
      throw new IllegalStateException(("not yet"))
    }
  }

不需要AtomicInteger-在我看来,事情变得更加复杂。需要synchronised包装器。

我必须强调一个事实,那就是这只是出于演示目的,并且在生产代码中使用这种设计可能不是最好的主意(阻止对calc方法的调用)。应该改用recoverWith实现。

感谢@SergGr,@Tim和@MichalPolitowksi的帮助