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

时间:2019-11-30 18:26:24

标签: swift promise future grpc swift-nio

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

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

由于我对.map.flatMap.always等文档有些迷茫,因此我有一些工作要做,并且正在寻求有关改进建议的建议。< / p>

这是我在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吗?有什么具体建议吗?

1 个答案:

答案 0 :(得分:2)

我有一个非常具体的建议,然后是一些一般性建议。第一个最具体的建议是,如果您不编写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)构造和拥有。然后它们将被系统中的其他组件“借用”。如前所述,对于您的特定应用程序,您可能希望事件循环归grpc-swift所有,因此您不应该关闭任何事件循环,但是通常,如果您遵循此政策,则适用一条明确的规则:如果您未创建EventLoop,请不要将其关闭。不是你的,你只是在借它。

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

EventLoopGroups是顶级对象

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

对于SwiftUI,最好的做法是让EventLoopGroupEnvironmentObject并由AppDelegate注入视图层次结构。然后,每个需要EventLoopGroup的视图都可以安排从环境中提取一个视图,这使您可以在应用程序中的所有视图之间共享事件循环。

线程安全

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