45

The setup

I have written a pretty complex piece of software in Python (on a Windows PC). My software starts basically two Python interpreter shells. The first shell starts up (I suppose) when you double click the main.py file. Within that shell, other threads are started in the following way:

    # Start TCP_thread
    TCP_thread = threading.Thread(name = 'TCP_loop', target = TCP_loop, args = (TCPsock,))
    TCP_thread.start()

    # Start UDP_thread
    UDP_thread = threading.Thread(name = 'UDP_loop', target = UDP_loop, args = (UDPsock,))
    TCP_thread.start()

The Main_thread starts a TCP_thread and a UDP_thread. Although these are separate threads, they all run within one single Python shell.

The Main_threadalso starts a subprocess. This is done in the following way:

p = subprocess.Popen(['python', mySubprocessPath], shell=True)

From the Python documentation, I understand that this subprocess is running simultaneously (!) in a separate Python interpreter session/shell. The Main_threadin this subprocess is completely dedicated to my GUI. The GUI starts a TCP_thread for all its communications.

I know that things get a bit complicated. Therefore I have summarized the whole setup in this figure:

enter image description here


I have several questions concerning this setup. I will list them down here:

Question 1 [Solved]

Is it true that a Python interpreter uses only one CPU core at a time to run all the threads? In other words, will the Python interpreter session 1 (from the figure) run all 3 threads (Main_thread, TCP_thread and UDP_thread) on one CPU core?

Answer: yes, this is true. The GIL (Global Interpreter Lock) ensures that all threads run on one CPU core at a time.

Question 2 [Not yet solved]

Do I have a way to track which CPU core it is?

Question 3 [Partly solved]

For this question we forget about threads, but we focus on the subprocess mechanism in Python. Starting a new subprocess implies starting up a new Python interpreter instance. Is this correct?

Answer: Yes this is correct. At first there was some confusion about whether the following code would create a new Python interpreter instance:

    p = subprocess.Popen(['python', mySubprocessPath], shell = True)

The issue has been clarified. This code indeed starts a new Python interpreter instance.

Will Python be smart enough to make that separate Python interpreter instance run on a different CPU core? Is there a way to track which one, perhaps with some sporadic print statements as well?

Question 4 [New question]

The community discussion raised a new question. There are apparently two approaches when spawning a new process (within a new Python interpreter instance):

    # Approach 1(a)
    p = subprocess.Popen(['python', mySubprocessPath], shell = True)

    # Approach 1(b) (J.F. Sebastian)
    p = subprocess.Popen([sys.executable, mySubprocessPath])

    # Approach 2
    p = multiprocessing.Process(target=foo, args=(q,))

The second approach has the obvious downside that it targets just a function - whereas I need to open up a new Python script. Anyway, are both approaches similar in what they achieve?

K.Mulier
  • 8,069
  • 15
  • 79
  • 141
  • https://docs.python.org/2/library/multiprocessing.html – mootmoot Apr 22 '16 at 13:29
  • 3
    I think you should question why you care on which physical cores your threads run. The OS will usually move threads around between the available CPUs in the system depending on various factors. Is there any particular reason why you want to monitor and/or interfere with this process? – Dolda2000 Apr 22 '16 at 14:14
  • Good question :-). Yes, I believe I do have a valid reason. I'm building a data acquisition system in Python, that reads my microcontroller data (like Analog inputs, ...) and shows live graphs in my GUI. As long as the incoming data is limited, nobody cares about multiprocessing. But once it gets really fast, I want to be in control. Maybe I can make certain low-latency parts of my Python software run on a dedicated CPU core that I do not use for anything else, hence ensuring high responsiveness. – K.Mulier Apr 22 '16 at 14:21
  • @K.Mulier: As long as you have fewer runnable threads than CPUs in your system, the OS will always let all of them run concurrently. Also, if you get so fast as to have to care about allocating threads to fix cores, you are far beyond the point where you can write the program in Python. – Dolda2000 Apr 22 '16 at 14:24
  • I think that your statement _'the OS will always let all of them run concurrently_' does not hold for Python. Python has a GIL (Global Interpreter Lock) to make sure that your software is not executed multicore. If you want to execute it on multiple CPU cores, you need to bypass this GIL. I'm only a beginner in Python, so feel free to correct me where I'm wrong. You are right that Python is not the best choice for CPU intensive tasks. The reasons that I chose Python have more to do with sharing the software with my colleagues who know Python. Anyway, that's beyond the scope of the question :-) – K.Mulier Apr 22 '16 at 14:28
  • It does hold insofar as I wrote "runnable" threads. Thanks to the GIL, only one thread per Python process (which attempts to run Python bytecode) will be runnable at one time, the others blocking on the GIL. – Dolda2000 Apr 22 '16 at 14:30
  • 1
    Also, it's not that I question your choice of language. I'm just saying that, as long as you're running Python code, then worrying about which specific, physical cores your threads are running on is beyond the scope of your performance issues. The only kinds of issues that CPU-pinning and the like can become relevant for is pretty fine-grained stuff like avoiding spurious cache flushes and whatnot, all of which is far beyond what will make a difference as long as you're running Python code. – Dolda2000 Apr 22 '16 at 14:33
  • Thank you Mr. Dolda. So, I do need multiple instances of the Python interpreter to make full use of the multicore CPU. – K.Mulier Apr 22 '16 at 14:36
  • Yes, you do. Alternatively, as danidee alluded to, you could also choose a GIL-less Python implementation, such as Jython. – Dolda2000 Apr 22 '16 at 14:36
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/109926/discussion-between-k-mulier-and-dolda2000). – K.Mulier Apr 22 '16 at 14:37
  • How come you need the two interpreter shells? Why not have a gui thread for the gui and a UDP thread to the micro controller? – Noelkd Apr 22 '16 at 15:10
  • unrelated: 1- use `Popen([sys.executable, mySubprocessPath])` instead of `Popen(['python', mySubprocessPath], shell=True)` 2- It doesn't look like you need neither `subprocess` nor `multiprocessing` here. You can run GUI, I/O threads in a single process. 3- If you have a series of CPU intensive tasks then either use C extensions such as `lxml`, `regex`, `numpy` (or your own one created using Cython) that can release GIL during long computations or off-load them into separate processes (a simple way is to use a process pool such as provided by `concurrent.futures`). – jfs Apr 22 '16 at 16:32
  • your function `foo` in your last question can return `subprocess.call(cmd, shell=False)`...so you can still start a new interpreter and run a script – danidee Apr 22 '16 at 17:50

3 Answers3

33

Q: Is it true that a Python interpreter uses only one CPU core at a time to run all the threads?

No. GIL and CPU affinity are unrelated concepts. GIL can be released during blocking I/O operations, long CPU intensive computations inside a C extension anyway.

If a thread is blocked on GIL; it is probably not on any CPU core and therefore it is fair to say that pure Python multithreading code may use only one CPU core at a time on CPython implementation.

Q: In other words, will the Python interpreter session 1 (from the figure) run all 3 threads (Main_thread, TCP_thread and UDP_thread) on one CPU core?

I don't think CPython manages CPU affinity implicitly. It is likely relies on OS scheduler to choose where to run a thread. Python threads are implemented on top of real OS threads.

Q: Or is the Python interpreter able to spread them over multiple cores?

To find out the number of usable CPUs:

>>> import os
>>> len(os.sched_getaffinity(0))
16

Again, whether or not threads are scheduled on different CPUs does not depend on Python interpreter.

Q: Suppose that the answer to Question 1 is 'multiple cores', do I have a way to track on which core each thread is running, perhaps with some sporadic print statements? If the answer to Question 1 is 'only one core', do I have a way to track which one it is?

I imagine, a specific CPU may change from one time-slot to another. You could look at something like /proc/<pid>/task/<tid>/status on old Linux kernels. On my machine, task_cpu can be read from /proc/<pid>/stat or /proc/<pid>/task/<tid>/stat:

>>> open("/proc/{pid}/stat".format(pid=os.getpid()), 'rb').read().split()[-14]
'4'

For a current portable solution, see whether psutil exposes such info.

You could restrict the current process to a set of CPUs:

os.sched_setaffinity(0, {0}) # current process on 0-th core

Q: For this question we forget about threads, but we focus on the subprocess mechanism in Python. Starting a new subprocess implies starting up a new Python interpreter session/shell. Is this correct?

Yes. subprocess module creates new OS processes. If you run python executable then it starts a new Python interpeter. If you run a bash script then no new Python interpreter is created i.e., running bash executable does not start a new Python interpreter/session/etc.

Q: Supposing that it is correct, will Python be smart enough to make that separate interpreter session run on a different CPU core? Is there a way to track this, perhaps with some sporadic print statements as well?

See above (i.e., OS decides where to run your thread and there could be OS API that exposes where the thread is run).

multiprocessing.Process(target=foo, args=(q,)).start()

multiprocessing.Process also creates a new OS process (that runs a new Python interpreter).

In reality, my subprocess is another file. So this example won't work for me.

Python uses modules to organize the code. If your code is in another_file.py then import another_file in your main module and pass another_file.foo to multiprocessing.Process.

Nevertheless, how would you compare it to p = subprocess.Popen(..)? Does it matter if I start the new process (or should I say 'python interpreter instance') with subprocess.Popen(..)versus multiprocessing.Process(..)?

multiprocessing.Process() is likely implemented on top of subprocess.Popen(). multiprocessing provides API that is similar to threading API and it abstracts away details of communication between python processes (how Python objects are serialized to be sent between processes).

If there are no CPU intensive tasks then you could run your GUI and I/O threads in a single process. If you have a series of CPU intensive tasks then to utilize multiple CPUs at once, either use multiple threads with C extensions such as lxml, regex, numpy (or your own one created using Cython) that can release GIL during long computations or offload them into separate processes (a simple way is to use a process pool such as provided by concurrent.futures).

Q: The community discussion raised a new question. There are apparently two approaches when spawning a new process (within a new Python interpreter instance):

# Approach 1(a)
p = subprocess.Popen(['python', mySubprocessPath], shell = True)

# Approach 1(b) (J.F. Sebastian)
p = subprocess.Popen([sys.executable, mySubprocessPath])

# Approach 2
p = multiprocessing.Process(target=foo, args=(q,))

"Approach 1(a)" is wrong on POSIX (though it may work on Windows). For portability, use "Approach 1(b)" unless you know you need cmd.exe (pass a string in this case, to make sure that the correct command-line escaping is used).

The second approach has the obvious downside that it targets just a function - whereas I need to open up a new Python script. Anyway, are both approaches similar in what they achieve?

subprocess creates new processes, any processes e.g., you could run a bash script. multprocessing is used to run Python code in another process. It is more flexible to import a Python module and run its function than to run it as a script. See Call python script with input with in a python script using subprocess.

Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • In you first question/answer do you imply that in presence of blocking IO operations multiple cores could be involved by pure Python multithreaded (but not multiprocess) application? – Serge Nov 07 '18 at 10:28
  • @Serge What are you referring to? Could you provide a direct quote. – jfs Nov 07 '18 at 14:42
  • No. GIL and CPU affinity are unrelated concepts. GIL can be released during blocking I/O operations, long CPU intensive computations inside a C extension anyway. If a thread is blocked on GIL; it is probably not on any CPU core and therefore it is fair to say that pure Python multithreading code may use only one CPU core at a time on CPython implementation. – Serge Nov 07 '18 at 15:12
  • @Serge I don't see how it relates to your question. Are you asking whether different CPython processes have their own GILs? (the answer is yes). Obviously, different Python processes may run on different CPUs at the same time (even on different hosts). – jfs Nov 07 '18 at 15:21
  • Nope, according to docs, The lock is also released around potentially blocking I/O operations like reading or writing a file, so that other Python threads can run in the meantime. Does it mean it released and then reaquired in order to do I/O, or reading of file contend might happen in parallel to another thread, using another core. – Serge Nov 07 '18 at 15:23
  • I am curious of number of cores one pure python app can use with multi threading, but not multi processing. – Serge Nov 07 '18 at 15:24
  • Docs actually say in the same page that only thread with GIL can execute, yet some coders claims they have seen multi thread app use more than one core. – Serge Nov 07 '18 at 15:26
  • @Serge if you have a separate question, ask it as a separate Stack Overflow question. It is best if you provide a minimal code example that demonstrates the issue in your opinion. And yes, it is easy to demonstrate that more than one core may be used (try any C extension that can release GIL such as lxml) – jfs Nov 07 '18 at 15:29
3

Since you are using the threading module which is build up on thread. As the documentation suggests, it uses the ''POSIX thread implementation'' pthread of your OS.

  1. The threads are managed by the OS instead of Python interpreter. So the answer will depend on the pthread library in your system. However, CPython uses GIL to prevent multiple threads from executing Python bytecodes simutanously. So they will be sequentialized. But still they can be separated to different cores, which depends on your pthread libs.
  2. Simplly use a debugger and attach it to your python.exe. For example the GDB thread command.
  3. Similar to question 1, the new process is managed by your OS and probably running on a different core. Use debugger or any process monitor to see it. For more details, go to the CreatProcess() documentation page.
gdlmx
  • 6,479
  • 1
  • 21
  • 39
1

1, 2: You have three real threads, but in CPython they're limited by GIL , so, assuming they're running pure python, code you'll see CPU usage as if only one core used.

3: As said gdlmx it's up to OS to choose a core to run a thread on, but if you really need control, you can set process or thread affinity using native API via ctypes. Since you are on Windows, it would be like this:

# This will run your subprocess on core#0 only
p = subprocess.Popen(['python', mySubprocessPath], shell = True)
cpu_mask = 1
ctypes.windll.kernel32.SetProcessAffinityMask(p._handle, cpu_mask)

I use here private Popen._handle for simplicty. The clean way would beOpenProcess(p.tid) etc.

And yes, subprocess runs python like everything else in another new process.

robyschek
  • 1,995
  • 14
  • 19
  • (3) works perfectly. Without it, a simple single-threaded busy loop gets thrown around all the cores; with that code, it is permanently on core #0. I have no idea if it's ever useful, but perhaps there are some cache-related reasons to keep a critical thread on a single core. – max May 30 '17 at 00:10