Ideally, all 3 of the basic comm I/O tasks should be performed using overlapped I/O. this can correctly be done with either one thread (doing all comm port I/O, which i have found to work quite nicely) or with up to 3 threads (one for input, one for output, one for status changes, if important) but i found that the latter was way way overkill.
after the various I/O's are hung on the port: wait for one of them to finish, handle it as quickly as is reasonable, and then hang the next I/O to replace the one you just completed. If there is no I/O of that type pending (e.g., you've written all you have to write for now) then just don't hang any new write until you have more data. You'll always be hanging a new read, of course, unless you're shutting down the comm. (And, by "handle it", i mean put incoming data in a queue somewhere and set an event or send a message for the main program thread to pick up the data - NOT "plot the trajectory of the comet described by the incoming data")
Overlapped I/O is the preferred method of comm port I/O in Windows. The code will be somewhat verbose, but it will work perfectly, will not lose characters as long as your ports are sufficiently buffered (check the hardware buffering capabilities if you wish, and adjust the buffering if necessary and if doing so is supported by your comm port hardware), and will contain no polling at all. if you're using ANY polling in code like this, it is a clear sign of a design defect.
HOWEVER - your first statement indicates a different problem. hanging a blocking read in a loop will NOT make your program CPU-busy UNLESS your port timeouts are set incorrectly (effectively making it a nonblocking read). Set the read timeout such that it waits until one data byte is available. DO NOT set it so that it 1) returns immediately or 2) returns after a specific interval or 3) returns after x-number of bytes have been received. In this context, each of those 3 choices is incorrect. Also, hang big reads - let the port give you as much as it has in one gulp. I usually retrieve the read & write buffer size of the hardware, adjust it if it has ridiculous values, then use that as the length of my reads. (I ignore the buffer size for writes because any bytes accepted for writing will be written, barring catastrophe, or I will be notified of such by the actual write length returned when the I/O completes).