Chapter 7 ■ Server arChiteCture
122
Finally, the single-threaded design makes poor use of the server CPU and system resources because it cannot
take other actions while waiting for the client to send the next request. You can time how long each line of the
single-threaded server takes by running it under the control of the trace module from the Standard Library.
To limit the output to only the server code itself, tell the tracer to ignore Standard Library modules (on my system,
Python 3.4 is installed beneath the /usr directory).
$ python3.4 -m trace -tg --ignore-dir=/usr srv_single.py ''
Each line of output gives the moment, counted in seconds from when the server is launched, at which a line
of Python code starts executing. You will see that most lines start executing as soon as the previous line is finished,
falling either in the same hundredth of a second or in the next hundredth. But every time the server needs to wait on
the client, execution stops and has to wait. Here is a sample run:
3.02 zen_utils.py(40): print('Accepted connection...'...)
3.02 zen_utils.py(41): handle_conversation(sock, address)
⋮
3.02 zen_utils.py(57): aphorism = recv_until(sock, b'?')
3.03 zen_utils.py(63): message = sock.recv(4096)
3.03 zen_utils.py(64): if not message:
3.03 zen_utils.py(66): while not message.endswith(suffix):
⋮
3.03 zen_utils.py(57): aphorism = recv_until(sock, b'?')
3.03 zen_utils.py(63): message = sock.recv(4096)
3.08 zen_utils.py(64): if not message:
3.08 zen_utils.py(66): while not message.endswith(suffix):
⋮
3.08 zen_utils.py(57): aphorism = recv_until(sock, b'?')
3.08 zen_utils.py(63): message = sock.recv(4096)
3.12 zen_utils.py(64): if not message:
3.12 zen_utils.py(66): while not message.endswith(suffix):
⋮
3.12 zen_utils.py(57): aphorism = recv_until(sock, b'?')
3.12 zen_utils.py(63): message = sock.recv(4096)
3.16 zen_utils.py(64): if not message:
3.16 zen_utils.py(65): raise EOFError('socket closed')
⋮
3.16 zen_utils.py(48): except EOFError:
3.16 zen_utils.py(49): print('Client socket...has closed'...)
3.16 zen_utils.py(53): sock.close()
3.16 zen_utils.py(39): sock, address = listener.accept()
This is an entire conversation—three requests and responses—with the client.py program. During a total of
0.14 seconds of processing time between the first and last lines of this trace, it has to wait on the client three different
times for a total of around 0.05 + 0.04 + 0.04 = 0.13 seconds spent idle! This means that the CPU is only about
0.01 / 0.14 = 7 percent occupied during this exchange. This is, of course, only a rough number. The fact that we are
running under trace slows the server down and increases its CPU usage, and the resolution of these numbers is
approximate in the first place. But it is a result you will find confirmed if you use more sophisticated tools. Single-threaded
servers, unless they are doing a large amount of in-CPU work during each request, are measurably poor at using the
server machine to its full potential. The CPU is sitting idle while other clients are waiting in line to be served.