为什么Play默认执行上下文下Action会表现出单线程特性?
根本原因
你观察到的差异本质是Future任务提交的线程池与当前阻塞线程所属线程池是否为同一个导致的,和Play默认EC单线程、线程池耗尽无关,具体逻辑如下:
- Play 2.8.x默认提供的执行上下文(即你在Controller中隐式导入的来自
ControllerComponents的ExecutionContext)是Akka Actor系统的默认调度器线程池,同时Action同步块的执行线程本身就属于这个线程池。 - 代码实际执行顺序如下:
- 系统分配Akka调度器的一个工作线程执行当前Action块
- 执行
Future { println("PLOP") },将打印任务提交到同一个Akka调度器的任务队列排队 - 立刻执行
Thread.sleep(2000),直接占用当前的Akka工作线程阻塞2秒,这段时间该线程无法处理队列里的其他任务 - 2秒后sleep结束,线程被释放空闲,才会从队列里取出之前提交的打印任务执行,所以PLOP会在sleep结束后才打印
- 当你显式指定
ExecutionContext.global作为Future的执行上下文时,打印任务会被提交到JDK全局的ForkJoinPool,和Akka调度器是完全独立的两个线程池:你阻塞的只是Akka的工作线程,global池的线程不受影响,会立刻执行打印任务,所以PLOP会在sleep结束前输出。
最佳实践
Play官方明确要求不允许在默认Akka调度器上做任何阻塞操作,Thread.sleep属于典型的阻塞操作,你的写法本身不符合Play的设计规范:
- 如果必须做阻塞操作,要单独配置独立的阻塞专用线程池,不要占用默认调度器线程
- 异步操作要显式通过
map/flatMap组合Future,不要用同步阻塞等待结果
验证方式
你可以加一行日志打印当前线程ID,就能直观看到差异:
Action { println(s"当前执行Action的线程ID:${Thread.currentThread().getId}") Future { println(s"执行Future打印任务的线程ID:${Thread.currentThread().getId}") println("PLOP") } // 用默认EC时两个线程ID一致,用global时ID完全不同 Thread.sleep(2000) Ok("") }
内容的提问来源于stack exchange,提问作者stackoverflowed
相关产品推荐
相关产品推荐

