scala的Future和Promise

前言

使用scala的Future和Promise就不得不提一下異步編程和多線程編程的區別。
共同點:異步和多線程兩者都可以達到避免調用線程阻塞的目的,從而提高軟件的可響應性
不同點:

  1. 線程不是一個計算機硬件的功能,而是操作系統提供的一種邏輯功能,線程本質上是進程中一段併發運行的代碼,所以線程需要操作系統投入CPU資源來運行和調度。
  2. 多線程在單個線程中的處理程序依然是順序執行,符合普通人的思維習慣,所以編程簡單。但是多線程的缺點也同樣明顯,線程的使用(濫用)會給系統帶來上下文切換的額外負擔。並且線程間的共享變量可能造成死鎖的出現。
  3. 異步操作無須額外的線程負擔,並且使用回調的方式進行結果處理,在設計良好的情況下,處理函數可以不必使用共享變量(即使無法完全不用,最起碼可以減少 共享變量的數量),減少了死鎖的可能。當然異步操作也並非完美無暇。編寫異步操作的複雜程度較高,程序主要使用回調方式進行處理,與普通人的思維方式有些 初入,而且難以調試。

Future

Future 是一種佔位符,即用於存儲值的內存位置。
個人理解:Future是對我們期待返回值存放位置的一個監視器。當我們異步操作的返回值就位時,能被Future感知到。

Future 對象和Future 計算

Future 對象是對異步操作返回值的一種包裝。
Future計算是生成Future對象的異步計算操作,是真正的異步操作實體

Future的基本使用

使用輪詢的方式獲得計算結果

  1. 加載上下文,上下文jue’d

  2. 創建Future計算,並生成Future對象

  3. 通過反覆調用Future對象的isCompleted方法來判斷是否可以獲得Future計算結果

     // step 1 加載上下文
     import scala.concurrent.ExecutionContext.Implicits.global
     
     // step 2 創建Future計算
     val num = Future.apply({
         1 + 1
     })
     
     // step 3 輪詢
     while (!num.isCompleted) {}
     
     println(num.value)
    

使用回調函數獲取計算結果

  1. 加載上下文
  2. 創建Future計算
  3. 使用回調函數
    // step 1 加載上下文
    import scala.concurrent.ExecutionContext.Implicits.global
    
    // step 2 創建Future計算
    val num = Future.apply({
        1 + 1
    })
    
    // step 3 使用回調函數處理計算結果
    // Success 和 Failures 是Try的兩個子類
    num.onComplete {
        case Success(value) => {
            println(value)
        }
        case Failure(exception) => {
            exception.printStackTrace()
        }
    }
    
    // 上面的方法相當於下面兩個方法的組合
    //        num.onFailure()
    //        num.onSuccess()
    
    Thread.sleep(1000) // 主線程並不會等Future執行結束才結束
    

注意:

  1. 除了用類似onSuccess的方法,使用 foreach也是可行的,不過foreach會忽略異常情況。但是使用faild.foreach是可以處理失敗情況的。

     num.failed.foreach(print)
    
  2. 回調函數是可以添加多個的,按添加順序依次執行

    num.onComplete {
            case Success(value) => {
                println(value)
            }
            case Failure(exception) => {
                exception.printStackTrace()
            }
        }
    
        num.onComplete {
            case Success(value) => {
                println(value)
            }
            case Failure(exception) => {
                exception.printStackTrace()
            }
        }
        
        num.foreach(value => println("回調 1"))
        num.foreach(value => println("回調 2"))
        num.foreach(value => println("回調 3"))
    
    

回調函數的組合

如果我們想按順序爲Future添加多個回調函數,實習一個函數調用的結果傳遞給另一個函數,可以使用類似集合的map方法。需要注意的是:map方法對正常執行的程序調用組合函數,但是對異常執行的程序異常會原封不動的傳遞

import scala.concurrent.ExecutionContext.Implicits.global

val num = Future.apply({
    1 / 1
})

val newNum = num.map(value => value * 10)

newNum.foreach(println)
newNum.failed.foreach(println)

回調函數和函數組合的選擇

如果我們的函數有副租用,我們應該選擇回調函數,否則,選擇函數組合。

Future組合字

map

map操作將一個Future映射成另一個Future

flatMap

faltMap操作也是將Future映射成另一個Future

map和flatMap使用的區別

如果僅僅是參數不同或者和list等集合使用類似,那這裏也沒什麼可以討論的。
現在有一個例子:我們有兩個Future[int],我們要把兩個int相加生成一個新的Future.我們既可以用map方法也可以用flatMap算子。

import scala.concurrent.ExecutionContext.Implicits.global

val num1 = Future(1)
val num2 = Future(100)

val ans1 = num1.map(value1 => {
    value1 + num2.value.get.get // 使用map這裏要把第二個Future裏解出來
})
ans1.foreach(println)
ans1.failed.foreach(println)


val ans2 = num1.flatMap(value1 => {
    num2.map(value2 => value1 + value2) //這裏不用解包
})
ans2.foreach(println)
ans2.failed.foreach(println)

Thread.sleep(3000)

輸出:

101
101

在上面這個例子中,輸出一致,好像沒什麼區別。無非是使用map要將num2裏面的數據解出來。但是這裏面有個問題就是:我們再解包num2裏面的數據,不一定拿得到num2裏面的數據,這時輸出就是:

java.util.NoSuchElementException: None.get
101

那我們能不能用回調函數或者foreach呢?
回答是不能。因爲使用回調函數和foreach的返回值都是空。
在這裏插入圖片描述
在這裏插入圖片描述
所以只有使用faltMap可以完成這樣的操作了。
所以最後的總結:flatMap可以當做異步操作的同步工具來使用,用來組合多個Future,而map不可以。

for 推導在Future裏的應用。

for 推導時間是就是上面的flatMap的簡化形式。

一維for推導

一維for推導就是簡單的遍歷

val res = for (
    a <- 0 to 10
    if a % 2 == 0
) yield a

println(res)

輸出:

Vector(0, 2, 4, 6, 8, 10)

高維for推導

高維for推導,實際上是求笛卡爾積

val res = for (
    a <- 0 to 10 if a % 3 == 0;
    b <- 0 to 10 if b % 3 == 1;
    c <- 0 to 10 if c % 3 == 2
) yield (a, b, c)

println(res)

輸出

Vector((0,1,2), (0,1,5), (0,1,8), (0,4,2), (0,4,5), (0,4,8), (0,7,2), (0,7,5), (0,7,8), (0,10,2), (0,10,5), (0,10,8), (3,1,2), (3,1,5), (3,1,8), (3,4,2), (3,4,5), (3,4,8), (3,7,2), (3,7,5), (3,7,8), (3,10,2), (3,10,5), (3,10,8), (6,1,2), (6,1,5), (6,1,8), (6,4,2), (6,4,5), (6,4,8), (6,7,2), (6,7,5), (6,7,8), (6,10,2), (6,10,5), (6,10,8), (9,1,2), (9,1,5), (9,1,8), (9,4,2), (9,4,5), (9,4,8), (9,7,2), (9,7,5), (9,7,8), (9,10,2), (9,10,5), (9,10,8))

for推導和flatMap的關係

如果我們只有兩個維度或者一個維度,使用flatMap並無不妥,但是我們如果有多個維度的對象需要通過flatMap組合,那麻煩就比較大了。

for 推導在Future中的應用

import scala.concurrent.ExecutionContext.Implicits.global
val func1 = Future({
    Thread.sleep(1000)
    1
})
val func2 = Future({
    Thread.sleep(1000)
    2
})
val func3 = Future({
    Thread.sleep(3000)
    3
})

val res = for (
    a <- func1;
    b <- func2;
    c <- func3
) yield (a, b, c) // 這裏生成的是Future[(int,int,int)] 對象

res.foreach(println)
Thread.sleep(4000)

這樣就很輕易的完成了多個Future的組合和同步

Promise

Promise的基本使用

  1. 定義Promise對象
  2. 添加回調函數(成功的回調或者失敗的都行)
  3. 爲Promise對象賦值(成功值或者失敗值)
import scala.concurrent.ExecutionContext.Implicits.global

val promise = Promise[String]

promise.future.foreach(str => print("我的祖國是: " + str))
promise.future.failed.foreach(e => println(e))


/*
賦值方法一:
使用success和failure。complete方式相當於success和failure的抽象
這種賦值方法有個問題就是我們的promise只允許賦值一次,重複賦值會被拋出異常
*/

promise.success("china")
promise.failure(new Exception("not china"))
promise.complete(Success[String]("china"))
promise.complete(Failure[String](new Exception("not china")))


/*
賦值方法二:
使用try方法賦值,這種賦值和方法一類似,但是這種方法賦值不會拋出異常
*/
promise.trySuccess("china")
promise.tryFailure(new Exception("not china"))
promise.tryComplete(Success("china"))
promise.tryComplete(Failure(new Exception("not china")))

Thread.sleep(3000)

注意:方法一賦值會拋出異常,方法二不會

Promise 的使用模式

模式一

先創建一個promise,然後在其他計算中完成這個promise對象的賦值。最後返回該promise對象的future對象。

import scala.concurrent.ExecutionContext.Implicits.global

def getFuture[T](b: => T): Future[T] = {
    val promised = Promise[T]
    global.execute(new Runnable {
        override def run(): Unit = {
            try {
                promised.success(b)
            } catch {
                case NonFatal(e) => promised.failure(e)
            }
        }
    })
    promised.future
}

模式二 基於回調函數的API轉換

示例:自定義timeout

import scala.concurrent.ExecutionContext.Implicits.global

val timer = new Timer(true)

def timeout(t: Int) = {
    val promisedUnit = Promise[Unit]
    timer.schedule(new TimerTask {
        override def run(): Unit = {
            promisedUnit.success()
            timer.cancel()
        }
    }, t)
    promisedUnit.future
}

println("延時之前")
timeout(1000).foreach(_ => println("延時一秒"))
println("延時之後")
Thread.sleep(2000)

擴展Future的方法

import scala.concurrent.ExecutionContext.Implicits.global
/**
 * 接收一個future,生成一個FutureOps,FutureOps具有or方法
 * 當future調用or方法的時候就會尋找該隱式類生成一個FutureOps
 *
 * @param self
 * @tparam T
 */
implicit class FutureOps[T](val self: Future[T]) {
    def or(that: Future[T]): Future[T] = {
        val promisedT = Promise[T]
        // 下面兩個賦值,只能成功一個,哪個Future先完善,哪個成功
        self.onComplete(x => promisedT tryComplete (x))
        that.onComplete(y => promisedT tryComplete (y))
        
        promisedT.future
    }
}

val future1 = Future {
    Thread.sleep(1000)
    1
}

val future2 = Future(2)

future1.or(future2).foreach(println)
Thread.sleep(2000)

和Future通信,結束Future

type Cancellable[T] = (Promise[Unit], Future[T])

/**
 * 這個方法接收首一個方法參數b,b接收一個future,返回一個T
 * 我們在主線程裏完善Future就可以在返回T的代碼塊裏感知到
 * 然後通過判斷我們的Future是否完善來拋出異常來結束我們的異步程序
 *
 * @param b
 * @tparam T
 * @return
 */

def cancellable[T](b: Future[Unit] => T): Cancellable[T] = {
    import scala.concurrent.ExecutionContext.Implicits.global
    val cancel = Promise[Unit]
    val f = Future {
        val r = b(cancel.future)
        
        // 在這裏向cancel對象執行賦值操作,如果我們的cancel已經被賦值
        // 如果我們正常結束,這裏就不會被賦值
        // 如果從外部調用cancel結束,那這裏就會拋出異常
        if (!cancel.tryFailure(new Exception)) {
            throw new CancellationException
        }
        r
    }
    (cancel, f)
}

使用await進行同步

基本用法

import scala.concurrent.ExecutionContext.Implicits.global
val future = Future {
    Thread.sleep(1000)
    1
}
// 使用await.ready,返回完善返回值的Future對象
Await.ready(future,Duration.apply(3,TimeUnit.SECONDS)).foreach(println)
// 使用await.result,返回Future裏面的完善值
println(Await.result(future, Duration(3, TimeUnit.SECONDS)))

使用blocking語句,增加異步回調線程提高速度

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.blocking

//當前時間,單位是納秒
val startTime = System.nanoTime()
// 如果異步程序數大於默認線程數,異步程序會分批次執行
val futures = for (- <- 0 until 10) yield Future {
    Thread.sleep(1000)
}


// 如果異步程序裏,使用block結構,會創建多餘的線程執行程序,防止阻塞
//        val futures = for (- <- 0 until 10) yield Future {
//            blocking {
//                Thread.sleep(1000)
//            }
//        }

for (f <- futures) Await.ready(f, Duration.Inf)
val endTime = System.nanoTime()

println((endTime - startTime) / 1000000)

Async庫

async方法等價於Future

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章