7

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

  • Go + gRPC 中的后端
  • Swift + SwiftUI + GRPC + NIO 中的 iOS 客户端

我有一些工作要做,并正在寻找有关如何改进它的建议,因为我在 , , 等周围的文档中有点.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和/或readItem吗?有什么具体建议吗?

4

1 回答 1

16

我有一个非常具体的建议,然后是一些一般性的建议。第一个,最具体的建议是,如果您不编写 NIO 代码,您可能根本不需要创建EventLoops。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)构建和拥有。然后它们将被系统中的其他组件“借用”。正如我所指出的,对于您的特定应用程序,您可能希望您的事件循环由 grpc-swift 拥有,因此您不应该关闭任何事件循环,但一般来说,如果您采用此策略,那么一个明确的规则适用:如果您没有创建EventLoop,请不要将其关闭。这不是你的,你只是借来的。

事实上,在 NIO 2.5.0 中,NIO 团队以这种方式关闭事件循环是错误try?的:如果您替换为,try!您会看到应用程序崩溃。

EventLoopGroups 是顶级对象

在您的MyView.getItem函数中,您创建一个MultiThreadedEventLoopGroup. 我上面的建议是让你根本不要创建自己的事件循环,但如果你要这样做,这不是一个好地方。

对于 SwiftUI,最好的办法是让你EventLoopGroup成为一个EnvironmentObjectAppDelegate. 然后,每个需要的视图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)")
}
于 2019-12-09T09:20:47.043 回答