Search code examples
multithreadingscalascala-3

Why can't I join threads containing a for loop in Scala 3?


Why does only this specific code break when everything else works?

No problems when commenting out the for loop OR join(). Just doesn't work with both.

Using a while loop works, putting the loop in another class also works, works in the REPL or as a script without wrapping it as an object/class (using @main), works in Scala 2. Doesn't work with a loop in a function in the same class/object

object Main extends App {
  val t = new Thread() {
    override def run() = {
      println("Started")
      for (j <- 1 to 2) {println("Work")}
    }
  }
  t.start()
  t.join()
}

Edit:

By doesn't work I mean unexpected behavior: Nothing in the loop or after the loop executes. Also replacing join() with Thread.sleep(3000), the new thread (containing the for loop) continues executing (starting off at the for loop) only after the main thread finishes sleeping

It can be tested in Scastie

Iterating over any collection seems to cause the problem. For can be replaced with this block and it still won't print

it = List(1, 2, 3)
it.foreach(x => println(x))

But this will print, which shouldn't work because this is the definition of foreach

it = List(1, 2, 3)
while(it.hasNext) println(it.next())

Solution

  • Scala 3 no longer treats DelayedInit the way Scala 2 did, with the result that the body of an App now executes as part of the static initializer (as it would for any other object). Any thread apart from the one that loads the class will block until such time as the static initializer finishes (in this case, the static initializer exits before the synthetic main method from App is even called).

    In the case of Seq(1, 2).foreach { _ => println("Working") }, the body of { _ => println("Working") } is hoisted out into being part (likely a method, though I haven't inspected the bytecode) of the enclosing Main object and thus the call to foreach blocks until Main is a "real object" in the eyes of the JVM, which isn't until the static initializer finishes. Unfortunately, the static initializer (thanks to the join) is waiting for foreach to finish. while is still specially interpreted by the compiler and turns into a loop in the bytecode.

    This can be seen by the minimal change of not extending App and only moving the call to join into a main method, as in this Scastie. The static initializer forks the thread and is finished (making Main a real object in the eyes of the JVM and thus allowing the foreach in the forked thread to execute). After this, the main method is entered and is able to join the forked thread.