Python
concurrency
threading
async
programming

What is better Select vs Threads?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

select and threads are not two versions of the same tool. They solve different concurrency problems, so the better choice depends on whether your program is mostly waiting on I/O, how many concurrent activities you need, and how much coordination complexity you want to manage.

What select Is Actually Good At

select lets one thread wait on many file descriptors and react only when one becomes readable or writable. That makes it a strong fit for network servers, socket relays, and other I/O-heavy programs where most tasks spend their time waiting.

In modern Python, the selectors module is usually more convenient than calling select.select(...) directly:

python
1import selectors
2import socket
3
4selector = selectors.DefaultSelector()
5server = socket.socket()
6server.bind(("127.0.0.1", 9000))
7server.listen()
8server.setblocking(False)
9selector.register(server, selectors.EVENT_READ)
10
11while True:
12    for key, _ in selector.select():
13        if key.fileobj is server:
14            conn, _ = server.accept()
15            conn.setblocking(False)
16            selector.register(conn, selectors.EVENT_READ)
17        else:
18            data = key.fileobj.recv(1024)
19            if data:
20                key.fileobj.sendall(data)
21            else:
22                selector.unregister(key.fileobj)
23                key.fileobj.close()

One control loop can handle many connections without creating one thread per client.

What Threads Are Better At

Threads are often easier to understand when each task can behave like an independent worker. They are especially practical when you rely on blocking libraries or need a small number of background jobs.

python
1import threading
2import time
3
4def worker(name):
5    print(f"{name} starting")
6    time.sleep(2)
7    print(f"{name} finished")
8
9threads = [threading.Thread(target=worker, args=(f"job-{i}",)) for i in range(3)]
10
11for thread in threads:
12    thread.start()
13
14for thread in threads:
15    thread.join()

This model is straightforward because each thread can block, sleep, and keep local state without participating in one central event loop.

Match The Model To The Workload

If the program is mostly waiting on many sockets or pipes, select-style concurrency usually scales better than creating large numbers of mostly idle threads. Memory overhead stays lower, and you avoid constant context switching between sleeping workers.

If the program depends on blocking APIs, older libraries, or a one-worker-one-job mental model, threads are often more practical. They are especially useful when the number of concurrent tasks is modest and the code benefits from simpler control flow.

The comparison changes again for CPU-bound work. In CPython, threads do not provide strong parallel speedups for CPU-heavy Python code because of the GIL. In that case, the better comparison is often threads versus multiprocessing, not select versus threads.

Complexity Matters As Much As Raw Performance

select centralizes I/O handling, which can scale well, but stateful workflows become harder to structure if everything must be modeled as callbacks or event-loop state. Threaded code can feel more natural, but shared mutable state introduces locks, races, and shutdown problems.

That tradeoff matters in real codebases. A slightly less efficient model that your team can debug confidently is often the better engineering choice.

Many systems also mix approaches. A program may use an event loop for socket readiness and a small thread pool for blocking file operations or library calls that do not fit non-blocking I/O cleanly.

In Modern Python, Also Consider Higher-Level APIs

In practice, most new Python code does not choose directly between raw select and raw threads. It often chooses between higher-level tools built on the same ideas:

  • 'asyncio or selectors for event-driven I/O'
  • 'threading or concurrent.futures.ThreadPoolExecutor for blocking tasks'
  • 'multiprocessing or ProcessPoolExecutor for CPU-heavy work'

That framing is more useful than asking which primitive is "better" in the abstract.

Common Pitfalls

  • Asking the question without separating I/O-bound work from CPU-bound work.
  • Using one thread per socket when thousands of mostly idle connections are expected.
  • Forcing everything into one event loop even when important libraries are blocking.
  • Ignoring synchronization costs when several threads mutate shared state.
  • Comparing raw select with threads while overlooking higher-level tools such as asyncio and executors.

Summary

  • 'select is strong for handling many concurrent I/O sources in one loop.'
  • Threads are strong when tasks are independent and blocking code is easier to work with.
  • For CPU-bound Python code, processes are often the real alternative.
  • Pick the model that matches the workload, library constraints, and complexity your team can support.

Course illustration
Course illustration

All Rights Reserved.