Search code examples
multithreadingscalaconcurrency

Waiting on main thread for callback methods


I am very new to Scala and following the Scala Book Concurrency section (from docs.scala-lang.org). Based off of the example they give in the book, I wrote a very simple code block to test using Futures:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}

object Main {
  def main(args: Array[String]): Unit = {
    val a = Future{Thread.sleep(10*100); 42}
    a.onComplete {
      case Success(x) => println(a)
      case Failure(e) => e.printStackTrace
    }
    Thread.sleep(5000)
  }
}

When compiled and run, this properly prints out:

Future(Success(42))

to the console. I'm having trouble wrapping my head around why the Thread.sleep() call comes after the onComplete callback method. Intuitively, at least to me, would be calling Thread.sleep() before the callback so by the time the main thread gets to the onComplete method a is assigned a value. If I move the Thread.sleep() call to before a.onComplete, nothing prints to the console. I'm probably overthinking this but any help clarifying would be greatly appreciated.


Solution

  • When you use the Thread.sleep() after registering the callback

        a.onComplete {
          case Success(x) => println(a)
          case Failure(e) => e.printStackTrace
        }
        Thread.sleep(5000)
    

    then the thread that is executing the body of the future has the time to sleep one second and to set the 42 as the result of successful future execution. By that time (after approx. 1 second), the onComplete callback is already registered, so the thread calls this as well, and you see the output on the console.

    The sequence is essentially:

    • t = 0: Daemon thread begins the computation of 42
    • t = 0: Main thread creates and registers callback.
    • t = 1: Daemon thread finishes the computation of 42
    • t = 1 + eps: Daemon thread finds the registered callback and invokes it with the result Success(42).
    • t = 5: Main thread terminates
    • t = 5 + eps: program is stopped.

    (I'm using eps informally as a placeholder for some reasonably small time interval; + eps means "almost immediately thereafter".)

    If you swap the a.onComplete and the outer Thread.sleep as in

        Thread.sleep(5000)
        a.onComplete {
          case Success(x) => println(a)
          case Failure(e) => e.printStackTrace
        }
    

    then the thread that is executing the body of the future will compute the result 42 after one second, but it would not see any registered callbacks (it would have to wait four more seconds until the callback is created and registered on the main thread). But once 5 seconds have passed, the main thread registers the callback and exits immediately. Even though by that time it has the chance to know that the result 42 has already been computed, the main thread does not attempt to execute the callback, because it's none of its business (that's what the threads in the execution context are for). So, right after registering the callback, the main thread exits immediately. With it, all the daemon threads in the thread pool are killed, and the program exits, so that you don't see anything in the console.

    The usual sequence of events is roughly this:

    • t = 0: Daemon thread begins the computation of 42
    • t = 1: Daemon thread finishes the computation of 42, but cannot do anything with it.
    • t = 5: Main thread creates and registers the callback
    • t = 5 + eps: Main thread terminates, daemon thread is killed, program is stopped.

    so that there is (almost) no time when the daemon thread could wake up, find the callback, and invoke it.