Re: Thread implementations, poll, etc.

Mike (ford@omnicron.com)
Tue, 23 Jun 98 22:49:03 -0700


I just got caught up on 3 days worth of interesting discussion; here are
some points that I didn't see made. Everything I say here about poll()
applies equally to select(), of course.

Someone said that if regular files supported poll(), "tail -f" could use
poll() instead of sleep(). This is not true; it is a common
misconception about what poll actually does. A poll for reading doesn't
mean "wake me when there are data which I can read". It means "wake me
when a read() would not block interruptibly. A read() on a regular file
will never block interruptibly (at least by tradition, interruptible NFS
mounts notwithstanding). Regular files already support poll() fully and
correctly.

Someone said that poll on regular files is the same as async I/O. It
isn't. poll isn't I/O at all. Operations under any async I/O paradigm
actually perform I/O and notify asynchronously of completion. poll
never performs I/O, it only gives the caller stale information about
whether certain hypothetical I/O operations would block. This is one of
the flaws in select, poll, ioctl(FIONREAD) and similar kludges. Between
the time that the call returns and the time you try to do something
about it, things can change. If you are correctly using non-blocking
operations, this is probably only a performance problem (you would just
loop back into another poll), but if you assume that the information
from poll is more than a hint, you can reach deadlock or some other
failure.

True async I/O requires that the actual I/O be asynchronous, not just a
mechanism for waiting until it is safe to do synchronous I/O.

Someone claimed that poll() could benefit from a wake-one implementation
or option. This doesn't make sense for two reasons. First, it just
doesn't make sense to have more than one context polling for the same
readiness condition, for the reasons above. Second, after the chosen
thread is awoken, what should happen to the other polling threads? They
are still in a poll() system call, one of the file descriptors they are
polling is ready, and there is no real guarantee that the awoken thread
will do anything with the ready fd any time soon. It may have other
ready fds or other internal processing to do first, or just be hosed in
general. The situation is one where the semantics of poll() say that
the other threads should wake up, but you "don't want" them to, but
there is no reasonable condition defined for when this special exception
period should end.

poll() is a substitute for multithreading and just wasn't meant to work
well in conjunction with multithreading. Because of the nature of
poll() it is most robust to multiplex your polling of any one condition
through one thread and pass off the events to other threads, even though
this has unfortunate performance costs.

Someone wondered what it would mean to be polling a file descriptor and
have that file descriptor become closed (either by closing it in another
thread, or by using some new asynchronous form of polling). The proper
semantics of poll() say that the poll should immediately report the
closed file descriptor as "ready" because a read() would not block, it
would immediately return (with EBADF).

Some people proposed various forms of asynchronous I/O or asynchronous
polling. Asynchronous I/O is not a bad idea, but it is a very different
paradigm from that of Unix. I find the idea of asynchronous polling,
however, absolutely hideous. It just adds a messy, complex interface on
top of a flawed incomplete alternative to true threads. An asynchronous
programming model would be much better based on true async I/O, with
completion messages indicating that the I/O is complete.

But all currently implemented or proposed asynchronous I/O schemes (of
course there probably some of which I am not aware) are still incomplete
alternatives to threads. There are many operations for which one needs
to wait that are not strictly I/O operations, or at least do not use the
read/write/file descriptor model. Examples: wait(), connect(),
pause(). Threads are really the only way to get everything right.

The idea of implementing async I/O on top of threads seems completely
backward to me. If I were designing and implementing a OS from scratch,
I would have the kernel provide only asynchronous I/O, and no threads.
Every single system call would be asynchronous in structure, although
most operations would no doubt be marked as already completed when the
trap returned. One version of libc would wrap these asynchronous calls
inside synchronous wrappers, doing an explicit wait for completion after
every call, providing a traditional Unix-like model. Another version of
libc would implement threads, doing a wait for completion only when all
threads were sleeping. Another version of libc could make the
asynchronous interfaces themselves available.

Note that the above hypothetical system fails to give any parallelism
benefit to a single running process on a MP system. This is fine by me.
Threads are a programming paradigm, not a perfomance tool. If you want
parallelism, use fork().

I'm sure at least that last statement will get some disagreement. :)

-=] Ford [=-

"God shuffled his feet (In Real Life: Mike Ditto)
and glanced around at them. ford@omnicron.com
The people cleared their throats http://www.omnicron.com/~ford/ford.html
and stared right back at him." - Crash Test Dummies

-
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@vger.rutgers.edu