grpc/grpc-swift

NIO Event Loops and in the context of async/await

feelingsonice opened this issue · 3 comments

I'm having a hard time reconciling my understanding of Event Loops in the context of async/await. Using the following code as a reference:

class GRPCClientStub {
    static let authClient = Auth_AuthServiceAsyncClient(
        channel: GRPCClientStub.shared.channel
    )
    static let userClient = User_UserServiceAsyncClient(
        channel: GRPCClientStub.shared.channel
    )

    
    private static let shared = try! GRPCClientStub()
    private let group: EventLoopGroup = PlatformSupport.makeEventLoopGroup(loopCount: 1)
    private let channel: GRPCChannel

    private init() throws {
        channel = try GRPCChannelPool.with(
            target: .host("localhost", port: 8000),
            transportSecurity: .plaintext,
            eventLoopGroup: group
        )
    }
    
    deinit {
        try? group.syncShutdownGracefully()
    }
}

I have the following questions

  1. Is an event loop group like PlatformSupport.makeEventLoopGroup(loopCount: 1) creating a separate executor different from the executor created by Swift's native runtime?
  2. Is an event loop the same thing as a thread? i.e. it there a one-to-one mapping between event loops and threads?
  3. Should one increase the loop count and use a single gRPC channel for multiple clients or should one create a separate gRPC channel for each client? What would be the difference?
  4. Should one annotate the event loop / clients with a global actor? If not, do the loops contend with the MainActor?

I understand that I'm asking a lot under the same post but IMO these questions are really symptoms of the same underlying issue which is that I don't understand event loops in the context of Swift's async/await and there's no sources that bridges this gap.

Is an event loop group like PlatformSupport.makeEventLoopGroup(loopCount: 1) creating a separate executor different from the executor created by Swift's native runtime?

Yes.

Is an event loop the same thing as a thread? i.e. it there a one-to-one mapping between event loops and threads?

Not necessarily, no. Each event loop is a separate executor and separate isolation domain, but some event loop backends can re-use threads. For example, the swift-nio-transport-services backend uses Dispatch for its event loops, and those queues may be multiplexed across separate threads.

Should one increase the loop count and use a single gRPC channel for multiple clients or should one create a separate gRPC channel for each client? What would be the difference?

Loop count and client count are typically entirely independent. You can have multiple clients on the same loop without trouble. Generally you should create one event loop group and use it throughout the program. Then you can use multiple clients on that loop if you wish.

Generally the group should have 1 loop unless you know you're heavily I/O bound.

Should one annotate the event loop / clients with a global actor? If not, do the loops contend with the MainActor?

Event loops are not actors. They never contend with the main actor.

Adding a little more to Cory's answers.

  1. Should one increase the loop count and use a single gRPC channel for multiple clients or should one create a separate gRPC channel for each client? What would be the difference?

Prefer using fewer channels. The channel pool scales the number of connections dynamically (up to a configurable limit). One of the limiting factors is the number of event loops in the group you provide it with. I believe the default is maximum one connection per event loop (but this is configurable). Most http/2 servers limit the number concurrent RPCs per connection to be 100. This means that the limit on concurrent RPCs per channel is 100 * event loop group size * max conns per event loop.

I understand that I'm asking a lot under the same post but IMO these questions are really symptoms of the same underlying issue which is that I don't understand event loops in the context of Swift's async/await and there's no sources that bridges this gap.

I empathise with this, prior to async/await all the APIs were built on top of NIOs futures and the API is currently a mixture of the two. This isn't viable in the long term and at the moment we're working on v2 which makes NIO an implementation detail.

Thanks for the explanations. I guess I'll look forward to v2. These explanations works for me for now :)