您的测试结构不正确,无法测试您的假设。
如果您查看this section in the docs,您会看到 Play 有一些线程池/执行上下文。对于您的问题,重要的是默认线程池以及它与您的操作所服务的 HTTP 请求有何关系。
正如文档所述,默认线程池是所有应用程序代码默认运行的地方。 IE。所有动作代码,包括所有Future's(没有明确定义自己的执行上下文),都将在这个执行上下文/线程池中运行。所以用你的例子:
def sync = Action {
// *** import Contexts.blockingPool
// *** Future {
// *** Thread.sleep(100)
// ***}
Ok("Done")
}
// *** 注释的未操作中的所有代码都将在默认线程池中运行。
IE。当请求被路由到您的操作时:
-
Future 和 Thread.sleep 将被分派到您的自定义执行上下文
- 然后无需等待
Future 完成(因为它在自己的线程池 [Context.blockingPool] 中运行,因此不会阻塞默认线程池上的任何线程)
- 您的
Ok("Done") 语句被评估并且客户端收到响应
- 大约。收到响应 100 毫秒后,您的
Future 完成
因此,为了解释您的观察,当您同时发送 100 个请求时,Play 将很乐意接受这些请求,路由到您的控制器操作(在 默认线程池上执行),发送到您的 Future然后回复客户端。
默认池的默认大小为
play {
akka {
...
actor {
default-dispatcher = {
fork-join-executor {
parallelism-factor = 1.0
parallelism-max = 24
}
}
}
}
}
每个内核使用 1 个线程,最多 24 个。
鉴于您的操作很少(不包括Future),您将能够毫不费力地处理每秒 1000 个请求。但是,您的 Future 将需要更长的时间来处理积压,因为您阻塞了自定义池中的唯一线程 (blockingPool)。
如果您使用我对您的操作稍作调整的版本,您将在日志输出中看到确认上述说明的内容:
object Threading {
def sync = Action {
val defaultThreadPool = Thread.currentThread().getName;
import Contexts.blockingPool
Future {
val blockingPool = Thread.currentThread().getName;
Logger.debug(s"""\t>>> Done on thread: $blockingPool""")
Thread.sleep(100)
}
Logger.debug(s"""Done on thread: $defaultThreadPool""")
Results.Ok
}
}
object Contexts {
implicit val blockingPool: ExecutionContext = Akka.system.dispatchers.lookup("blocking-pool-context")
}
您的所有请求都会先得到迅速处理,然后您的Future会一个一个完成。
总之,如果你真的想测试 Play 将如何处理多个并发请求,而只有一个线程处理请求,那么你可以使用以下配置:
play {
akka {
akka.loggers = ["akka.event.Logging$DefaultLogger", "akka.event.slf4j.Slf4jLogger"]
loglevel = WARNING
actor {
default-dispatcher = {
fork-join-executor {
parallelism-min = 1
parallelism-max = 1
}
}
}
}
}
您可能还想像这样在您的操作中添加Thread.sleep(以减慢默认线程池孤独线程的速度)
...
Thread.sleep(100)
Logger.debug(s"""<<< Done on thread: $defaultThreadPool""")
Results.Ok
}
现在您将有 1 个线程用于请求,1 个线程用于您的 Future's。
如果您在高并发连接下运行此程序,您会注意到客户端阻塞,而 Play 会一一处理请求。这是你期望看到的......