Have you looked at IO::Select? It will allow you to have a loop which is "simultaneously" waiting for input from multiple sources and waiting for outputs to become ready for write and allowing for timeouts. Or you could use the underlying select call.

It is architecturally uncomplicated: the process is actually doing only one thing at a time, even though it is unpredictable what it might do next.

You never have to worry about things being incomplete ... about accidentally sending someone a partial-message that you haven’t finished reading yet.

You can easily prioritize what it is doing, according to the business requirement.

Messy timing issues are generally avoided completely.

The program itself is really a dispatcher, connected to everything else with nice, flexible pipes and sockets. It goes to sleep, wakes up with a “honey-do list,” does its chores in whatever sequence it best sees fit, then goes back to sleep again. As long as none of these things take a significant time to actually do, esp. relative to the others, it all works great.