Scala: параллельный сбор в инициализаторе объекта приводит к зависанию программы

Я только что заметил тревожное поведение. Допустим, у меня есть отдельная программа, состоящая из единственного объекта:

object ParCollectionInInitializerTest {
  def doSomething { println("Doing something") }

  for (i <- (1 to 2).par) {
    println("Inside loop: " + i)
    doSomething
  }

  def main(args: Array[String]) {
  }
}

Программа совершенно невинна и, если диапазон, используемый в цикле for, не является параллельным, выполняется правильно и выдает следующий вывод:

Внутри петли: 1
Делать что-то
Внутри петли: 2
Делать что-то

К сожалению, при использовании параллельной коллекции программа просто зависает, даже не вызывая метод doSomething, поэтому вывод выглядит следующим образом:

Внутри петли: 2
Внутри петли: 1

И тогда программа зависает.
Это просто неприятная ошибка? Я использую scala-2.10.

1 ответ

Решение

Это неотъемлемая проблема, которая может возникнуть в Scala при освобождении ссылки на одноэлементный объект до завершения строительства. Это происходит из-за того, что другой поток пытается получить доступ к объекту ParCollectionInInitializerTest прежде чем он был полностью построен. Это не имеет ничего общего с main скорее, это связано с инициализацией объекта, который содержит main метод - попробуйте запустить это в REPL, набрав в выражении ParCollectionInInitializerTest и вы получите те же результаты. Это также не имеет никакого отношения к рабочим потокам fork-join, являющимся потоками демонов.

Одиночные объекты инициализируются лениво. Каждый одноэлементный объект может быть инициализирован только один раз. Это означает, что первый поток, который обращается к объекту (в вашем случае основной поток) должен захватить блокировку объекта, а затем инициализировать его. Каждый последующий поток должен ждать, пока основной поток инициализирует объект и, в конце концов, снимет блокировку. Именно так в Scala реализованы одноэлементные объекты.

В вашем случае рабочий поток параллельной коллекции пытается получить доступ к одноэлементному объекту для вызова doSomething, но не может сделать это, пока основной поток не завершит инициализацию объекта - так что он ждет. С другой стороны, основной поток ожидает в конструкторе, пока параллельная операция не завершится, что зависит от завершения всех рабочих потоков - основной поток все время удерживает блокировку инициализации для синглтона. Следовательно, возникает тупик.

Вы можете вызвать это поведение с фьючерсами из 2.10 или с простыми потоками, как показано ниже:

def execute(body: =>Unit) {
  val t = new Thread() {
    override def run() {
      body
    }
  }

  t.start()
  t.join()
}

object ParCollection {

  def doSomething() { println("Doing something") }

  execute {
    doSomething()
  }

}

Вставьте это в REPL, а затем напишите:

scala> ParCollection

и REPL висит.

Другие вопросы по тегам