I share a session object among multiple channel handlers. Are thread-safe operations required for the object?
Yes, as all of those channels are operating on their own threads. You can see in my PacketHandler
type (and probably some NIO examples too) that access to shared data needs to be behind locks. Luckily NIO has the NIOConcurrencyHelpers
package to make that a bit easier.
Hi Jon, thank you for your reply and sample code. I'm a little confused about why we need a lock within the ChannelHandler
in your example. clientIDsToChannels
is a private property to PacketHandler
right? I thought all the ChannelHandler
's events are called on the same EventLoop
.
func handleConnect(_ connect: Connect, in context: ChannelHandlerContext) {
// TODO: Store all relevant properties, not just clientID
let channel = context.channel
Logger.log("Writing to activeChannels for clientID: \(connect.clientID)")
protector.withLock {
self.clientIDsToChannels[connect.clientID] = channel
self.channelsToClientIDs[ObjectIdentifier(channel)] = connect.clientID
precondition(self.clientIDsToChannels.count == self.channelsToClientIDs.count)
}
let acknowledgement = ConnectAcknowledgement(response: .accepted, isSessionPresent: false)
Logger.log("Sending ConnectAcknowledgement for clientID: \(connect.clientID)")
context.writeAndFlush(wrapOutbound(.connectAcknowledgement(acknowledgement))).whenComplete { _ in
Logger.log("Sent ConnectAcknowledgement for clientID: \(connect.clientID)")
}
}
In this example from SwiftNIO-HTTP2, the HTTP2StreamMultiplexer
doesn't use any lock. I assume that you shared the same PacketHandler
in multiple ChannelPipeline
s? Are we supposed to use ChannelHandler
in different Channel
s?
My PacketHandler
is also where my session state lives. It keeps track of all ongoing connections, not just the state for one, so it's added to the channels somewhat differently, as you can see in my DragonflyServer
class that sets up the channel handlers:
static func bootstrapServer(enableLogging: Bool) -> (group: MultiThreadedEventLoopGroup, bootstrap: ServerBootstrap) {
let sharedPacketHandler = PacketHandler()
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let bootstrap = ServerBootstrap(group: group)
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.childChannelInitializer { channel in
creatHandlers(for: channel, packetHandler: sharedPacketHandler, enableLogging: enableLogging)
}
.childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
.childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 16)
.childChannelOption(ChannelOptions.recvAllocator, value: AdaptiveRecvByteBufferAllocator())
return (group: group, bootstrap: bootstrap)
}
As you can see, I have one instance of PacketHandler
that is shared amongst all connections. Since these connections are handled in parallel, anything shared between them must be thread-safe. (Note that some of this setup may be outdated, as I haven't updated it since June, and there have been some changes to NIO that make some of it unnecessary.)
Whether it's great design to have my session state inside an ChannelInboundHandler
, I'm not sure, I'll let a NIO expert weight in here, but it is how the ChatServer
example works as well.
Thank you so much for the detailed explanation! Also, I was wondering if there is a preference of using Atomic/Lock
over Dispatch
. I see the ChatServer
example uses Dispatch
.
Lock
is much more performant and I didn’t want to rely on Dispatch
in general.
First of all, thanks for your question, please keep those coming. Exactly as @Jon_Shier said, every Channel
is associated with exactly one EventLoop (which owns one thread). So you do not need any synchronisation to access state on a ChannelHandler
(assuming you don’t add one handler instance to multiple Channels which almost always you should not).
If you need to access state from multiple ChannelHandler
s in different Channel
s, you do need to synchronise (unless you make sure all those Channel
s are on the same EventLoop which you can not generally assume but you can arrange for that to happen). And just to be very explicit, all ChannelHandler
s in the same Channel
(Pipeline
) are always called from the EventLoop the Channel is associated with. And all callbacks for all futures call back on the EventLoop they have been created from. And all Channel(Pipeline) operations always return futures from the EventLoop their Channel is associated with.
Those are the base rules. The benefits of this are: that unless you have state across multiple Channels, you never need to synchronise which makes it easy and fast.
Now, for HTTP2 there are additional guarantees: HTTP2 has multiple sorts of Channels: One root channel (the actual TCP) connection which can have many many child Channels which are used for the individual HTTP2 streams (one stream does exactly one HTTP request/response pair). The extra guarantee that NIOHTTP2 gives you is that these child Channels are on the same EventLoop as their root Channel. Therefore, the HTTP2 multiplexer does not need any thread synchronisation.
I hope this makes sense.
One other area that sometimes isn’t clear: All Channel API is fully thread safe, ie if you hold a Channel
you may call all of its public API from wherever.