Concepts / Using Threads for Computational Speedup

Using Threads for Computational Speedup

Network input and output is blocking I/O, because those operations can block for indefinite periods of time, so network programs must be prepared for waiting.

  • Programming

A Server Full of Waiting Threads

A common rule of thumb says that a program should never run more threads than the computer has processors. For many programs this is a sensible starting point. But consider a server that runs on a machine with a small number of processors, where each of its threads spends most of its time waiting for network input and output to finish, blocked around 90 percent of the time. Would such a server want more threads than processors, fewer, or the same number?

Why Network Waiting Is Called Blocking I/O

Definition

Network input and output is called blocking I/O because those operations can block for indefinite periods of time. A thread that asks the network for data may have to stop and wait an unpredictable amount of time before the data arrives.

The word blocking describes what the thread is doing, not what the network is doing. While the operation is pending, the thread cannot continue past that point in its own code. Because the length of the wait is unpredictable, a network program cannot assume that a read will finish quickly. The source makes this point directly: programs that use the network must be prepared to deal with this blocking. Waiting is not an unusual event in network code. It is part of how that code normally behaves.

Blocking input and output also appears outside networks. Reading input that a user types at a command line program is another form of blocking input and output. The difference that matters for network programs is not whether they wait, but what else the program could be doing during the wait.

How a Thread Moves Through Blocked and Running States

To see what blocking does to a thread, follow one thread through a single network read. The thread starts out running, executing its own code. When it reaches a network read whose data has not arrived yet, it becomes blocked. While it is blocked it is waiting, and it does not compete for the processor. When the data arrives, the thread becomes runnable again, meaning it is ready to continue. The operating system then gives it processor time, and it returns to running.

Runningexecuting its own codeNetwork read issueddata not yet arrivedBlockedwaiting, no processor useData arriveswait endsRunnableready for processor timenextwaitseventresumes
What happens to this one thread while a network read waits for data, and when does it resume?

The timeline shows one thread. The key observation is the gap between issuing the read and the data arriving. During that gap the thread is blocked, and the length of the gap is not something the program can predict in advance. The source describes this as blocking for indefinite periods of time, so the length of the blocked stretch varies from read to read.

Runningusing the processorBlockedwaiting on networkRunnablewaiting for processor timeblocking network calldata arrivesprocessor assigned
Which state does a thread enter when it makes a blocking network call, and which event moves it back to runnable?

The state diagram names the same movement as a set of transitions. A thread enters the blocked state when it makes a blocking network call. The event that moves it out of blocked is the arrival of the data it was waiting for. A blocked thread does not compete for processing time, which is the property the rest of this article relies on.

When Waiting Is Acceptable and When It Is Not

Blocking is not always a problem. In some programs it is acceptable to stop all other processing and wait for input. A program with nothing else to do can afford to wait. A program with other work available benefits from letting some parts keep running while other parts are blocked. Threads make that possible: they let a program keep doing useful work in some parts while other parts are blocked.

The source contrasts two network programs. A client that sends requests to a server might be fine with a single thread, if it has nothing else to do while it waits for responses. A server is different. A server can usually be connected to several clients at the same time, and while it waits for data from one client, it can communicate with the others. When the server gives each client connection its own thread, a block on one connection does not stop the server from talking to the rest.

In the real world

Think of a server handling three client connections. Client A's connection is blocked waiting for data, while clients B and C have data ready. If each connection has its own thread, the threads for B and C keep running and the server keeps serving them. Only A's thread is stuck. If there were one thread for all three connections, A's wait would hold up everyone.

Two Different Goals for Threads

The processor rule breaks down once you separate two goals for threads. The first goal is computational speedup: using threads to make calculations finish sooner. The second goal is handling blocking input and output. The two goals ask the same question, how many threads can actually be doing work at the same time, but they get different answers, because a blocked thread does not count as doing work.

About one thread perprocessornatural targetBlocked threads skipthe processorthey do not competeExtra threads addoverheadno speedup on one processorMany more threads canbe usefultied to time spent waiting
How does a thread waiting on the network differ from a thread doing CPU-bound computation, in terms of the resource it uses?

For computational speedup, the natural choice is one thread per available processor. If only one processor exists, adding more threads gives no speedup at all, and it slows the program because of the overhead of creating and managing the threads. Speedup is limited by how many threads can make real progress at the same time, and computing threads all want the processor at once.

For blocking input and output the situation is different. Many threads are waiting at any given moment, and a waiting thread does not compete for the processor. Only the active, unblocked threads compete for processing time. A program can therefore have many more threads than processors without overloading them, because most of those threads are not running at a given moment. The useful thread count for a blocking program is tied to how much of the time the threads spend waiting, not only to the number of processors.

Estimating a Useful Thread Count

The source gives a rough target for blocking I/O: about one active thread per processor. If the threads are mostly blocked, that target can mean many more threads in total. The rule of thumb is a starting point for estimation, not an exact formula. The calculation depends on how much blocking actually happens, which means you need to measure the blocked fraction for the program in question.

Worked example

Threads for a server blocked 90 percent of the time

A server runs on a machine with 4 processors. Each thread is blocked about 90 percent of the time. Roughly how many threads can the server make use of to keep the processors busy?

  1. 1
    Find the active fraction

    If a thread is blocked 90 percent of the time, it is active about 10 percent of the time (100 percent minus 90 percent).

  2. 2
    Find the active threads needed per processor

    The target is about one active thread per processor. Each processor needs one thread active at any moment, so the total number of threads must be large enough that about one is active per processor.

  3. 3
    Scale by the blocked fraction

    Since each thread is active only about 10 percent of the time, about 10 threads are needed to supply one active thread on average. That is about 10 threads per processor.

  4. 4
    Multiply by the processor count

    With 4 processors, 4 multiplied by 10 gives about 40 threads.

Result

About 40 threads, roughly ten per processor. This is a rough estimate for these assumed numbers, not a universal answer.

Mistakes With the Processor Rule

  • Common mistake

    Applying the one-thread-per-processor rule to every program

    What it looks like

    A network server caps itself at the processor count, even though its threads spend most of their time waiting on the network.

    Why it fails

    The rule comes from threads that compute and want to run at full speed. It ignores what the threads are actually doing.

    Do this instead

    Ask what the threads spend their time doing. Compute-bound threads follow the processor rule; mostly blocked threads can justify many more.

  • Common mistake

    Expecting extra threads to speed up computation

    What it looks like

    Adding threads to a CPU-bound calculation on a single processor and expecting it to finish sooner.

    Why it fails

    Computing threads all want the processor. On one processor, extra threads give no speedup and add overhead from creating and managing them.

    Do this instead

    For computational speedup, aim for roughly one thread per available processor.

  • Common mistake

    Assuming blocked threads still use the processor

    Why it fails

    A blocked thread is waiting, so it does not compete for processing time. Only active, unblocked threads compete.

  • Common mistake

    Treating a fixed number like 40 as a universal answer for every server

Trace the Server's Thread Count

MEDIUM

Return to the opening server, blocked about 90 percent of the time. Suppose it runs on 2 processors. Using the one-active-thread-per-processor target, estimate how many threads the server can make use of, and explain why this is more than 2.

Hints
  • Start from the active fraction: a thread blocked 90 percent of the time is active about 10 percent of the time.
  • Work out how many threads are needed so that about one is active per processor, then multiply by the 2 processors.

Key Takeaways

  • Network input and output is called blocking I/O because those operations can block for indefinite periods of time, so network programs must be prepared for waiting.
  • A thread that blocks leaves the processor free: a blocked thread does not compete for processing time, while a runnable thread waits for the processor.
  • Threads for computational speedup should roughly match the number of processors, since extra threads add overhead without speedup on one processor.
  • Threads for blocking I/O can outnumber processors, and a rough target of about one active thread per processor can mean many more threads in total when most threads are blocked.
  • The useful thread count depends on the measured blocked fraction, so it must be estimated for each program rather than fixed by a rule.