如何正确使用Swift中的EventLoopFuture?
How to use EventLoopFuture in Swift properly?
我是 EventLoop 期货和承诺的新手。我的软件堆栈:
- Go + gRPC 中的后端
- iOS客户端在Swift+SwiftUI+GRPC+NIO
我有一些工作要做,正在寻找有关如何改进它的建议,因为我对 .map
、.flatMap
、.always
等方面的文档有点迷茫.
这是 iOS 应用程序中我的 gRPC 数据单例的相关函数:
import Foundation
import NIO
import GRPC
class DataRepository {
static let shared = DataRepository()
// skip ...
func readItem(id: Int64, eventLoop: EventLoop) -> EventLoopFuture<V1_ReadResponse> {
// TODO: Is this the right place?
defer {
try? eventLoop.syncShutdownGracefully()
}
let promise = eventLoop.makePromise(of: V1_ReadResponse.self)
var request = V1_ReadRequest()
request.api = "v1"
request.id = id
let call = client.read(request, callOptions: callOptions) // client - GRPCClient initialized in the singleton
call.response.whenSuccess{ response in
return promise.succeed(response)
}
call.response.whenFailure{ error in
return(promise.fail(error))
}
return promise.futureResult
}
我的代码在SwiftUI视图中:
import SwiftUI
import NIO
struct MyView : View {
@State private var itemTitle = "None"
var body: some View {
Text(itemTitle)
}
func getItem() {
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let result = DataRepository.shared.readItem(id: 1, eventLoop: eventLoopGroup.next())
_ = result.always { (response: Result<V1_ReadResponse, Error>) in
let res = try? response.get()
if let resExist = res {
self.itemTitle = resExist.item.title
}
_ = response.mapError{ (err: Error) -> Error in
print("[Error] Connection error or item not found: \(err)")
return err
}
}
}
我应该重构 getItem
and/or readItem
吗?有什么具体建议吗?
我有一个非常具体的建议,然后是一些一般性的建议。第一个也是最具体的建议是,如果您不编写 NIO 代码,则可能根本不需要创建 EventLoop
。 grpc-swift 将创建自己的事件循环,您可以简单地将其用于回调管理。
这让您可以将 readItem
代码重构为:
func readItem(id: Int64, eventLoop: EventLoop) -> EventLoopFuture<V1_ReadResponse> {
var request = V1_ReadRequest()
request.api = "v1"
request.id = id
let call = client.read(request, callOptions: callOptions) // client - GRPCClient initialized in the singleton
return call.response
}
这大大简化了您的代码,让您基本上可以将管理事件循环的所有复杂工作推迟到 grpc-swift,因此您可以担心您的应用程序代码。
否则,这里有一些一般性的建议:
不要关闭你不拥有的事件循环
在 readItem
的顶部你有这个代码:
// TODO: Is this the right place?
defer {
try? eventLoop.syncShutdownGracefully()
}
你可以看到我在上面的例子中删除了它。那是因为这不是合适的地方。事件循环是长期存在的对象:构造和关闭它们的成本很高,因此您很少想这样做。通常,您希望事件循环由相当高级的对象(如 AppDelegate
,或某些高级视图控制器或 View
)构建和拥有。然后它们将被系统中的其他组件"borrowed"。对于您的特定应用程序,正如我所指出的,您可能希望您的事件循环由 grpc-swift 拥有,因此您不应该关闭 any 事件循环,但一般来说,如果您采用此策略,则适用一条明确的规则:如果您没有创建 EventLoop
,请不要将其关闭。不是你的,是借用的
事实上,在 NIO 2.5.0 中,NIO 团队 made it an error 以这种方式关闭事件循环:如果您将 try?
替换为 try!
,您会看到在您的应用程序中崩溃。
EventLoopGroup 是顶级对象
在您的 MyView.getItem
函数中创建一个 MultiThreadedEventLoopGroup
。我上面的建议是你根本不要创建自己的事件循环,但如果你打算这样做,这不是一个好地方。
对于 SwiftUI,最好的办法是让你的 EventLoopGroup
成为一个 EnvironmentObject
,由 AppDelegate
注入到视图层次结构中。每个需要 EventLoopGroup
的视图都可以安排从环境中提取一个,这样您就可以在应用程序中的所有视图之间共享事件循环。
线程安全
EventLoopGroup
s 使用自己线程的私有池来执行回调和应用程序工作。在 getItem
中,您从这些未来回调之一中修改视图状态,而不是从主队列中修改。
您应该在修改您的状态之前使用DispatchQueue.main.async { }
重新加入主队列。您可能希望将其包装在这样的辅助函数中:
extension EventLoopFuture {
func always<T>(queue: DispatchQueue, _ body: (Result<T>) -> Void) {
return self.always { result in
queue.async { body(result) }
}
}
}
回调重构
作为次要的生活质量,此代码块可以重构自:
let res = try? response.get()
if let resExist = res {
self.itemTitle = resExist.item.title
}
_ = response.mapError{ (err: Error) -> Error in
print("[Error] Connection error or item not found: \(err)")
return err
}
对此:
switch response {
case .success(let response):
self.itemTitle = response.item.title
case .failure(let err):
print("[Error] Connection error or item not found: \(err)")
}
我是 EventLoop 期货和承诺的新手。我的软件堆栈:
- Go + gRPC 中的后端
- iOS客户端在Swift+SwiftUI+GRPC+NIO
我有一些工作要做,正在寻找有关如何改进它的建议,因为我对 .map
、.flatMap
、.always
等方面的文档有点迷茫.
这是 iOS 应用程序中我的 gRPC 数据单例的相关函数:
import Foundation
import NIO
import GRPC
class DataRepository {
static let shared = DataRepository()
// skip ...
func readItem(id: Int64, eventLoop: EventLoop) -> EventLoopFuture<V1_ReadResponse> {
// TODO: Is this the right place?
defer {
try? eventLoop.syncShutdownGracefully()
}
let promise = eventLoop.makePromise(of: V1_ReadResponse.self)
var request = V1_ReadRequest()
request.api = "v1"
request.id = id
let call = client.read(request, callOptions: callOptions) // client - GRPCClient initialized in the singleton
call.response.whenSuccess{ response in
return promise.succeed(response)
}
call.response.whenFailure{ error in
return(promise.fail(error))
}
return promise.futureResult
}
我的代码在SwiftUI视图中:
import SwiftUI
import NIO
struct MyView : View {
@State private var itemTitle = "None"
var body: some View {
Text(itemTitle)
}
func getItem() {
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let result = DataRepository.shared.readItem(id: 1, eventLoop: eventLoopGroup.next())
_ = result.always { (response: Result<V1_ReadResponse, Error>) in
let res = try? response.get()
if let resExist = res {
self.itemTitle = resExist.item.title
}
_ = response.mapError{ (err: Error) -> Error in
print("[Error] Connection error or item not found: \(err)")
return err
}
}
}
我应该重构 getItem
and/or readItem
吗?有什么具体建议吗?
我有一个非常具体的建议,然后是一些一般性的建议。第一个也是最具体的建议是,如果您不编写 NIO 代码,则可能根本不需要创建 EventLoop
。 grpc-swift 将创建自己的事件循环,您可以简单地将其用于回调管理。
这让您可以将 readItem
代码重构为:
func readItem(id: Int64, eventLoop: EventLoop) -> EventLoopFuture<V1_ReadResponse> {
var request = V1_ReadRequest()
request.api = "v1"
request.id = id
let call = client.read(request, callOptions: callOptions) // client - GRPCClient initialized in the singleton
return call.response
}
这大大简化了您的代码,让您基本上可以将管理事件循环的所有复杂工作推迟到 grpc-swift,因此您可以担心您的应用程序代码。
否则,这里有一些一般性的建议:
不要关闭你不拥有的事件循环
在 readItem
的顶部你有这个代码:
// TODO: Is this the right place?
defer {
try? eventLoop.syncShutdownGracefully()
}
你可以看到我在上面的例子中删除了它。那是因为这不是合适的地方。事件循环是长期存在的对象:构造和关闭它们的成本很高,因此您很少想这样做。通常,您希望事件循环由相当高级的对象(如 AppDelegate
,或某些高级视图控制器或 View
)构建和拥有。然后它们将被系统中的其他组件"borrowed"。对于您的特定应用程序,正如我所指出的,您可能希望您的事件循环由 grpc-swift 拥有,因此您不应该关闭 any 事件循环,但一般来说,如果您采用此策略,则适用一条明确的规则:如果您没有创建 EventLoop
,请不要将其关闭。不是你的,是借用的
事实上,在 NIO 2.5.0 中,NIO 团队 made it an error 以这种方式关闭事件循环:如果您将 try?
替换为 try!
,您会看到在您的应用程序中崩溃。
EventLoopGroup 是顶级对象
在您的 MyView.getItem
函数中创建一个 MultiThreadedEventLoopGroup
。我上面的建议是你根本不要创建自己的事件循环,但如果你打算这样做,这不是一个好地方。
对于 SwiftUI,最好的办法是让你的 EventLoopGroup
成为一个 EnvironmentObject
,由 AppDelegate
注入到视图层次结构中。每个需要 EventLoopGroup
的视图都可以安排从环境中提取一个,这样您就可以在应用程序中的所有视图之间共享事件循环。
线程安全
EventLoopGroup
s 使用自己线程的私有池来执行回调和应用程序工作。在 getItem
中,您从这些未来回调之一中修改视图状态,而不是从主队列中修改。
您应该在修改您的状态之前使用DispatchQueue.main.async { }
重新加入主队列。您可能希望将其包装在这样的辅助函数中:
extension EventLoopFuture {
func always<T>(queue: DispatchQueue, _ body: (Result<T>) -> Void) {
return self.always { result in
queue.async { body(result) }
}
}
}
回调重构
作为次要的生活质量,此代码块可以重构自:
let res = try? response.get()
if let resExist = res {
self.itemTitle = resExist.item.title
}
_ = response.mapError{ (err: Error) -> Error in
print("[Error] Connection error or item not found: \(err)")
return err
}
对此:
switch response {
case .success(let response):
self.itemTitle = response.item.title
case .failure(let err):
print("[Error] Connection error or item not found: \(err)")
}