Thread Limits . .

On a computer with only (1 CPU), how many threads can I run at the same time in the python process ?

As many as you like! There are practical limits, but no hard limits.

How many can you practically use? it depends. For example, if your threads are all doing network calls (fetching things from remote servers, responding to web app requests, etc), you can probably have 100-1000 before it becomes a problem. Does your CPU have multiple cores? (Most physical CPUs do, these days, but if you’re renting a virtual computer, you get whatever you pay for.) On a single-core CPU, you’re only ever going to have one thread running at a time, but on a multi-core CPU, it’s possible to run more than one thread simultaneously.

The best way to find out is to try it. Start making more and more threads. Python is great for experimentation!

Thousands. They’ll just run very slowly.

Why do you want to use threads? If you are doing it for performance reasons, you may be disappointed unless the threads are doing lots of file system I/O.

For the CPython implementation, the global interpreter lock (GIL) restricts executing Python bytecode to one thread per interpreter at any given time, regardless of the number of CPUs or cores per CPU. Often it’s not an issue because the workload executing on the threads is I/O bound, and thus a thread spends much of the time waiting for data to be ready, sent, or received.

That said, many computationally intensive problems benefit from parallel processing. By using an extension module that executes native code instead of interpreted bytecode, multiple threads can execute simultaneously. Packages such as numpy and scipy leverage native numerical libraries for this. Alternatively, with the multiprocessing module, computationally bound work can be divided up amongst multiple interpreters, each running in its own process.

Together with what has already been said, maybe have a look at this.

I’m not being condescending, btw, with reference to the ‘Beginners’ part of that link; it’s just that the read is very good (IMHO) explainer.

This is true at a basic level, but as always, it’s never quite that simple. There are a number of CPU-bound tasks which can actually be parallelized over multiple Python threads; the only requirement is that the heavy lifting is done by a C (or Fortran) extension that first releases the GIL. Conversely, even with I/O-bound tasks, there will usually be a limit on the number of threads that you can usefully run, due to the overhead of context switching - though it’s far FAR greater than your CPU core count. So when you truly need epic numbers of connections (say, a high-volume web app), you’ll need to go for some sort of asynchronous I/O event loop (see eg the asyncio module), and possibly even multiple such event loops in order to take advantage of your CPU cores.

Here’s a simple and rather stupid example of a program that can happily use all your CPU cores with a single process:

import bcrypt
import threading

# Tune these to your CPU's capabilities
COST = 10

pin = input("Enter a four-digit PIN: ")
password = bcrypt.hashpw(pin.encode(), bcrypt.gensalt(COST))
pin = None # Pretend we can't see this one

# Okay, now to brute-force that thing!
tasks = iter(range(10000))
found = None
def search():
	global found
	for task in tasks:
		if bcrypt.checkpw(b"%04d" % task, password):
			found = task
		if found is not None:

threads = [threading.Thread(target=search) for _ in range(THREADS)]
for t in threads: t.start()
for t in threads: t.join()
if found is None:
	print("I guess that wasn't a four-digit PIN.")
	print("The PIN you entered was:", found)

(It’s worth noting that a bcrypt cost factor of 10 rounds is pretty poor by today’s standards, but I didn’t feel like cooking my CPU for hours just for a quick demo.)

Even with the GIL, this is entirely capable of saturating a full set of cores, simply because most of the work isn’t happening in Python. This is a more general case of “I/O bound works well with threads”, since I/O also isn’t happening in Python.

And yes, this is CPython (specifically version 3.12, although probably any version will work).

I said as much already.

1 Like

True but a lot of people won’t understand the subtleties of “Python bytecode” in your explanation. There is an important distinction hidden in that phrase which needed to be highlighted IMO.