Hi,
Why the non-threaded runtime, out of interest?
Threads forked with forkIO are pretty lightweight, and although things look like blocking calls from the Haskell point of view, as I understand it under the hood it's all done with events of one form or another. Thus even with the non-threaded runtime you will see forkIO-threads behaving as if they're running concurrently. In particular, you have two threads blocked trying to read from two different Handles and each will be awoken just when there's data to read, and the rest of the runtime will carry on even while they're blocked. Try it!
If you're dealing with FDs that you've acquired from elsewhere, the function
unix:System.Posix.IO.ByteString.fdToHandle can be used to import them and then they work like normal Handles in terms of blocking operations etc.
Whenever I've had to deal with waking up for one of a number of reasons (not all of which are FDs) I've found the simplicity of STM is hard to beat. Something like:
atomically ((Left <$> waitForFirstThing) <|> (Right <$> waitForSecondThing))
where waitForFirstThing and waitForSecondThing are blocked waiting for something interesting to occur in a TVar that they're watching. It's so simple that I reckon it's worth doing it like that and only trying something more complicated if it turns out from experimentation that this has too much overhead for you - "make it right" precedes "make it fast".
Hope that helps,
David