Threads
Threads#
Threads are independent execution paths within a program that can run simultaneously. Think of a thread as a separate worker in a factory—each worker can perform tasks independently while sharing the same workspace and tools with other workers.
Imagine a restaurant kitchen during dinner rush. The head chef (main thread) coordinates the team, while line cooks (worker threads) prepare different dishes simultaneously. One cook grills steaks, another prepares salads, and a third makes desserts—all working in parallel to serve customers faster. All cooks share the same kitchen space (memory), ingredients (data), and equipment (resources).
Threads enable your program to do multiple things at once: keep the user interface responsive while processing data in the background, handle multiple client requests simultaneously, or take advantage of multiple CPU cores for faster computation.
Thread Lifecycle#
A thread's journey from creation to completion follows a predictable path. When first created, the thread exists but hasn't started working yet. Once started, it enters the ready state, waiting for the operating system to give it CPU time. When scheduled, the thread runs on a CPU core until it either finishes its work, needs to wait for something (like file I/O or a lock), or gets temporarily suspended to let other threads run.
stateDiagram-v2
[*] --> New: Thread Creation
New --> Ready: Thread Start
Ready --> Running: Scheduler Dispatch
Running --> Ready: Time Slice Expiry
Running --> Blocked: Resource Wait
Blocked --> Ready: Resource Available
Running --> Terminated: Completion
Terminated --> [*]: Resource Cleanup
Creating Threads#
Most languages offer multiple ways to create and work with threads, from simple inheritance-based approaches to sophisticated thread pool systems that manage resources automatically.
Class-Based Threads#
One approach for creating threads involves defining a class that encapsulates thread behavior and specifies what work should be performed. Different languages offer varying implementations—from inheritance-based patterns to queue-based dispatch systems and operation objects.
Java uses class inheritance to create threads. By extending the Thread class and overriding the run() method, you define what work the thread performs. Call start() to begin execution and join() to wait for completion.
class WorkerThread extends Thread {
private String name;
public WorkerThread(String name) { this.name = name; }
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + " working: " + i);
try { Thread.sleep(1000); } catch (InterruptedException e) { break; }
}
}
}
// Usage
WorkerThread worker = new WorkerThread("Worker-1");
worker.start();
worker.join();
Kotlin also extends the Thread class, similar to Java but with more concise syntax. The run() method is overridden to define the thread's work, and the same start() and join() methods control execution.
TypeScript/JavaScript doesn't support traditional threading. JavaScript is single-threaded by design and doesn't have an equivalent class-based thread creation mechanism.
Dart doesn't support traditional threading. Dart uses isolates for concurrency, which have a fundamentally different architecture with no shared memory.
Swift provides multiple approaches for concurrency. Grand Central Dispatch (GCD) uses dispatch queues to manage work, while OperationQueue provides a higher-level, object-oriented approach with dependencies and cancellation support.
import Foundation
// Using DispatchQueue (Grand Central Dispatch)
func dispatchQueueExample() {
// 'qos' stands for "quality of service"—it controls thread priority and scheduling.
// .userInitiated means high priority, tasks started by the user and needing quick results.
let queue = DispatchQueue(label: "com.example.worker", qos: .userInitiated)
queue.async {
for i in 0..<5 {
print("Worker working: \(i)")
Thread.sleep(forTimeInterval: 1.0)
}
}
}
// Using OperationQueue
class WorkerOperation: Operation {
private let name: String
private let iterations: Int
init(name: String, iterations: Int) {
self.name = name
self.iterations = iterations
}
override func main() {
for i in 0..<iterations {
print("\(name) working: \(i)")
Thread.sleep(forTimeInterval: 1.0)
}
}
}
// Usage
let operation = WorkerOperation(name: "Worker-1", iterations: 5)
let operationQueue = OperationQueue()
operationQueue.addOperation(operation)
operationQueue.waitUntilAllOperationsAreFinished()
Python extends the threading.Thread class, similar to Java and Kotlin. Override the run() method to define the work, and use start() to begin execution and join() to wait for completion.
import threading
import time
class WorkerThread(threading.Thread):
def __init__(self, name, iterations):
super().__init__()
self.name = name
self.iterations = iterations
def run(self):
for i in range(self.iterations):
print(f"{self.name} working: {i}")
time.sleep(1)
# Usage
worker = WorkerThread("Worker-1", 5)
worker.start()
worker.join()
Function-Based Threads#
Another approach separates the task definition from thread creation by using functions, closures, or callable objects. This provides more flexibility and follows the principle of composition over inheritance, allowing better code organization and reusability across different concurrent execution models.
Java uses the Runnable interface to separate the task from the thread. Create a class implementing Runnable, define the work in run(), then pass it to a Thread constructor. This allows your class to extend other classes.
class Task implements Runnable {
private String name;
public Task(String name) { this.name = name; }
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + " processing: " + i);
try { Thread.sleep(1000); } catch (InterruptedException e) { break; }
}
}
}
// Usage
Thread thread = new Thread(new Task("Worker-1"));
thread.start();
thread.join();
Kotlin also implements the Runnable interface with cleaner syntax. The approach is identical to Java—separate the task from the thread for better flexibility.
TypeScript/JavaScript doesn't support traditional threading and has no equivalent to the Runnable pattern.
Dart doesn't support traditional threading and has no equivalent to the Runnable pattern.
Swift does not have a direct equivalent of Java's Runnable interface or Kotlin's Runnable class for threads.
Swift provides explicit threads using the Thread class. You can pass either a closure directly or define the task as a function and pass that function as the thread's entry point.
import Foundation
// Define the task as a function
func workerTask() {
for i in 0..<5 {
print("Worker-1 processing: \(i)")
Thread.sleep(forTimeInterval: 1.0)
}
}
// Example: Spawning a Thread and passing a function as the entry point using a closure
let thread = Thread(block: workerTask)
thread.start()
thread.join() // Waits for thread completion (join is available in Foundation)
Python uses regular functions as tasks by passing them to the Thread constructor with the target parameter. Arguments are provided via the args tuple, making it simple to define work without creating classes.
Using Lambda Expressions#
Modern languages support lambda expressions for creating threads with clean, concise syntax—perfect for simple tasks that don't warrant a separate class. This approach is similar to passing a function as demonstrated in the previous section, but allows expressing the thread's task inline.
Java supports lambda expressions that implement the Runnable interface inline. This eliminates boilerplate code for simple, one-off tasks, making the code more readable and concise.
Kotlin has excellent lambda support with trailing lambda syntax, making thread creation extremely concise. The lambda is automatically converted to a Runnable.
TypeScript/JavaScript doesn't support traditional threading. Lambda expressions cannot be used to create threads.
Dart doesn't support traditional threading. Lambda expressions cannot be used to create threads.
Swift allows you to create threads concisely using closures—no need to declare a separate function. Just pass a closure directly to the Thread initializer with the block: parameter.
Python supports lambda functions, though for multi-statement tasks, regular functions are often more readable. Lambdas work well for very simple, inline thread tasks.
Thread Pools#
Thread pools provide the most efficient approach for production applications. Instead of creating new threads for every task, a pool maintains a set of reusable worker threads. This eliminates the overhead of constantly creating and destroying threads while automatically managing the thread lifecycle.
Java provides the ExecutorService framework for thread pools. Use Executors factory methods to create pools of various sizes, submit tasks, and manage graceful shutdown with shutdown() and awaitTermination().
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) throws InterruptedException {
// Create thread pool
ExecutorService executor = Executors.newFixedThreadPool(4);
// Submit tasks
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " executed by " +
Thread.currentThread().getName());
try { Thread.sleep(1000); } catch (InterruptedException e) {}
});
}
// Graceful shutdown
executor.shutdown();
executor.awaitTermination(30, TimeUnit.SECONDS);
}
}
Kotlin uses Java's ExecutorService framework with cleaner syntax. The same pool patterns apply, with Kotlin's concise lambda syntax making task submission more elegant.
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
fun main() {
val executor = Executors.newFixedThreadPool(4)
repeat(10) { taskId ->
executor.submit {
println("Task $taskId executed by ${Thread.currentThread().name}")
Thread.sleep(1000)
}
}
executor.shutdown()
executor.awaitTermination(30, TimeUnit.SECONDS)
}
TypeScript/JavaScript doesn't support traditional threading and has no equivalent to thread pools.
Dart doesn't support traditional threading and has no equivalent to thread pools.
Swift uses OperationQueue as a thread pool abstraction. By limiting maxConcurrentOperationCount, you control the parallelism level, and you submit units of work as Operation (or BlockOperation) objects. The system manages a pool of threads for you and runs the operations concurrently up to your chosen limit.
import Foundation
// Swift using OperationQueue for thread pool-like behavior
func threadPoolWithOperationsExample() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4 // Thread pool with 4 worker threads
var operations: [BlockOperation] = []
for taskId in 0..<10 {
let op = BlockOperation {
print("Task \(taskId) executed by \(Thread.current)")
Thread.sleep(forTimeInterval: 1.0)
}
operations.append(op)
}
queue.addOperations(operations, waitUntilFinished: true)
print("All tasks completed")
}
// Usage
threadPoolWithOperationsExample()
Python provides ThreadPoolExecutor in the concurrent.futures module. Use context managers for automatic cleanup, submit tasks, and wait for completion. The pool handles thread lifecycle automatically.
import concurrent.futures
import time
def execute_task(task_id):
print(f"Task {task_id} executed")
time.sleep(1)
return task_id
# Using ThreadPoolExecutor
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(execute_task, i) for i in range(10)]
concurrent.futures.wait(futures)
# Usage
print("All tasks completed")
Thread Coordination#
Join Operations#
Sometimes you need to wait for concurrent work to finish before continuing. Join operations (or their equivalents) block the calling context until the target work completes, ensuring tasks finish in the correct order. Different languages implement this through various mechanisms—explicit join methods, dispatch groups, or async/await patterns.
Java uses the join() method to wait for a thread to complete. Call join() on each thread you want to wait for, and the calling thread will block until those threads finish their work.
Thread worker1 = new Thread(() -> {
System.out.println("Worker 1 starting");
try { Thread.sleep(2000); } catch (InterruptedException e) {}
System.out.println("Worker 1 finished");
});
Thread worker2 = new Thread(() -> {
System.out.println("Worker 2 starting");
try { Thread.sleep(1500); } catch (InterruptedException e) {}
System.out.println("Worker 2 finished");
});
worker1.start();
worker2.start();
// Wait for both to complete
worker1.join();
worker2.join();
System.out.println("All workers completed");
Kotlin also uses the join() method from Java's threading API. The syntax is cleaner with Kotlin's lambda syntax, but the concept is identical—wait for threads to complete.
val worker1 = Thread {
println("Worker 1 starting")
Thread.sleep(2000)
println("Worker 1 finished")
}
val worker2 = Thread {
println("Worker 2 starting")
Thread.sleep(1500)
println("Worker 2 finished")
}
worker1.start()
worker2.start()
// Wait for both to complete
worker1.join()
worker2.join()
println("All workers completed")
TypeScript/JavaScript doesn't support traditional threading and has no equivalent join operation for threads.
Dart doesn't support traditional threading and has no equivalent join operation for threads.
Swift can use the Operation and OperationQueue APIs to coordinate concurrent work, and you can enforce completion order using dependencies between operations. By setting a dependency, you ensure one operation only starts after another completes—thus controlling their execution order.
import Foundation
class WorkerOperation: Operation {
let name: String
let sleepTime: TimeInterval
init(name: String, sleepTime: TimeInterval) {
self.name = name
self.sleepTime = sleepTime
}
override func main() {
print("\(name) starting")
Thread.sleep(forTimeInterval: sleepTime)
print("\(name) finished")
}
}
func joinOperationsWithOrderedCompletion() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
let worker1 = WorkerOperation(name: "Worker 1", sleepTime: 2.0)
let worker2 = WorkerOperation(name: "Worker 2", sleepTime: 1.5)
// Ensure worker2 starts after worker1 finishes
worker2.addDependency(worker1)
queue.addOperations([worker1, worker2], waitUntilFinished: true)
print("All workers completed in order")
}
// Usage
joinOperationsWithOrderedCompletion()
Python uses the join() method on Thread objects to wait for completion. Like Java, calling join() blocks the current thread until the target thread finishes execution.
import threading
import time
def worker1():
print("Worker 1 starting")
time.sleep(2)
print("Worker 1 finished")
def worker2():
print("Worker 2 starting")
time.sleep(1.5)
print("Worker 2 finished")
# Create and start threads
thread1 = threading.Thread(target=worker1)
thread2 = threading.Thread(target=worker2)
thread1.start()
thread2.start()
# Wait for both to complete
thread1.join()
thread2.join()
print("All workers completed")
Interrupt Mechanism#
To gracefully stop concurrent work, languages provide various cancellation mechanisms. This cooperative approach allows running tasks to clean up resources before stopping, rather than abruptly terminating them. Implementations range from interrupt flags and stop events to cancellation tokens and boolean flags.
Java uses the interrupt mechanism with interrupt() and isInterrupted(). Threads check the interrupt flag periodically and handle InterruptedException for blocking operations. Always restore the interrupt status in catch blocks.
class InterruptibleWorker extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Working...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Interrupted, cleaning up...");
Thread.currentThread().interrupt(); // Restore interrupt status
break;
}
}
}
}
// Usage
InterruptibleWorker worker = new InterruptibleWorker();
worker.start();
// After some time, interrupt the worker
Thread.sleep(5000);
worker.interrupt();
worker.join();
Kotlin uses Java's interrupt mechanism with the same semantics. Check isInterrupted, handle InterruptedException, and restore the interrupt status when interrupted.
class InterruptibleWorker : Thread() {
override fun run() {
while (!isInterrupted) {
println("Working...")
try {
Thread.sleep(1000)
} catch (e: InterruptedException) {
println("Interrupted, cleaning up...")
interrupt() // Restore interrupt status
break
}
}
}
}
// Usage
val worker = InterruptibleWorker()
worker.start()
// After some time, interrupt the worker
Thread.sleep(5000)
worker.interrupt()
worker.join()
TypeScript/JavaScript doesn't support traditional threading and has no equivalent interrupt mechanism for threads.
Dart doesn't support traditional threading and has no equivalent interrupt mechanism for threads.
Swift uses custom boolean flags for cancellation. For modern async/await, use Task.isCancelled to check cancellation status and respond appropriately.
import Foundation
class InterruptibleWorker {
private var isInterrupted = false
private let workQueue = DispatchQueue(label: "worker")
func work() {
workQueue.async {
while !self.isInterrupted {
print("Working...")
Thread.sleep(forTimeInterval: 1.0)
}
print("Interrupted, cleaning up...")
}
}
func interrupt() {
isInterrupted = true
}
}
// Usage
let worker = InterruptibleWorker()
worker.work()
// After some time, interrupt the worker
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
worker.interrupt()
}
Python uses threading.Event for cancellation. The thread checks is_set() periodically or uses wait() with a timeout to detect cancellation signals. Call set() to trigger cancellation.
import threading
import time
class InterruptibleWorker(threading.Thread):
def __init__(self):
super().__init__()
self._stop_event = threading.Event()
def run(self):
while not self._stop_event.is_set():
print("Working...")
if self._stop_event.wait(1.0): # Wait with timeout
print("Interrupted, cleaning up...")
break
def interrupt(self):
self._stop_event.set()
# Usage
worker = InterruptibleWorker()
worker.start()
# After some time, interrupt the worker
time.sleep(5)
worker.interrupt()
worker.join()
Thread Safety#
When multiple threads access shared data simultaneously, race conditions can occur. Imagine a shared whiteboard with a sales counter: two people read "47 sales" at the same time, both add their sale and write "48"—but there should be 49! One sale vanishes.
Thread safety requires synchronization mechanisms to coordinate access to shared resources. For comprehensive coverage of locks, semaphores, condition variables, and atomic operations, see the Synchronization page.