Swift vapor3 - Async-非同步處理

weixin_33890499發表於2018-09-25

Async-非同步處理

Vapor 3最重要的新功能之一是(Async)非同步處理, 但也可能是最令人困惑的一個功能。 為什麼它如此重要呢?

想象一下您的伺服器只有一個執行緒但是有四個客戶端需要請求資源的情況,請求順序如下:

  1. 對股票報價資訊的請求。 這需要在另一臺伺服器上呼叫API來獲取結果。
  2. 對靜態CSS樣式表的請求。 CSS無需查詢即可立即使用並返回結果。
  3. 對使用者使用者資料的請求。 必須從資料庫中獲取使用者資訊資料。
  4. 對一些靜態HTML的請求。 HTML無需查詢即可立即使用並及時向客戶端返回結果。

在同步伺服器(synchronous server)的處理中,伺服器的唯一執行緒將一直阻塞,直到獲得股票報價資訊。 獲取後它返回股票報價資訊和CSS樣式表資訊。 但在資料庫獲取使用者資訊時其再次阻塞(因為需要查詢資料庫資訊)。 只有完全獲取到使用者資訊並返回結果後,伺服器才會將靜態HTML返回給客戶端。

另一方面,在非同步伺服器(asynchronous server)中,執行緒啟動呼叫去獲取股票報價資訊,因為其不能立即返回結果,於是就將此請求放在一邊讓其自己處理直到獲得結果並返回。 但同時他會處理第二個請求並立即返回CSS樣式表資訊,緊接著啟動資料庫去獲取使用者資訊(因為使用者資訊需要耗時查詢資料庫所以也需要放一邊)並及時返回獲取到的靜態HTML。 當放在一邊的請求處理(獲取股票報價資訊和查詢資料庫使用者資訊)完成後,執行緒將繼續處理並將結果返回給客戶端。

2270079-68f0d129f10db192.png
image.png

你可能會說,伺服器有很多執行緒啊。的確是有不少!但是執行緒的數量雖多也是有上限的。同時線上程之間切換處理環境開銷也是巨大的,並且還要確保所有資料訪問都是執行緒安全的,這非常耗時且容易出錯。 因此,嘗試僅通過新增執行緒來解決問題是一種糟糕,且低效的解決方案。

Futures 和 Promises

為了在等待響應時“擱置”請求,您必須將請求包含在promise中,以便在收到響應時恢復其執行。 實際上,這意味著您必須更改“擱置”的函式返回型別。

在同步的環境中,一個函式通常可以像這樣書寫:

func getAllUsers() -> [User] {
    //do sth db queries
}

在非同步環境中,這種方式是不行的,因為在getAllUsers()必須返回時,您的資料庫呼叫可能尚未完成。 你知道你將來能夠返回[User],但現在不行。 在Vapor中,您承諾(promise)提供結果稱為未來(Future)。 所以你的程式碼應該是這樣:

func getAllUsers() -> Future<[User]> {
    //do sth db queries
}

使用Future去處理任務

Unwrapping futures

Vapor具有許多convenience functions是和future一起使用的。 但是,有很多場景需要使用future並等待promise執行。 為了演示,假設您有一條返回HTTP狀態程式碼204 No Content的路由。 此路由使用類似上述功能從資料庫中獲取使用者列表,並在返回之前修改列表中的第一個使用者。

為了使用該呼叫的結果,您必須unwrapp結果並提供一個閉包,以便在Future解析時執行。 您將使用兩個主要函式來執行此操作:

  • flatMap(to:): 用於當promise閉包返回Future型別時使用。
  • map(to:): 當promise閉包返回Future以外的型別時使用。
// 1
return database
       .getAllUsers()
       .flatMap(to: HTTPStatus.self) { users in
  // 2
  let user = users[0]
  user.name = "Bob"
  // 3
  return user.save().map(to: HTTPStatus.self) { user in
    //4    
    return HTTPStatus.noContent
  }
}
  1. 從資料庫中獲取所有使用者。 如上所述,getAllUsers()返回Future <[User]>。 由於完成此Future的結果是另一個Future(參見步驟3),因此使用flatMap(to :)來解包結果。 flatMap(to :)的閉包接收完成的Future - [User]作為引數。 這個.flatMap(to :)返回Future <HTTPStatus>
  2. 更新第一個使用者的名稱。
  3. 將更新的使用者儲存到資料庫。 這將返回Future <User>,但您需要返回的HTTPStatus值並不是Future,因此需使用map(to :)
  4. 返回適當的HTTPStatus值。

Transform

有時你不關心Future的結果,只關心它是否成功。 在上面的示例中,你不想使用save()的結果並對其解包操作。 對於此場景,您可以使用transform(to :)簡化步驟3:

return database
       .getAllUsers()
       .flatMap(to: HTTPStatus.self) { users in
  let user = users[0]
  user.name = "Bob"
  return user.save().transform(to: HTTPStatus.noContent)
}

這有助於減少程式碼的巢狀量,並使程式碼更易於閱讀和維護。

Flatten

有時您必須等待一些Futures完成。 比如在資料庫中儲存多個模型時。 在這種情況下,您使用flatten(on:)。 例如:

static func save(_ users: [User], request: Request)
  -> Future<HTTPStatus> {
  // 1
  var userSaveResults: [Future<User>] = []
  // 2
  for user in users {
    userSaveResults.append(user.save())
  }
  // 3
  return userSaveResults.flatten(on: request)
    //4
    .transform(to: HTTPStatus.created)
}
  1. 定義一個Future <User>的陣列,即第二步中save()的返回型別。
  2. 迴圈遍歷users陣列中的每個使用者,並將user.save()的返回值新增到陣列中。
  3. 使用flatten(on :)等待所有future完成。 這需要一個Worker,即實際執行任務的執行緒。 worker通常是Vapor中的請求。 如果需要,flatten(on :)的閉包將返回的collection作為引數。
  4. 返回201 Created狀態。

flatten(on :)等待所有future返回,因為它們是由同一個Worker非同步執行的。

Multiple futures

有時候,你需要等待一些不相互依賴的不同型別的Future。 例如,在解碼請求資料並從資料庫中獲取使用者時會遇到這種情況。 Vapor提供了許多全域性的convenience函式,可以等待多達五種不同的future。 這有助於避免程式碼的深層巢狀或令人困惑的鏈式書寫。

如果你有兩個future-從資料庫中獲取使用者列表,及從請求中解碼一些資料,你可以這樣做:

// 1
flatMap(
  to: HTTPStatus.self,
  database.getAllUsers(),
  // 2
  request.content.decode(UserData.self)) { allUsers, userData in
    // 3
    return allUsers[0]
      .addData(userData)
      .transform(to: HTTPStatus.noContent)
}
  1. 使用全域性flatMap(to:_:_ :)等待兩個future完成。
  2. 閉包將完成的future作為引數.
  3. 呼叫addData(_ :),它返回一些future的結果並將返回型別transform.noContent

如果閉包返回非future結果,則可以使用全域性map(to:_:_ :)代替:

// 1
map(
  to: HTTPStatus.self,
  database.getAllUsers(),
  // 2
  request.content.decode(UserData.self)) { allUsers, userData in
    // 3
    allUsers[0].syncAddData(userData)
    // 4
    return HTTPStatus.noContent
}
  1. 運用全域性函式 map(to:_:_:) 去等待兩個Future完成。
  2. 這個閉包將兩個處理完成的futures作為引數。
  3. 呼叫同步函式syncAddData(_:);
  4. 返回 .noContent;

建立 Futures

有時你需要建立自己的Future。 如果if語句返回型別不是Future,而else返回的型別是Future,編譯器會丟擲錯誤(這些返回型別必須一致)。 要解決此問題,您必須使用request.future(_ :)將非Future型別轉換為Future型別。 例如:

// 1
func createTrackingSession(for request: Request)
  -> Future<TrackingSession> {
  return request.makeNewSession()
}

// 2
func getTrackingSession(for request: Request)
  -> Future<TrackingSession> {
  // 3
  let session: TrackingSession? =
    TrackingSession(id: request.getKey())
  // 4
  guard let createdSession = session else {
    return createTrackingSession(for: request)
  }
  // 5
return request.future(createdSession)
}
  1. 定義一個從請求中建立TrackingSession的函式。 這將返回Future <TrackingSession>
  2. 定義一個從請求中獲取Tracking Session的函式。
  3. 嘗試使用請求的key建立Tracking Session。 如果無法建立Tracking Session,則返回nil
  4. 確保Session已成功建立,否則建立新的Tracking Session
  5. 使用request.future(_ :)createdSession中建立Future <TrackingSession>。 這將返回執行請求的同一個Worker上的Future

由於createTrackingSession(for :)返回Future<TrackingSession>,您必須使用request.future(_ :)createdSession轉換為Future<TrackingSession>以使編譯器不出現報警異常。

錯誤的處理

Vapor在其整個框架中大量使用Swift的錯誤處理。 許多函式throws,允許您處理不同級別的錯誤。 您可以選擇在route handlers內處理錯誤,或者使用中介軟體(middleware)來捕獲更高階別的錯誤,或兩者兼而有之。

但是,在非同步世界中處理錯誤有點不同。 你不能使用Swift的do/catch,因為不知道什麼時候會執行do/catch。 Vapor提供了許多函式來幫助處理這些錯誤。 在基本層面上,Vapor有自己的do/catch回撥函式與Futures一起使用:

let futureResult = user.save()
futureResult.do { user in
  print("User was saved")
}.catch { error in
  print("There was an error saving the user: \(error)")
}

在Vapor中,您必須在處理請求時返回一些內容,即使它的型別是future。 使用上面的do/catch方法不會阻止錯誤的發生,但它會讓你看到錯誤是什麼。 如果save()呼叫失敗並返回futureResult,則失敗仍然會沿著呼叫鏈向上傳播。 但是,在大多數情況下,您希望嘗試糾正此問題。

Vapor提供了catchMap(_ :)catchFlatMap(_ :)來處理這種型別的failure。 這允許您處理錯誤(error),並修復它或丟擲不同的錯誤(error)。 例如:

// 1
return user.save(on: req).catchMap { error -> User in
  // 2
  print("Error saving the user: \(error)")
  // 3
  return User(name: "Default User")
}
  1. 嘗試儲存使用者。 如果出現錯誤,提供catchMap(_ :)來處理這個錯誤。此閉包將error作為引數,並且必須返回已解析的future的型別 - 在本例中為User
  2. 列印出收到的錯誤資訊。
  3. 建立一個預設的使用者例項並返回。

當相關聯的閉包返回Future時,Vapor還提供相關的catchFlatMap(_ :)方法:

return user.save().catchFlatMap { error -> Future<User> in
  print("Error saving the user: \(error)")
  return User(name: "Default User").save()
}

由於save()返回future,因此必須呼叫catchFlatMap(_ :)

catchMapcatchFlatMap只在失敗時執行它們的閉包。 但是如果你想要同時處理錯誤並處理成功案例呢? 簡單! 只需鏈式呼叫適當的方法即可!

future的鏈式呼叫

處理future有時在使用時很容易得到巢狀多層深度的程式碼。 Vapor允許您將future鏈式呼叫而不是多層巢狀的使用它們。 例如,考慮一個如下所示的程式碼段:

return database
       .getAllUsers()
       .flatMap(to: HTTPStatus.self) { users in
  let user = users[0]
  user.name = "Bob"
  return user.save().map(to: HTTPStatus.self) { user in
    return HTTPStatus.noContent
  }
}

方法map(to:)flatMap(to:)可以一起鏈式使用。

return database
       .getAllUsers()
       // 1
       .flatMap(to: User.self) { users in
                let user = users[0]
                user.name = "Bob"
                return user.save()
        // 2
        }
       .map(to: HTTPStatus.self) { user in
            return HTTPStatus.noContent
        }

更改flatMap(to:)的返回型別允許鏈式呼叫map(to:),它接收Future <User>。 最終的map(to:)返回你最初要返回的型別。

future鏈式呼叫允許您減少程式碼中的巢狀,並且可以使其更容易理解,這在非同步世界中尤其有用。 然而,無論你是巢狀使用或鏈式呼叫這完全是個人喜好。

Always

有時無論future的結果如何, 你都想要執行一些事情。 您可能需要關閉連線,觸發通知或僅記錄future的執行。 對於這些使用always回撥進行處理。 例如:

// 1
let userResult: Future<User> = user.save()
// 2
userResult.always {
  // 3
  print("User save has been attempted")
}
  1. Save a user and set the result to userResult. This is of type Future<User>.
  2. Chain an always to the result.
  3. Print a string when the app executes the future.

無論是future的結果是失敗還是成功,閉包always都會被執行。 它對future也沒有影響。 您也可以將其與其他方法一起鏈式呼叫。

Waiting

在某些情況下,您可能希望實際等待結果返回。 為此可以使用wait()

注意:這有一個很大的警告:你不能在主事件迴圈上使用wait(),這意味著所有請求處理程式和大多數其他情況不能使用wait()

但是,這個方法在測試中尤其有用,因為編寫非同步測試很困難。 例如:

let savedUser = try user.save(on: connection).wait()

savedUser的型別不是Future<User>,因為您使用的是wait(),在這個例項中savedUser是一個User物件。請注意,如果執行promise失敗,wait()將引發錯誤。

值得重申的是:不能在主事件迴圈內使用wait()!

翻譯自Raywenderlich--Server Side Swift with Vapor

相關文章