Как правильно использовать EventLoopFuture в Swift?
Я новичок в фьючерсах и обещаниях EventLoop. Мой программный стек:
- Бэкэнд в Go + gRPC
- Клиент iOS в Swift + SwiftUI + GRPC + NIO
У меня есть над чем поработать, и я ищу предложения, как это улучшить, так как я немного потерялся в документации. .map
, .flatMap
, .always
, так далее.
Вот соответствующая функция из моего синглтона данных gRPC в приложении iOS:
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 View:
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 ответ
У меня есть одно очень конкретное предложение, за которым следуют несколько общих. Первое, наиболее конкретное предложение состоит в том, что если вы не пишете код NIO, вам, вероятно, не нужно создаватьEventLoop
s на всех. 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
быть 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)")
}