Swift vapor3 - Async-非同步處理
Async-非同步處理
Vapor 3最重要的新功能之一是(Async)非同步處理, 但也可能是最令人困惑的一個功能。 為什麼它如此重要呢?
想象一下您的伺服器只有一個執行緒但是有四個客戶端需要請求資源的情況,請求順序如下:
- 對股票報價資訊的請求。 這需要在另一臺伺服器上呼叫API來獲取結果。
- 對靜態CSS樣式表的請求。 CSS無需查詢即可立即使用並返回結果。
- 對使用者使用者資料的請求。 必須從資料庫中獲取使用者資訊資料。
- 對一些靜態HTML的請求。 HTML無需查詢即可立即使用並及時向客戶端返回結果。
在同步伺服器(synchronous server)的處理中,伺服器的唯一執行緒將一直阻塞,直到獲得股票報價資訊。 獲取後它返回股票報價資訊和CSS樣式表資訊。 但在資料庫獲取使用者資訊時其再次阻塞(因為需要查詢資料庫資訊)。 只有完全獲取到使用者資訊並返回結果後,伺服器才會將靜態HTML返回給客戶端。
另一方面,在非同步伺服器(asynchronous server)中,執行緒啟動呼叫去獲取股票報價資訊,因為其不能立即返回結果,於是就將此請求放在一邊讓其自己處理直到獲得結果並返回。 但同時他會處理第二個請求並立即返回CSS樣式表資訊,緊接著啟動資料庫去獲取使用者資訊(因為使用者資訊需要耗時查詢資料庫所以也需要放一邊)並及時返回獲取到的靜態HTML。 當放在一邊的請求處理(獲取股票報價資訊和查詢資料庫使用者資訊)完成後,執行緒將繼續處理並將結果返回給客戶端。
你可能會說,伺服器有很多執行緒啊。的確是有不少!但是執行緒的數量雖多也是有上限的。同時線上程之間切換處理環境開銷也是巨大的,並且還要確保所有資料訪問都是執行緒安全的,這非常耗時且容易出錯。 因此,嘗試僅通過新增執行緒來解決問題是一種糟糕,且低效的解決方案。
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
}
}
- 從資料庫中獲取所有使用者。 如上所述,
getAllUsers()
返回Future <[User]>
。 由於完成此Future
的結果是另一個Future
(參見步驟3),因此使用flatMap(to :)
來解包結果。flatMap(to :)
的閉包接收完成的Future
-[User]
作為引數。 這個.flatMap(to :)
返回Future <HTTPStatus>
。 - 更新第一個使用者的名稱。
- 將更新的使用者儲存到資料庫。 這將返回
Future <User>
,但您需要返回的HTTPStatus
值並不是Future
,因此需使用map(to :)
。 - 返回適當的
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)
}
- 定義一個
Future <User>
的陣列,即第二步中save()
的返回型別。 - 迴圈遍歷
users
陣列中的每個使用者,並將user.save()
的返回值新增到陣列中。 - 使用
flatten(on :)
等待所有future
完成。 這需要一個Worker,即實際執行任務的執行緒。 worker通常是Vapor中的請求。 如果需要,flatten(on :)
的閉包將返回的collection作為引數。 - 返回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)
}
- 使用全域性
flatMap(to:_:_ :)
等待兩個future完成。 - 閉包將完成的
future
作為引數. - 呼叫
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
}
- 運用全域性函式
map(to:_:_:)
去等待兩個Future
完成。 - 這個閉包將兩個處理完成的
futures
作為引數。 - 呼叫同步函式
syncAddData(_:)
; - 返回
.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)
}
- 定義一個從請求中建立
TrackingSession
的函式。 這將返回Future <TrackingSession>
。 - 定義一個從請求中獲取
Tracking Session
的函式。 - 嘗試使用請求的
key
建立Tracking Session
。 如果無法建立Tracking Session
,則返回nil
。 - 確保
Session
已成功建立,否則建立新的Tracking Session
。 - 使用
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")
}
- 嘗試儲存使用者。 如果出現錯誤,提供
catchMap(_ :)
來處理這個錯誤。此閉包將error
作為引數,並且必須返回已解析的future
的型別 - 在本例中為User
。 - 列印出收到的錯誤資訊。
- 建立一個預設的使用者例項並返回。
當相關聯的閉包返回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(_ :)
。
catchMap
和catchFlatMap
只在失敗時執行它們的閉包。 但是如果你想要同時處理錯誤並處理成功案例呢? 簡單! 只需鏈式呼叫適當的方法即可!
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")
}
- Save a user and set the result to
userResult
. This is of typeFuture<User>
. - Chain an
always
to the result. - 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
相關文章
- Swift Image 的處理Swift
- Swift的時區處理Swift
- Netty之非阻塞處理Netty
- 非同步流程處理非同步
- 非同步處理方法非同步
- JavaScript | 非同步處理JavaScript非同步
- js中非同步處理JS非同步
- Laravel 事件&非同步處理Laravel事件非同步
- QT執行緒同步與非同步處理QT執行緒非同步
- DeferredResult——非同步請求處理非同步
- js的三種非同步處理JS非同步
- Mysql自動處理同步報錯MySql
- js學習之非同步處理JS非同步
- 影像處理的實現與應用(Swift 版)Swift
- DDGScreenShot —iOS 圖片處理--多圖片拼接 (swift)iOSSwift
- 同步非同步,阻塞非阻塞非同步
- 非同步、同步、阻塞、非阻塞非同步
- JavaScript非同步處理的那些事兒JavaScript非同步
- [轉] Scala 中的非同步事件處理非同步事件
- 非同步處理方案系列- 1.callback非同步
- Oracle DG同步失敗故障處理(二)Oracle
- SpringBoot 教程之處理非同步請求Spring Boot非同步
- JavaScript 非同步函式的 Promisification 處理JavaScript非同步函式
- ActiveMQ-MessageListener非同步回撥處理MQ非同步
- SpringMVC非同步處理的 5 種方式SpringMVC非同步
- 同步、非同步,阻塞、非阻塞理解非同步
- 同步、非同步、阻塞與非阻塞非同步
- 同步非同步 與 阻塞非阻塞非同步
- 理解阻塞、非阻塞、同步、非同步非同步
- 直播帶貨原始碼,非同步處理中會處理兩次請求原始碼非同步
- tidb之dm叢集同步異常處理TiDB
- 處理可能超時的非同步操作非同步
- swift-bridge支援從Swift呼叫非同步Rust函式Swift非同步Rust函式
- Go 臨界資源的安全問題(引入同步非同步處理)Go非同步
- java同步非阻塞IOJava
- Swift---協議和擴充套件、 錯誤處理、泛型Swift協議套件泛型
- Swift-為什麼實現Equatable不需要處理OptionalSwift
- 同步、非同步、阻塞、非阻塞的區別非同步