繁体   English   中英

期货如何提高引擎盖下的并发性?

[英]How do futures work to improve concurrency under the hood?

我试图围绕期货的幕后工作进行思考。 我对Java和Scala中的概念都很熟悉。 我一直在PlayFramework中使用Future来防止阻塞操作占用我的连接线程。 在大多数情况下,这种方法效果很好,但是我仍然觉得我缺少某些方面。 特别是,在保持用于执行阻塞操作的线程数较低的情况下。

我的世代假设是(如果我错了,请纠正我)应该有一个线程(在最简单的情况下),在一组未决的期货上运行一个无限循环。 在每个回合中,线程都会睡一会儿,然后一个接一个地挑选期货,检查是否有一些结果到达,然后返回,然后从集合中删除完成的期货。

恕我直言,这仅在基础操作也未阻塞的情况下才有效。 否则,逻辑告诉我应该将那些作为池的一部分隔离在各自独立的线程中。 我的思路崩溃了,即使在将来,每项操作都从根本上阻碍了这一步。 然后,我假设在最坏的情况下,即使阻塞在期货中,我们每次阻塞操作都会再次遇到一个线程。

问题在于Java中广泛使用的IO代码的很大一部分在根本上是阻塞的。 这意味着执行期货中包装的15个JDBC操作仍将拆分出15个线程。 否则,我们将不得不在单个线程上顺序调用它们,这甚至更糟。

我要说的是,从根本上说,将从根本上阻止未来的IO操作包装在理论上应该完全没有帮助。 我是对还是错? 拜托,帮我建立难题。

“我想说的是,从根本上包装期货中的IO操作,从理论上讲完全没有帮助。我是对还是错?”

这取决于您所说的“帮助”。 这不会神奇地使您的I / O操作成为非阻塞的,这是事实(很多人对此感到困惑)。

根据线程池的配置方式,它将确保您的I / O(或任何阻塞操作)在专用线程中运行-使“核心”线程可以自由运行。

假设您的池具有与计算机核心相同的多个线程,如果您要执行阻塞操作,则可以向池提示该线程,并且它将为该操作创建特定的线程,因为它知道该线程将占用大量CPU。

另一种常见的做法是使用专用的ExecutionContext阻止操作,以将其执行与程序的其余部分隔离开。

尽管“潜在的” I / O是通过阻塞线程池发生的,使用期货仍然有一些有用的方面,但您说对了,通常这通常不会提供Future的某些重要优势,这是正确的。 您所缺少的是实现真正无阻塞的I / O的可能性,即最终在系统调用级别调用select或类似接口。 Java NIO接口可以做到这一点,在此之上构建的许多框架(例如Netty)或面向回调的库(例如Apache的HttpAsyncClient )也可以做到这HttpAsyncClient

不幸的是,尽管有例如postgresql-async,但它并没有替代我所知道的完整JDBC,它至少涵盖了一些用例。

“我的世代假设是(如果我错了,请纠正我)应该有一个线程(在最简单的情况下),在未决期货的集合上运行一个无限循环。” 并非总是如此。 这取决于正在使用的ExecutionContext。 您可以根据需要为每个Future创建一个新线程的ExecutionContext,也可以具有运行Future的固定线程池。 还有其他选择。 这实际上取决于您要执行的操作。

IMO,期货的真正力量在于它所允许的编程模型。 一旦了解了模型,就可以轻松组成期货。 然后,如果您需要稍微更改并发模型,则可以通过配置ExecutionContexts并在每组任务中使用多个/不同的ExecutionContexts来实现。 在实际/大型应用程序中,使用了多个ExecutionContext-不仅是默认的。

您提到了阻止IO和JDBC,因此我将举一个与此相关的示例。 过去,我将Futures与JDBC一起使用,以对不同的表进行并发插入。 如果插入是不相关的(例如,没有FK),并且假设您配置的ExecutionContext至少配置了与JDBC连接一样多的线程,则调用方将仅等待最长插入时间,而不是所有插入时间的总和。 例如:

// functions that insert into tables
// return Future primary key (Long)
// use blocking JDBC calls
def insertIntoTable1(col1: String, col2: String) : Long = ...

def persistData(... input data for all tables ...) = {
  // get Future primary keys
  val futurePrimaryKey1 = insertIntoTable1(...)
  val futurePrimaryKey2 = insertIntoTable2(...)
  val futurePrimaryKey3 = insertIntoTable3(...)

  // inserts for id1 ... idN are done concurrently
  val futureParentTablePrimaryKey : Future[Long] = for {
    id1 <- futurePrimaryKey1
    id2 <- futurePrimaryKey2
    id3 <- futurePrimaryKey3
  } yield insertIntoParentTable(id1, id2, id3)

  // wait for final insert
  Await.result(futureParentTablePrimaryKey, Duration.Inf)
  // of course if you wanted the caller to know about Futures, don't do the Await.
}

从伪代码来看不是很明显,但是应该使用非默认的ExecutionContext。 因此,在特定示例中,到处都是阻塞IO。 但是关键在于它是并发等待,并且编程模型易于理解。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM