17 Future

  • 併發應用
  • 在Future{}裏的代碼塊是併發執行的
  • future要麼成功得到一個結果,要麼以異常失敗
  • 用回調實現future完成時收到通知,串聯回調比較麻煩
  • map/flatMap或for組合多個future
  • promise有一個其值可被單次設置的future,使得實現產出的結果的任務變得更爲靈活
  • 適合併發計算負載的執行上下文

在future中運行任務

  • scala.concurrent.Future對象可以在未來執行代碼塊
  • ExecutionContext特質提供線程池
  • 每個Future在構造時都必須有一個指向ExecutionContext的引用
  • import ExecutionContext.Implicits.global謹慎使用,對於那些可能會阻塞的任務
import java.time._
import scala.concurrent._
import ExecutionContext.Implicits.global
Future{
  Thread.sleep(10000)
  println(s"This is the future at ${LocalTime.now()}")
}
println(s"This is the present at ${LocalTime.now()}")

//This is the present at 22:17:52.993
//scala> This is the future at 22:18:02.994

  • 在scala交互式可以看到效果,如果在idea中運行就看不到了
  • 多個future併發執行,在scala交互式中,使用粘貼模式執行
Future {
  for(i <- 1 to 100){print("A");Thread.sleep(10)}
}
Future {
  for(i <- 1 to 100){print("B");Thread.sleep(10)}
}
//得到的結果ABABABABABABABABABABABABABABABABABABBAABBAABABABABABBABBABABABABABAB
  • 語句塊是有值的
  • Future是一個會在未來發生的某個時間點給你一個結果(或失敗)的對象
  • Future等效於Java8的CompletionStage接口
  • 併發任務避免帶有副作用的操作,不要對共享的計數器遞增操作,不要填充共享的映射,而是要每個future都產生一個值,在所有的Future完成後對這些值進行組合
scala> val f = Future {Thread.sleep(5000);123}
f: scala.concurrent.Future[Int] = Future(<not completed>)
//五秒之內的結果
scala> f
res8: scala.concurrent.Future[Int] = Future(<not completed>)
//五秒之後的結果
scala> f
res9: scala.concurrent.Future[Int] = Future(Success(123))
//另一個示例
scala> val f = Future {throw new Exception("too late");123}
f: scala.concurrent.Future[Int] = Future(<not completed>)
//失敗的示例
scala> f
res11: scala.concurrent.Future[Int] = Future(Failure(java.lang.Exception: too late))

等待結果

  • 對Await的調用阻塞10秒,然後交出future的結果,scala.concurrent.duration._ 會允許使用從整數到Duration對象轉換,如seconds、millis
  • 如果在分配的時間內任務沒有完成,Await.ready方法將拋出java.util.concurrent.TimeoutException,例如result2
  • value方法返回的是Option[Try[T]],在future沒有完成時是None,完成時是Some(t),t是Try類的對象,它所持有的要麼是結果,要麼是造成任務失敗的異常
import java.time._
import scala.concurrent._
import scala.concurrent.duration._

import ExecutionContext.Implicits.global

val f = Future {Thread.sleep(10000);123}
val result = Await.result(f,10.seconds)
//val result2 = Await.result(f,5.seconds) 拋出異常

val f2 = Future {Thread.sleep(10000);123}
Await.ready(f2,10.seconds) //
val Some(t) = f2.value
//f2.value的類型和值是 Option[scala.util.Try[Int]] = Some(Success(123))
//如果ready結束f2還沒有完成,f2.value是None
//t是Success(123)

  • 實踐當中,應該不會經常使用Await.result和Await.ready方法,通常在任務是耗時的情況下併發地運行它們,主程序做其他更有用的事,見第4部分回調。
  • Future類也有result和ready方法,不要調它們

Try類

  • scala.util.{Failure, Success,Try}
  • Try[T]的示例 要麼是Success(v),v是類型T的值,要麼是Failure(ex),ex是一個Throwable,處理它的一種方式是match
import scala.util.{Failure, Success}
//直接拋出異常,match語句會輸出異常的字符串haha,
val f2 = Future {throw new Exception("haha");123}
Await.ready(f2,2.seconds)
val Some(t) = f2.value

t match{
  case Success(v) => println(s"The answer is ${v}")
  case Failure(ex) => println(ex.getMessage)
}
  • 還可以用isSuccess,isFailure獲知Try對象代表的是成功還是失敗,如果成功,可以用get方法獲取值
  • 與上文match語句相同的功能的代碼是
if (t.isSuccess) println(s"The answer is ${t.get}")
if (t.isFailure) println(t.failed.get.getMessage)
  • 將Try對象傳給預期可選值的方法,可以用toOption轉換成Option,將把Success轉換爲Some,把Failure轉換爲None
  • 構造Try對象使用Try{代碼塊}
scala> import scala.util.Try
import scala.util.Try

scala> val a = Try{"".toInt}
a: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "")

scala> val a = Try{"2".toInt}
a: scala.util.Try[Int] = Success(2)

//toOption
scala> {Try{"2".toInt}}.toOption
res9: Option[Int] = Some(2)

scala> {Try{"".toInt}}.toOption
res10: Option[Int] = None

//Try的示例
scala> Try{3/0}
res11: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)

scala> Try{3/2}
res12: scala.util.Try[Int] = Success(1)

scala> Try{3/2.2}
res13: scala.util.Try[Double] = Success(1.3636363636363635)

回調

  • 爲了更好的性能,不應該用阻塞的等待在獲取future的結果,而是future應該把結果報告給回調函數
  • 使用onComplete方法。f結束後就對失敗或成功進行判別運算。
  • 雖然避免了阻塞,但是某個Future在長時間計算後跟着另一個計算,再一個計算,嵌套回調
  • 更好的做法是將future想象成可以組合的實體,像函數那樣
  • 除了onComplete外還有onSuccess(只在future成功的時候),onFailure(只在future失敗的時候),這些也很要命
import java.time._
import scala.util.Random
import scala.concurrent._
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
import scala.util.{Failure, Success, Try}
val f = Future{
  Thread.sleep(10000)
  if (Random.nextInt()<5) throw new Exception("haha")
  42
}

f.onComplete{
  case Success(v) => println(s"The answer is ${v}")
  case Failure(ex) => println(ex.getMessage)
}

組合future任務

  • 組合兩個併發的future
val f1 = Future{Thread.sleep(10000);10}
val f2 = Future{Thread.sleep(5000);20}
val combined = f1.flatMap(n1 => f2.map(n2 => n1+n2))
//或for
val combined1 = for(n1 <- f1 ; n2 <- f2 ) yield n1+n2
  • 如果兩個有先後順序
  • 將第二個的val改成def,這樣就會先執行第一個,再執行第二個
  • 第一個是val或def並不重要,如果用def創建會在for開始的時候
def f1 = Future{Thread.sleep(10000);10}
def f2 = Future{Thread.sleep(5000);20}
val combined = f1.flatMap(n1 => f2.map(n2 => n1+n2))
//或for
val combined1 = for(n1 <- f1 ; n2 <- f2 ) yield n1+n2

其他future變換

  • map和flatMap是最基本的變換
  • foreach爲了得到副作用
  • recover方法接受一個可以將異常轉換爲成功結果的偏函數
  • fallbackTo 提供了另一種恢復機制,當調用f.fallbackTo(f2)的時候如果f失敗了,那麼f2會被執行,其結果值就是future的結果值,不過看不到失敗的原因
  • failed將一個失敗的Future[T]轉換成另一個成功的Future[Throwable],例如for(ex <- f.failed) println(ex)
  • zip將兩個future拉鍊zip起來,f1.zip(f2)調用交出:如果f1的結果是v而f2的結果是w,結果就是對偶(v,w),如果任何一個失敗了,結果就是一個異常,如果兩個都失敗了,返回f1的異常
  • zipWith,接受一個組合兩個結果的方法而不是返回對偶
    在這裏插入圖片描述

Future對象中的方法

  • Future伴生對象包含了用於操作由future組成的集合的若干方法
  • 例如 對一個列表的每個元素乘一個數,得到了future組成的集合
scala> val futures = List(3,1,2).map(p => Future{ p*10})
futures: List[scala.concurrent.Future[Int]] = List(Future(Success(30)), Future(<not completed>), Future(Success(20)))

scala> futures
res2: List[scala.concurrent.Future[Int]] = List(Future(Success(30)), Future(Success(10)), Future(Success(20)))

  • 得到所有結果的集合,使用Future.sequence方法
  • 這個調用不會阻塞,給出的是包含了集合的future,例如由List[Future[T]]變成了Future[List[T]]
  • 如果有一個future失敗了,作爲結果的future也會失敗,其異常爲集合所有失敗的future中最靠左邊的那一個的異常,也就是說如果多個future失敗了,看不到其他的失敗信息了
scala> val result = Future.sequence(futures)
result: scala.concurrent.Future[List[Int]] = Future(<not completed>)

scala> result
res3: scala.concurrent.Future[List[Int]] = Future(Success(List(30, 10, 20)))
  • traverse方法將map和sequence的兩步合併爲一步
  • 第二個柯里化參數的函數會被應用到列表的每個元素
val futures = List(3,1,2).map(p => Future{ p*10})
val result = Future.sequence(futures)
//合併爲
val result2 = Future.traverse(List(3,1,2))(p => Future{p * 10})
//result2: scala.concurrent.Future[List[Int]] = Future(<not completed>)

//scala> result2
//res5: scala.concurrent.Future[List[Int]] = Future(Success(List(30, 10, 20)))

  • reduceLeft和foldLeft操作,與之前的類似,好像是2.12版本以後纔有的,參考API
scala> val result3 = Future.reduce(futures)(_+_)
result3: scala.concurrent.Future[Int] = Future(<not completed>)

scala> result3
res6: scala.concurrent.Future[Int] = Future(Success(60))
  • 部分結果,例如第一個成功或失敗的結果
scala> val result4 = Future.firstCompletedOf(futures)
result4: scala.concurrent.Future[Int] = Future(Success(30))
  • find方法類似,不過需要提供一個條件
scala> val result4 = Future.find(futures)(_ < 20)
result4: scala.concurrent.Future[Option[Int]] = Future(<not completed>)

scala> result4
res7: scala.concurrent.Future[Option[Int]] = Future(Success(Some(10)))
//所有的都完成了,沒有成功的
scala> val result4 = Future.find(futures)(_ < 10)
result4: scala.concurrent.Future[Option[Int]] = Future(<not completed>)

scala> result4
res8: scala.concurrent.Future[Option[Int]] = Future(Success(None))
  • 其他的生成future的便利方法
  • Future.successful( r)是一個已經成功完成的結果爲r的future
  • Future.failed(e)是一個已經失敗的結果爲e的future
  • Future.fromTry(t)是一個已經完成的,通過Try對象t給出成功或異常結果的future
  • Future.unit是一個已經完成的結果爲Unit的future
  • Future.never是一個永遠不會完成的future

Promise

  • Future的對象是隻讀的,future的結果是在任務結束或失敗時隱式地被設置,Promise的值可以顯式地設置
  • computerAnswer爲普通的Future,computerAnswer2爲使用Promise,對一個promise調用future方法交出的是與其關聯的Future對象,該方法在開始那個最終會交出的結果的任務後,立即返回了相應的Future,任務將在另一個由Future{}表達式定義的Future中運行,這個future跟promise的future沒有關聯關係。
  • 對promise調用success將設置結果,或者用一個異常來調用failure,讓promise失敗,一旦這兩個方法中的一個被調用,與promise關聯的future也就會完成,這兩個方法都不能再被調用,如果調用則會拋出異常
  • 從消費端(調用computerAnswer2)的角度看,這兩種方案沒有區別,消費端都會有一個Future,並最終得到結果
def computerAnswer(arg:String) = Future{
  val n = workHard(arg)
  n
}
def computerAnswer2(arg:String) = {
  val p = Promise[Int]()
  Future{
    val n = workHard(arg)
    p.success(n)
    workOnSomethingElse()
  }
  p.future
}
  • 對於生產端,使用Promise會有更大的靈活度,生產端可以在滿足promise的同時做別的工作,生產端可以同時滿足多個promise
val p1 = Promise[Int]()
val p2 = Promise[Int]()
Future{
  val n1 = getData1()
  p1.success(n1)
  val n2 = getData2()
  p2.success(n2)
}
  • 多個任務併發工作滿足單個promise,當其中的一個任務有了結果時,它可以對promise調用trySuccess,這個方法接受結果並且在promise還沒有完成時返回true,如果promise已經完成了,則返回false並忽略當前這個結果
  • 下面的情況,promise將由第一個順利產出結果的任務完成
  • scala的promise等效於java8的CompletableFuture
val p = Promise[Int]()
Future{
	var n = workHard(arg)
	p.trySuccess(n)
}
Future{
	var n = workSmart(arg)
	p.trySuccess(n)
}

執行上下文

  • 默認情況下,Scala的future會在fork-join線程池中執行,這對於計算密集型很適用,但是如果當任務必須等待時就會耗盡所有的線程,就不合適了
  • 這時可以使用執行上下午,通知程序即將阻塞
val f = Future{
  val url = ""
  blocking{
  val contents = Source.fromURL(url).mkString
  }
}
  • 執行上下文收到通知後,可能會增加線程數,fork-join線程池如此,不過不會針對所有的阻塞場景進行優化。如果執行輸入輸出或連接數據庫,別的線程池可能更好,Java的併發類庫的Executors類給出了若干選擇,緩存線程池(cached thread pool)適用於IO密集型的工作負載,可以將它顯式地傳入Future.apply方法,也可以設爲隱式地執行上下文,這樣當ec在作用域內,所有的future都將使用該線程池
import java.util.concurrent.Executors
val pool = Executors.newCachedThreadPool()
implicit val ec = ExecutionContext.fromExecutor(pool)
發佈了75 篇原創文章 · 獲贊 83 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章