Chapter 12. Non-Blocking I/O
Compared to CPUs and memory or even disks,
networks are slow. A high-end modern PC is capable of moving data
between the CPU and main memory at speeds of around six gigabytes per
second. It can move data to and from disk at the much slower but
still respectable speed of about 150 megabytes per second.[1] By contrast, the theoretical maximum on
today's fastest local area networks tops out at 120
megabytes per second, though most LANs only support speeds ten to a
hundred times slower than that. And the speed across the public Internet is
generally at least an order of magnitude smaller than what you see
across a LAN. My faster than average SDSL line promises 96 kilobytes
per second, but normally delivers only about two-thirds of that. And
as I type this, my router has died and I've been
reduced to a dialup connection whose bandwidth is less than six
kilobytes per second. CPUs, disks, and networks are all speeding up
over time. These numbers are all substantially higher than I could
have reported in the first couple of editions of this book.
Nonetheless, CPUs and disks are likely to remain several orders of
magnitude faster than networks for the foreseeable future. The last
thing you want to do in these circumstances is make the blazingly
fast CPU wait for the (relatively) molasses-slow
network.[1] These are rough, theoretical maximum numbers. Nonetheless,
it's worth pointing out that I'm
using megabyte to mean 1,024*1,024 bytes and gigabyte to mean 1,024
megabytes. Manufacturers often round the size of a gigabyte down to
1,000 megabytes and the size of a megabyte down to 1,000,000 bytes to
make their numbers sound more impressive. Furthermore, networking
speeds are often referred to in kilo/mega/giga
bits per second rather than bytes per second.
Here I'm reporting all numbers in bytes so I can
compare hard drive, memory, and network bandwidths.
The traditional Java solution for
allowing the CPU to race ahead of the
network is a combination of
buffering and
multithreading. Multiple
threads can generate data for several different connections at once
and store that data in buffers until the network is actually ready to
send it; this approach works well for fairly simple servers and
clients without extreme performance needs. However, the overhead of
spawning multiple threads and switching between them is nontrivial.
For instance, each thread requires about one extra megabyte of RAM.
On a large server that may be processing thousands of requests a
second, it's better not to assign a thread to each
connection, even if threads for subsequent requests can be reused, as
discussed in Chapter 5. The overhead of thread
management severely degrades system performance.
It's faster if one thread can take responsibility
for multiple connections, pick one that's ready to
receive data, fill it with as much data as that connection can manage
as quickly as possible, then move on to the next ready connection.To really work well, this approach needs to be supported by the
underlying operating system. Fortunately, pretty much every modern
operating system you're likely to be using as a
high-volume server supports such non-blocking I/O. However, it might
not be well-supported on some client systems of interest, such as
PDAs, cell phones, and the like (i.e., J2ME environments). Indeed,
the java.nio package that provides this support is
not part of any current or planned J2ME profiles. However, the whole
new I/O API is designed for and only really matters on servers, which
is why I haven't done more than allude to it until
we began talking about servers. Client and even peer-to-peer systems
rarely need to process so many simultaneous connections that
multithreaded, stream-based I/O becomes a noticeable bottleneck.
There are some exceptionsa web spider such as Google that
downloads millions of pages simultaneously could certainly use the
performance boost the new I/O APIs providebut for most clients
the new I/O API is overkill, and not worth the extra complexity it
entails.