Scala:使用ListBuffer附加并行执行不会产生预期结果

时间:2016-05-19 00:22:48

标签: scala listbuffer

我知道我在mutable.ListBuffer做错了,但我无法弄清楚如何修复它(以及对问题的正确解释)。

我简化了下面的代码以重现行为。

我基本上尝试并行运行函数,以便在我的第一个列表被处理时将元素添加到列表中。我最终“失去”元素。

import java.util.Properties

import scala.collection.mutable.ListBuffer
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}


import scala.concurrent.{ExecutionContext}
import ExecutionContext.Implicits.global


object MyTestObject {

  var listBufferOfInts = new ListBuffer[Int]() // files that are processed

  def runFunction(): Int = {
    listBufferOfInts = new ListBuffer[Int]()
    val inputListOfInts = 1 to 1000
    val fut = Future.traverse(inputListOfInts) { i =>
      Future {
        appendElem(i)
      }
    }
    Await.ready(fut, Duration.Inf)
    listBufferOfInts.length
  }

  def appendElem(elem: Int): Unit = {
    listBufferOfInts ++= List(elem)
  }
}

MyTestObject.runFunction()
MyTestObject.runFunction()
MyTestObject.runFunction()

返回:

res0: Int = 937
res1: Int = 992
res2: Int = 997

显然我希望1000能够一直返回。如何修复我的代码以保持“架构”但让我的ListBuffer“同步”?

3 个答案:

答案 0 :(得分:3)

我不知道究竟是什么问题,因为你说你简化了它,但你仍然有一个明显的竞争条件,多个线程修改一个可变集合,这是非常糟糕的。正如其他答案所指出的,你需要一些锁定,这样只有一个线程可以同时修改集合。如果你的计算量很大,那么将结果以同步的方式附加到缓冲区不应该显着影响性能,但是如果有疑问则总是测量。

但是不需要同步,你可以做其他事情,没有变量和可变状态。让每个Future返回您的部分结果,然后将它们合并到一个列表中,事实上Future.traverse就是这样。

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

def runFunction: Int = {
  val inputListOfInts = 1 to 1000

  val fut: Future[List[Int]] = Future.traverse(inputListOfInts.toList) { i =>
    Future { 
      // some heavy calculations on i
      i * 4
    }
  }

  val listOfInts = Await.result(fut, Duration.Inf)
  listOfInts.size
}

Future.traverse已经为您提供了一个包含所有结果的不可变列表,无需将它们附加到可变缓冲区。 不用说,你总会得到1000

@ List.fill(10000)(runFunction).exists(_ != 1000) 
res18: Boolean = false

答案 1 :(得分:1)

我不确定上面是否显示了您正在尝试正确执行的操作。也许问题是你实际上正在共享一个var ListBuffer,你可以在runFunction中重新初始化。

当我把它拿出来时,我收集了我正确期待的所有事件:

import java.util.Properties

import scala.collection.mutable.ListBuffer
import scala.concurrent.duration.Duration
import scala.concurrent.{ Await, Future }

import scala.concurrent.{ ExecutionContext }
import ExecutionContext.Implicits.global

object BrokenTestObject extends App {

  var listBufferOfInts = ( new ListBuffer[Int]() )

  def runFunction(): Int = {
    val inputListOfInts = 1 to 1000
    val fut = Future.traverse(inputListOfInts) { i =>
      Future {
        appendElem(i)
      }
    }
    Await.ready(fut, Duration.Inf)
    listBufferOfInts.length
  }

  def appendElem(elem: Int): Unit = {
    listBufferOfInts.append( elem )
  }

  BrokenTestObject.runFunction()
  BrokenTestObject.runFunction()
  BrokenTestObject.runFunction()

  println(s"collected ${listBufferOfInts.length} elements")
}

如果您确实遇到同步问题,可以使用以下内容:

import java.util.Properties

import scala.collection.mutable.ListBuffer
import scala.concurrent.duration.Duration
import scala.concurrent.{ Await, Future }

import scala.concurrent.{ ExecutionContext }
import ExecutionContext.Implicits.global

class WrappedListBuffer(val lb: ListBuffer[Int]) {
  def append(i: Int) {
    this.synchronized {
      lb.append(i)
    }
  }
}

object MyTestObject extends App {

  var listBufferOfInts = new WrappedListBuffer( new ListBuffer[Int]() )

  def runFunction(): Int = {
    val inputListOfInts = 1 to 1000
    val fut = Future.traverse(inputListOfInts) { i =>
      Future {
        appendElem(i)
      }
    }
    Await.ready(fut, Duration.Inf)
    listBufferOfInts.lb.length
  }

  def appendElem(elem: Int): Unit = {
    listBufferOfInts.append( elem )
  }

  MyTestObject.runFunction()
  MyTestObject.runFunction()
  MyTestObject.runFunction()

  println(s"collected ${listBufferOfInts.lb.size} elements")
}

答案 2 :(得分:0)

更改

listBufferOfInts ++= List(elem)

synchronized {
    listBufferOfInts ++= List(elem)
}

让它发挥作用。可能会成为性能问题?我仍然对解释感兴趣,也许是一种更好的做事方式!