如何正确使用Swift中的EventLoopF​​uture?

How to use EventLoopFuture in Swift properly?

我是 EventLoop 期货和承诺的新手。我的软件堆栈:

我有一些工作要做,正在寻找有关如何改进它的建议,因为我对 .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 的视图都可以安排从环境中提取一个,这样您就可以在应用程序中的所有视图之间共享事件循环。

线程安全

EventLoopGroups 使用自己线程的私有池来执行回调和应用程序工作。在 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)")
}