什么是Future
当程序中的某个过程执行时间较长,同时它与程序中的其他环节没有很强的关联性,那么这个过程便可以提取出来作为一个线程单独运行,Scala中的Future便是用来解决此类问题,可以将用于并发执行的操作添加到Future中,然后在未来的某个时间点再获取其执行结果。
回调函数
为了能够获取Future线程的执行结果,就需要在特定的状态通过回调函数获取返回值。Future的状态主要有两类,完成与未完成,而完成状态又分为成功与失败,成功会有并发执行函数的返回值,失败则会抛出异常。
常用的有三类回调函数,onComplete、onSuccess、onFailure 。
def onComplete[U](f: (Try[T]) ⇒ U)(implicit executor: ExecutionContext): Unit
从onComplete的接口来看它需要接收一个Try[T] => U的参数,Future如果执行成功,回调会使用Success[T]类型的值,如果失败则会使用Failure[T]类型的值。
import scala.util.{Success, Failure}
val f: Future[List[String]] = Future {
session.getRecentPosts
}
f onComplete {
case Success(posts) => for (post <- posts) println(post)
case Failure(t) => println("An error has occured: " + t.getMessage)
}
当只想处理成功或者失败其中一种情况时,可以通过后两张回调实现
def onSuccess[U](pf: PartialFunction[T, U])(implicit executor: ExecutionContext): Unit
def onFailure[U](pf: PartialFunction[Throwable, U])(implicit executor: ExecutionContext): Unit
它们接收一个偏函数作为参数,用于匹配返回值
f onFailure {
case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
case posts => for (post <- posts) println(post)
}
自定义ExecutionContext
从上面回调函数的API看出,它们传入了一个隐式参数implicit executor: ExecutionContext,一般只需要引入对应的类就行了,不需要再手动传入
import scala.concurrent.ExecutionContext.Implicits.global
但是在使用过程中发现,默认的ExecutionContext所能开辟的Future线程是受到机器CPU核数影响,那么超过核数的线程将会排队等待前面的线程完成才能开始,所以需要调大线程数。ExecutionContext类中提供了一个自定义的方法
object Implicits {
val threadPool = Executors.newFixedThreadPool(1000)
implicit lazy val ec = ExecutionContext.fromExecutor(threadPool)
}
用一个新的线程池创建一个执行器,再作为参数传入构造方法即可。
那么原有的ExecutionContext的线程数为什么受到CPU核数限制呢?从源码中可以看出,在构造的时候传入的执行器是个null
implicit lazy val global: ExecutionContextExecutor = impl.ExecutionContextImpl.fromExecutor(null: Executor)
null又是如何创建执行器的呢?在ExecutionContextImpl.scala源码中可以看到
val executor: Executor = es match {
case null => createExecutorService
case some => some
}
def createExecutorService: ExecutorService = {
def getInt(name: String, default: String) = (try System.getProperty(name, default) catch {
case e: SecurityException => default
}) match {
case s if s.charAt(0) == 'x' => (Runtime.getRuntime.availableProcessors * s.substring(1).toDouble).ceil.toInt
case other => other.toInt
}
def range(floor: Int, desired: Int, ceiling: Int) = scala.math.min(scala.math.max(floor, desired), ceiling)
val desiredParallelism = range(
getInt("scala.concurrent.context.minThreads", "1"),
getInt("scala.concurrent.context.numThreads", "x1"),
getInt("scala.concurrent.context.maxThreads", "x1"))
val threadFactory = new DefaultThreadFactory(daemonic = true)
try {
new ForkJoinPool(
desiredParallelism,
threadFactory,
uncaughtExceptionHandler,
true) // Async all the way baby
} catch {
case NonFatal(t) =>
System.err.println("Failed to create ForkJoinPool for the default ExecutionContext, falling back to ThreadPoolExecutor")
t.printStackTrace(System.err)
val exec = new ThreadPoolExecutor(
desiredParallelism,
desiredParallelism,
5L,
TimeUnit.MINUTES,
new LinkedBlockingQueue[Runnable],
threadFactory
)
exec.allowCoreThreadTimeOut(true)
exec
}
}
从上面的代码可以看出默认创建的是一个ForkJoinPool线程池,而它的线程数大小是通过Runtime.getRuntime.availableProcessors获取的,也就是CPU核数
Future的超时判定
Future内部并没有提供超时判定的接口,所以使用阻塞等待的方式来判定超时
def result[T](awaitable: Awaitable[T], atMost: Duration): T
val result = Await.result(future, 1 second)
第二个参数接收一个等待时间,一旦future执行超过这个时间,result便会抛出一个TimeoutException异常,后续可以通过捕获这个异常来做超时处理。
这个方式因为是阻塞等待的,所以一定程度上会影响并行的性能,但有些时候为了获取结果而采取一些强制手段也是迫不得已,例如不想因为并行的任务因为超时无法被中断而影响后面任务的执行,那么就可以再起一个Future线程执行,得知Future线程超时后直接跑下一个任务就行了,前一个任务的Future线程有没有中断就不用那么在意了。
Scala中的Future用于处理并发操作,允许将长时间运行的任务放入单独线程执行,并通过回调函数onComplete、onSuccess和onFailure获取结果。默认ExecutionContext线程数受限于CPU核数,可通过自定义线程池扩大线程数。虽然Future本身不支持超时判定,但可以通过阻塞等待的方式实现超时处理,或者在超时后启动新任务来避免性能影响。
306

被折叠的 条评论
为什么被折叠?



