In commit "proxy: defer thread-local client cleanup" (90c1ae7b602c407f56bbd2ff61cc5fc12a7be9c6)
I think this would be simpler, and avoid some ordering dependencies between event loops, if it queued cleanup for each map entry on that entry's own event loop instead of queuing everything on the first entry's loop.
These destructors run on threads libmultiprocess doesn't control, which can make calls over any number of connections and event loops. With the current approach, all of the thread's clients are released by one loop's async thread, which calls sync() into the other loops. So that loop's other async cleanup (m_impl destruction, worker joins) waits on the other loops, and the other loops can't shut down until that loop's async thread gets to the callback holding their clients. I don't think this causes a problem today, but it's an assumption that isn't needed.
Queuing per entry would also remove the empty() checks, the choice of loop, and moving clients while its mutex is locked. Something like (untested):
ThreadContext::~ThreadContext()
{
// The Windows loader lock may be held here. Queue cleanup for each client
// on its own event loop without waiting for the loop or for a thread to
// start. Disconnect callbacks keep referring to the shared maps and mutex.
std::shared_ptr<ThreadClients> shared{std::move(clients)};
const Lock lock(shared->mutex);
for (ConnThreads* threads : {&shared->request_threads, &shared->callback_threads}) {
for (const auto& [connection, client] : *threads) {
EventLoop& loop = *client->m_context.loop;
loop.addAsyncCleanup([&loop, shared, threads, connection = connection] {
loop.sync([&] { shared->release(*threads, connection); });
});
}
}
}
where ThreadClients::release() extracts the entry for connection under the mutex, if a disconnect hasn't already removed it, and destroys it after unlocking. That's what the SetThread disconnect callback does after 9783fb2eb23d316842eb47bea9a394986304e276 from #361, so on top of that commit the two could share the helper. clients would need to be a std::shared_ptr since several callbacks share it.
This would also replace the synchronous ~ThreadContext that commit adds. Its race between the exiting thread and the disconnect callback can't happen once every entry is released on its loop thread, since disconnect callbacks run on that thread too. Its regression test, "Thread exiting while its connection is destroyed", would need updating, since its hook blocks the exiting thread inside ~ProxyClient<Thread>, which would no longer run on that thread.