KernelClient: factor out generic async client from Qt client

before #7108 it was simple for a third-party front-end to recieve message notifications asynchronously from (non-blocking) KernelClient by overriding, or even monkeypatching the `call_handlers` method on the channels like this
```
self.kernel_client.shell_channel.call_handlers = self.on_shell_msg
self.kernel_client.iopub_channel.call_handlers = self.on_iopub_msg
...
```
This is not as simple anymore; now `qt.client` implements the requied logic (a single ioloop thread) but it is tied to the Qt signal/slot mechanism. This PR splits out a generic async kernel client from QtKernelClient, that essentially preserves the behaviour of the old `KernelClient` (execpt that is uses less threads, thanks to #7108).

The only (intended) "semantical" change is that `_check_kernel_info_client` is moved outside `call_handlers` (so that subclasses don't need to care about this), but I don't know the exact purpose this have, so I can't be entirely sure this is handled correctly. 

I'm not sure `AsyncKernelClient` is the best name tho, perhaps `ThreadKernelClient` is more explicit?


I was thinking about the thread safety of the changes it makes to session to set up the protocol adaptation. But on thinking some more, I guess that session is mostly/only used from the ioloop thread once it's been set up, so it should be OK.