It turns out that cancelling concurrent.futures.Futures doesn't help – they are already done by the time "SalLogHandler.close" is called. This is as expected because SalLogHandler is created by Controller, which waits for some time at shutdown to give final SAL messages time to be sent. So the underlying problem is not due to these concurrent futures still running at shutdown.
What did help immensely is using asyncio tasks (instead of concurrent futures) to emit messages whenever SalLogHandler is called from the main thread. That avoids the warning messages. Logging is very rarely done from a thread, so this dramatically reduces the number of warnings. The new code also keeps track of unfinished tasks and cancels them in the close method, just in case, even though it should not be necessary in practice, as explained above.
For future reference: I filed https://stackoverflow.com/questions/75090778/making-a-logging-handler-with-async-emit and perhaps someday that will get a useful answer. It is also conceivable that it is a bug in Python causing the messages for messages emitted using asyncio.run_coroutine_threadsafe, in which case the problem may go away with a future version of Python. If I get a better solution I'll use a new ticket to apply it.
Pull request: https://github.com/lsst-ts/ts_salobj/pull/265