1

我们到处都可以读到,在执行长时间运行的操作或阻塞操作时,最好使用特殊的执行上下文来解决这个问题。像访问数据库这样的阻塞操作。我明白为什么。这是为了避免线程饥饿。我们不希望“8”个可用线程忙于一些可能最终返回或继续阻塞的阻塞代码。它要么严重减慢应用程序的速度,要么无限期地阻止它。

同时,我想知道像 Spray 或 Play 这样的东西是如何实现的。确实,让我们以客户端为例。当请求被发送时,我们会得到一个未来的响应。换句话说,请求是异步执行的。顺便说一句,这可能最终成为一项长期运行的操作。但是,在这种情况下,没有什么可以说发起许多请求会导致线程饥饿。因此,我想知道为什么在这种情况下它不是问题。他们有特殊的线程池吗?

我在“Learning concurrent programming in Scala”一书中写道,在 Future 中使用“Blocking {}”语句块有助于其调度程序自动生成更多线程。会不会是他们处理的方式?

接收请求也可以这样说,在游戏中我们可以执行异步操作。如果想从此操作访问数据库,应使用“Blocking {}”语句块。如何执行那个动作是一个特殊的threadPool/ExecutionContext。

我在这里的假设是它们依赖于implicit.global ExecutionContext。也许我错了。底线是。默认情况下发出请求是一个很长的操作,例如在您的代码中使用喷雾如何处理它以便不在您的代码中创建线程饥饿?

我们使用不同的 ExecutionContext 吗?

编辑:刚刚发现这个简短的演示文稿Don't Block - How to Mess Up Akka and Spray恰好更好地说明了我在这里遇到的问题。

无论如何,我希望有其他意见

编辑:这是我了解到的,在使用未来时会以某种方式发生:

def apply[T](body: =>T): Future[T] = impl.Future(body)  //here I have omitted the implicit ExecutorContext
impl.Future is an implementation of Future trait:

def apply[T](body: =>T)(implicit executor: ExecutionContext): scala.concurrent.Future[T] =
{
  val runnable = new PromiseCompletingRunnable(body)
  executor.prepare.execute(runnable)
  runnable.promise.future
}

PromiseCompletingRunnable 看起来像这样:

class PromiseCompletingRunnable[T](body: => T) extends Runnable {
val promise = new Promise.DefaultPromise[T]()

override def run() = {
  promise complete {
    try Success(body) catch { case NonFatal(e) => Failure(e) }
  }
} } 

摘自:需要澄清 Scala 中的 future 和 promises 我在“Learning concurrent programming in Scala”一书中写了一些更简单和类似的东西

这对我来说意味着:线程池中有一个线程,它使该任务出列并尝试使用该任务的执行结果设置一个承诺未来值。如果这是正确的,我看不出进行 IO 调用的任务如何不会阻止该线程的运行。

4

2 回答 2

5

我认为您不理解的是,当您使用阻塞 API 发出客户端请求时,线程会阻塞,坐在那里什么都不做,直到响应返回。但是,如果您使用异步 API,则在等待响应返回时,没有线程在等待。当它回来时,当然,一个线程被拉出执行上下文来完成工作,但这就是你希望你的线程做的事情——工作,而不是什么都没有。让数百个线程无所事事地等待客户端请求或数据库查询返回是一种资源浪费。而且因为它们不是免费的,所以你必须限制它们,这就是线程饥饿出现的地方。在异步框架中,只有在有工作要做时才使用线程。这意味着如果每个 CPU 核心有一个线程,如果您用尽线程池,则意味着您的 CPU 已被 100% 使用,而在阻塞框架中,您可能会在 CPU 使用率仅为 10% 的情况下耗尽线程池。请记住,大多数普通 Web 应用程序将大部分时间花在 IO 上,即等待数据库调用或 http 客户端调用返回。等待的数量与实际工作的数量通常是一个数量级或更多。因此,仅在有工作要做时才使用线程是一个很大的优势。

于 2015-02-17T10:15:47.353 回答
1

以I/O操作为例,我认为唯一缺失的环节就是I/O复用,可以通过epoll或者kqueue来实现。

通过使用 epoll/kqueue,一个线程可以同时等待多个 I/O 事件如果没有 I/O 响应,则该线程正在等待(饥饿),但您看到只有该线程在等待。

nginx 和 nodejs 都使用这种工作模式。

于 2016-07-15T13:40:02.197 回答