Understanding concurrency fundamentals with psutil
Author
Your Name
Published
November 15, 2025
Learning Objectives
By the end of this lesson, you will be able to:
Understand the difference between processes and threads
Use psutil to monitor system processes
Identify when to use processes vs threads
Understand the basic concepts of concurrency
What is a Process?
A process is an instance of a running program. Each process has its own memory space, file handles, and system resources. Processes are isolated from each other by the operating system.
Let’s explore processes using the psutil library:
import psutilimport osimport time# Get current process informationcurrent_process = psutil.Process()print(f"Process ID (PID): {current_process.pid}")print(f"Process Name: {current_process.name()}")print(f"Parent Process ID: {current_process.ppid()}")print(f"Memory Info: {current_process.memory_info()}")print(f"CPU Usage: {current_process.cpu_percent()}%")
Process ID (PID): 53108
Process Name: python3
Parent Process ID: 53107
Memory Info: pmem(rss=132276224, vms=885882880, shared=35389440, text=31588352, lib=0, data=243585024, dirty=0)
CPU Usage: 0.0%
System-wide Process Information
# Get all running processesprocesses = []for proc in psutil.process_iter(['pid', 'name', 'memory_percent']):try: processes.append(proc.info)except psutil.NoSuchProcess:pass# Sort by memory usage and show top 5top_processes =sorted(processes, key=lambda x: x['memory_percent'], reverse=True)[:5]print("Top 5 processes by memory usage:")for proc in top_processes:print(f"PID: {proc['pid']:<6} Name: {proc['name']:<20} Memory: {proc['memory_percent']:.1f}%")
A thread is a lightweight execution unit within a process. Threads within the same process share memory space and resources, making communication between them easier but also introducing potential issues like race conditions.
import threadingimport timedef worker_function(name, delay):"""A simple worker function that simulates some work"""print(f"Thread {name} starting...") time.sleep(delay)print(f"Thread {name} finished after {delay} seconds")# Get information about the current threadmain_thread = threading.current_thread()print(f"Main thread name: {main_thread.name}")print(f"Main thread ID: {main_thread.ident}")print(f"Active thread count: {threading.active_count()}")
Main thread name: MainThread
Main thread ID: 123313093158720
Active thread count: 8
Creating and Managing Threads
# Create some threadsthreads = []for i inrange(3): thread = threading.Thread( target=worker_function, args=(f"Worker-{i}", i +1), name=f"WorkerThread-{i}" ) threads.append(thread)# Start all threadsfor thread in threads: thread.start()# Wait for all threads to completefor thread in threads: thread.join()print("All threads completed!")
Thread Worker-0 starting...Thread Worker-1 starting...
Thread Worker-2 starting...
Thread Worker-0 finished after 1 seconds
Thread Worker-1 finished after 2 seconds
Thread Worker-2 finished after 3 seconds
All threads completed!
Process vs Thread Comparison
Let’s create a practical example to demonstrate the differences:
import multiprocessingimport threadingimport timeimport osdef cpu_intensive_task(n):"""A CPU-intensive task for demonstration""" result =0for i inrange(n): result += i * ireturn resultdef io_intensive_task(duration):"""An I/O-intensive task (simulated with sleep)""" time.sleep(duration)returnf"Task completed after {duration} seconds"# Function to run with multiprocessingdef process_worker(task_id, n): start_time = time.time() result = cpu_intensive_task(n) end_time = time.time()print(f"Process {task_id} (PID: {os.getpid()}) completed in {end_time - start_time:.2f} seconds")return result# Function to run with threadingdef thread_worker(task_id, duration): start_time = time.time() result = io_intensive_task(duration) end_time = time.time()print(f"Thread {task_id} completed in {end_time - start_time:.2f} seconds")return result# Demonstrate threading for I/O-bound tasksprint("=== Threading Example (I/O-bound) ===")start_time = time.time()threads = []for i inrange(3): thread = threading.Thread(target=thread_worker, args=(i, 1)) threads.append(thread) thread.start()for thread in threads: thread.join()print(f"Total time with threading: {time.time() - start_time:.2f} seconds")
=== Threading Example (I/O-bound) ===
Thread 0 completed in 1.00 seconds
Thread 1 completed in 1.00 seconds
Thread 2 completed in 1.00 seconds
Total time with threading: 1.00 seconds
Memory and Resource Usage
Let’s examine how processes and threads use system resources:
import psutilimport threadingimport multiprocessingimport timedef monitor_resources():"""Monitor system resources""" process = psutil.Process()print(f"Memory usage: {process.memory_info().rss /1024/1024:.2f} MB")print(f"CPU usage: {process.cpu_percent()}%")print(f"Number of threads: {process.num_threads()}")# print(f"Number of file descriptors: {process.num_fds()}")print("---")print("Initial resource usage:")monitor_resources()# Create some threadsdef dummy_thread_work(): time.sleep(2)threads = []for i inrange(5): thread = threading.Thread(target=dummy_thread_work) threads.append(thread) thread.start()print("After creating 5 threads:")monitor_resources()# Clean upfor thread in threads: thread.join()print("After threads completed:")monitor_resources()
Initial resource usage:
Memory usage: 126.27 MB
CPU usage: 0.0%
Number of threads: 14
---
After creating 5 threads:
Memory usage: 126.27 MB
CPU usage: 0.0%
Number of threads: 19
---
After threads completed:
Memory usage: 126.27 MB
CPU usage: 0.0%
Number of threads: 14
---
When to Use Processes vs Threads
Use Processes when:
CPU-bound tasks - Tasks that require intensive computation
Fault isolation - You want to isolate failures
True parallelism - You need to utilize multiple CPU cores
Different programming languages - Communicating between different systems
Use Threads when:
I/O-bound tasks - Tasks that wait for file reads, network requests, etc.
Shared state - You need to share data between concurrent operations
Lightweight concurrency - You need many concurrent operations with low overhead
Responsive interfaces - Keeping a GUI responsive while doing background work
Practical Example: System Monitor
Let’s create a simple system monitor that demonstrates these concepts:
import psutilimport threadingimport timefrom datetime import datetimeclass SystemMonitor:def__init__(self):self.monitoring =Falseself.stats = []def collect_stats(self):"""Collect system statistics"""whileself.monitoring: stats = {'timestamp': datetime.now().strftime('%H:%M:%S'),'cpu_percent': psutil.cpu_percent(interval=1),'memory_percent': psutil.virtual_memory().percent,'disk_usage': psutil.disk_usage('/').percent ifhasattr(psutil, 'disk_usage') else0,'process_count': len(psutil.pids()) }self.stats.append(stats) time.sleep(1)def start_monitoring(self, duration=5):"""Start monitoring for specified duration"""self.monitoring =Trueself.stats = []# Start monitoring thread monitor_thread = threading.Thread(target=self.collect_stats) monitor_thread.daemon =True monitor_thread.start()# Let it run for specified duration time.sleep(duration)self.monitoring =False# Wait for thread to finish monitor_thread.join()returnself.statsdef display_stats(self):"""Display collected statistics"""ifnotself.stats:print("No statistics collected")returnprint("System Statistics:")print("Time | CPU% | Memory% | Processes")print("-"*40)for stat inself.stats:print(f"{stat['timestamp']} | {stat['cpu_percent']:5.1f} | {stat['memory_percent']:7.1f} | {stat['process_count']:9d}")# Use the system monitormonitor = SystemMonitor()print("Starting system monitoring for 5 seconds...")monitor.start_monitoring(5)monitor.display_stats()
Starting system monitoring for 5 seconds...
System Statistics:
Time | CPU% | Memory% | Processes
----------------------------------------
20:17:06 | 5.1 | 17.3 | 98
20:17:08 | 1.0 | 17.3 | 98
20:17:10 | 1.0 | 17.3 | 98
Key Takeaways
Processes are isolated instances of programs with their own memory space
Threads are lightweight execution units within processes that share memory
CPU-bound tasks benefit from multiprocessing
I/O-bound tasks benefit from multithreading
psutil is an excellent library for system monitoring and process management
Exercises
Process Explorer: Create a script that lists all running processes and their memory usage
Thread Pool: Implement a simple thread pool to handle multiple I/O operations
Resource Monitor: Build a real-time system resource monitor using threading
---title: "Processes vs Threads"subtitle: "Understanding concurrency fundamentals with psutil"author: "Your Name"date: todaytoc: trueexecute: echo: true output: true---# Learning ObjectivesBy the end of this lesson, you will be able to:- Understand the difference between processes and threads- Use psutil to monitor system processes- Identify when to use processes vs threads- Understand the basic concepts of concurrency# What is a Process?A **process** is an instance of a running program. Each process has its own memory space, file handles, and system resources. Processes are isolated from each other by the operating system.Let's explore processes using the `psutil` library:```{python}import psutilimport osimport time# Get current process informationcurrent_process = psutil.Process()print(f"Process ID (PID): {current_process.pid}")print(f"Process Name: {current_process.name()}")print(f"Parent Process ID: {current_process.ppid()}")print(f"Memory Info: {current_process.memory_info()}")print(f"CPU Usage: {current_process.cpu_percent()}%")```## System-wide Process Information```{python}# Get all running processesprocesses = []for proc in psutil.process_iter(['pid', 'name', 'memory_percent']):try: processes.append(proc.info)except psutil.NoSuchProcess:pass# Sort by memory usage and show top 5top_processes =sorted(processes, key=lambda x: x['memory_percent'], reverse=True)[:5]print("Top 5 processes by memory usage:")for proc in top_processes:print(f"PID: {proc['pid']:<6} Name: {proc['name']:<20} Memory: {proc['memory_percent']:.1f}%")```# What is a Thread?A **thread** is a lightweight execution unit within a process. Threads within the same process share memory space and resources, making communication between them easier but also introducing potential issues like race conditions.```{python}import threadingimport timedef worker_function(name, delay):"""A simple worker function that simulates some work"""print(f"Thread {name} starting...") time.sleep(delay)print(f"Thread {name} finished after {delay} seconds")# Get information about the current threadmain_thread = threading.current_thread()print(f"Main thread name: {main_thread.name}")print(f"Main thread ID: {main_thread.ident}")print(f"Active thread count: {threading.active_count()}")```## Creating and Managing Threads```{python}# Create some threadsthreads = []for i inrange(3): thread = threading.Thread( target=worker_function, args=(f"Worker-{i}", i +1), name=f"WorkerThread-{i}" ) threads.append(thread)# Start all threadsfor thread in threads: thread.start()# Wait for all threads to completefor thread in threads: thread.join()print("All threads completed!")```# Process vs Thread ComparisonLet's create a practical example to demonstrate the differences:```{python}import multiprocessingimport threadingimport timeimport osdef cpu_intensive_task(n):"""A CPU-intensive task for demonstration""" result =0for i inrange(n): result += i * ireturn resultdef io_intensive_task(duration):"""An I/O-intensive task (simulated with sleep)""" time.sleep(duration)returnf"Task completed after {duration} seconds"# Function to run with multiprocessingdef process_worker(task_id, n): start_time = time.time() result = cpu_intensive_task(n) end_time = time.time()print(f"Process {task_id} (PID: {os.getpid()}) completed in {end_time - start_time:.2f} seconds")return result# Function to run with threadingdef thread_worker(task_id, duration): start_time = time.time() result = io_intensive_task(duration) end_time = time.time()print(f"Thread {task_id} completed in {end_time - start_time:.2f} seconds")return result# Demonstrate threading for I/O-bound tasksprint("=== Threading Example (I/O-bound) ===")start_time = time.time()threads = []for i inrange(3): thread = threading.Thread(target=thread_worker, args=(i, 1)) threads.append(thread) thread.start()for thread in threads: thread.join()print(f"Total time with threading: {time.time() - start_time:.2f} seconds")```# Memory and Resource UsageLet's examine how processes and threads use system resources:```{python}import psutilimport threadingimport multiprocessingimport timedef monitor_resources():"""Monitor system resources""" process = psutil.Process()print(f"Memory usage: {process.memory_info().rss /1024/1024:.2f} MB")print(f"CPU usage: {process.cpu_percent()}%")print(f"Number of threads: {process.num_threads()}")# print(f"Number of file descriptors: {process.num_fds()}")print("---")print("Initial resource usage:")monitor_resources()# Create some threadsdef dummy_thread_work(): time.sleep(2)threads = []for i inrange(5): thread = threading.Thread(target=dummy_thread_work) threads.append(thread) thread.start()print("After creating 5 threads:")monitor_resources()# Clean upfor thread in threads: thread.join()print("After threads completed:")monitor_resources()```# When to Use Processes vs Threads## Use **Processes** when:1. **CPU-bound tasks** - Tasks that require intensive computation2. **Fault isolation** - You want to isolate failures3. **True parallelism** - You need to utilize multiple CPU cores4. **Different programming languages** - Communicating between different systems## Use **Threads** when:1. **I/O-bound tasks** - Tasks that wait for file reads, network requests, etc.2. **Shared state** - You need to share data between concurrent operations3. **Lightweight concurrency** - You need many concurrent operations with low overhead4. **Responsive interfaces** - Keeping a GUI responsive while doing background work# Practical Example: System MonitorLet's create a simple system monitor that demonstrates these concepts:```{python}import psutilimport threadingimport timefrom datetime import datetimeclass SystemMonitor:def__init__(self):self.monitoring =Falseself.stats = []def collect_stats(self):"""Collect system statistics"""whileself.monitoring: stats = {'timestamp': datetime.now().strftime('%H:%M:%S'),'cpu_percent': psutil.cpu_percent(interval=1),'memory_percent': psutil.virtual_memory().percent,'disk_usage': psutil.disk_usage('/').percent ifhasattr(psutil, 'disk_usage') else0,'process_count': len(psutil.pids()) }self.stats.append(stats) time.sleep(1)def start_monitoring(self, duration=5):"""Start monitoring for specified duration"""self.monitoring =Trueself.stats = []# Start monitoring thread monitor_thread = threading.Thread(target=self.collect_stats) monitor_thread.daemon =True monitor_thread.start()# Let it run for specified duration time.sleep(duration)self.monitoring =False# Wait for thread to finish monitor_thread.join()returnself.statsdef display_stats(self):"""Display collected statistics"""ifnotself.stats:print("No statistics collected")returnprint("System Statistics:")print("Time | CPU% | Memory% | Processes")print("-"*40)for stat inself.stats:print(f"{stat['timestamp']} | {stat['cpu_percent']:5.1f} | {stat['memory_percent']:7.1f} | {stat['process_count']:9d}")# Use the system monitormonitor = SystemMonitor()print("Starting system monitoring for 5 seconds...")monitor.start_monitoring(5)monitor.display_stats()```# Key Takeaways1. **Processes** are isolated instances of programs with their own memory space2. **Threads** are lightweight execution units within processes that share memory3. **CPU-bound tasks** benefit from multiprocessing4. **I/O-bound tasks** benefit from multithreading5. **psutil** is an excellent library for system monitoring and process management# Exercises1. **Process Explorer**: Create a script that lists all running processes and their memory usage2. **Thread Pool**: Implement a simple thread pool to handle multiple I/O operations3. **Resource Monitor**: Build a real-time system resource monitor using threading# Additional Resources- [psutil Documentation](https://psutil.readthedocs.io/)- [Python Threading Module](https://docs.python.org/3/library/threading.html)- [Python Multiprocessing Module](https://docs.python.org/3/library/multiprocessing.html)- [Real Python - Concurrency](https://realpython.com/python-concurrency/)---**Next**: [Lesson 2: Multiprocessing and Multithreading](02-multiprocessing-multithreading.qmd)