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?


This means that `_check_kernel_info_reply()` is now getting called in the ioloop thread, whereas previously it was called in the main application thread. I *think* this is safe, but at a minimum, we should put comments in `_check_kernel_info_reply` and `_handle_kernel_info_reply` mentioning this fact, so we don't do something later that assumes they're in the main thread.