softshipper
softshipper

Reputation: 34099

Difference between ExecutionContext.global and main Thread

I have following code:

object KafkaApi {

  private implicit val main: ExecutionContextExecutor = ExecutionContext.global
  private val workers = ExecutionContext.fromExecutor(Executors.newCachedThreadPool())

  def main(args: Array[String]) {

    foo.unsafeRunAsync(_ => ())
    //foo.unsafeRunSync()
    println("Hello")


  }

  def foo: IO[Unit] =
    for {
      _ <- IO {
        println(Thread.currentThread().getName)
      }
      _ <- IO.shift(workers)
      _ <- IO {
        println(Thread.currentThread().getName)
      }
      _ <- IO {
        println(Thread.currentThread().getName)
      }
      _ <- IO {
        println(Thread.currentThread().getName)
      }
      _ <- IO.shift(main)
      _ <- IO {
        println(Thread.currentThread().getName)
      }
      _ <- IO {
        println(Thread.currentThread().getName)
      }
      _ <- IO {
        println(Thread.currentThread().getName)
      }
    } yield ()
}

and the output is:

main
Hello
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
scala-execution-context-global-14
scala-execution-context-global-14
scala-execution-context-global-14

What is the difference between main and scala-execution-context-global-14?

If these two are different, how to get the main thread back?

Upvotes: 1

Views: 1267

Answers (1)

SergGr
SergGr

Reputation: 23788

Running the code above, why the application never get terminated?

This additional question is too big for a comment so here goes my answer.

The thing is that in JVM all Threads are divided into "normal" and "daemon" threads. The important thing here is that

The Java Virtual Machine exits when the only threads running are all daemon threads.

So if you have any running non-daemon Thread, JVM thinks your application is still working even if it actually does nothing (maybe it is just waiting for some input). The "main" thread is obviously a "normal" thread. Threads created by standard ExecutionContext.global are daemon and thus don't stop your app from quitting when the main thread finishes. Threads created by Java's Executors.newCachedThreadPool are non-daemon and thus keep the application alive. There are several possible solutions:

  1. Don't use other ExecutionContext except for the global i.e. don't use Executors.newCachedThreadPool at all. Depending on your case this might be or not be what you want.

  2. Explicitly shutdown your custom ExecutorService when all its job is done. Be careful here because shutdown doesn't wait for all active tasks to be finished. So the code should become something like

private val pool = Executors.newCachedThreadPool
implicit private val workers = ExecutionContext.fromExecutor(pool)

// do whatever you want with workers 


// optionally wait for all the work to be done

pool.shutdown()
  1. Use custom pool that creates daemon threads. For example you could do something like this:
val workers = ExecutionContext.fromExecutor(Executors.newCachedThreadPool(new ThreadFactory {
  private val defaultDelegate = Executors.defaultThreadFactory()

  override def newThread(r: Runnable): Thread = {
    val t = defaultDelegate.newThread(r)
    //let the default factory do all the job and just override daemon-flag 
    t.setDaemon(true)
    t
  }
}))

IMHO the main trade-off between #2 and #3 is convenience vs correctness. In #3 you don't have to think where all tasks are finished so it is safe to call shutdown which is convenient. The price is that if for some reason you misjudged and your "main" thread quits before all other tasks are finished, you will not know that anything went wrong because daemon threads will be just silently killed. If you go with #2 and do the same mistake either your app will continue to run if you din't call shutdown in that code path, or you will see some warning in the log that the pool was shutdown while there still were some tasks in progress. So if this is just a middle step in a long sequence of processing what for some reason requires custom thread pool I'd probably go with #3; but if this parallel execution is the main behavior I'd go with more explicit #2 way.

Upvotes: 4

Related Questions