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

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

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

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

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

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

Внутренний цикл: 1
Что-то делать Внутренний цикл: 2
Что-то делать

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

Внутренний цикл: 2
Внутренний контур: 1

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

Ответ 1

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

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

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

Вы можете вызвать это поведение с фьючерсами от 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 зависает.