First Concurrent Programs

Starting a thread is like hiring a temporary helper and giving them a job sheet.

Learning Objectives

Create, start and join threads; return results; pass arguments safely; capture variables correctly in closures.

Understand thread lifetime, ownership, worker functions and main-thread responsibilities; joining versus detaching.

Use thread pools, task pools and futures; handle exceptions in workers; cancel and shut down workers; clean up resources.

Name threads for diagnostics, respect creation limits, avoid the common beginner errors, and build a small safe task runner.

Plain-Language Explanation

Starting a thread is like hiring a temporary helper and giving them a job sheet. You must make sure the job sheet stays valid while they work, collect what they produce, find out if something went wrong, and make sure everyone has gone home before you lock up. Thread pools are a permanent team: instead of hiring for every job, you hand jobs to a fixed group of helpers and get a receipt — a future — that later gives you the result or the error.

Precise Technical Explanation

Creating a thread Every language wraps the OS primitive: pthread_create (POSIX C), std::thread/std::jthread (C++), new Thread(runnable).start() / Thread.ofVirtual() (Java), threading.Thread(target=…).start() (Python), new Thread(...) / Task.Run (C#), go f() (Go goroutines), std::thread::spawn (Rust).

Starting and joining threads Creating a thread object and starting it can be separate steps (Java, Python) or the same (C++, POSIX). Joining blocks until the thread finishes; it is also the point after which the thread's effects are guaranteed visible to the joiner.

Returning results from workers Options: write into a slot owned by the joiner (an array element per thread), return through the join (pthread_join's void **, Rust's JoinHandle<T>), or use a future (std::future, concurrent.futures.Future, Task<T>, CompletableFuture).

Passing arguments safely Pass values (copies) or pointers/references to objects that outlive the thread's use of them. Never pass a pointer to a stack variable that may change (a loop counter) or die (a local in a function that returns).

Capturing variables in closures Closures capture variables, not values, in many languages. In Python a lambda created in a loop sees the variable's *final* value unless you bind it (lambda i=i: …). Go before 1.22 shared one loop variable across iterations; Go 1.22+ gives each iteration its own. C++ lambdas capture by value [x] or by reference [&x] — a reference capture of a dying local dangles. Java requires captured locals to be effectively final; C# foreach variables are per-iteration since C# 5, but for loop variables are shared.

Thread lifetime and ownership Every thread should have an owner responsible for joining it (or deliberately detaching it) and for keeping its inputs alive. RAII types (std::jthread), structured scopes (Module 13) and with blocks for pools make ownership explicit.

A worker function should take everything it needs as parameters, return or publish its result, handle its own errors (or let the framework capture them), and check for cancellation regularly.

The main thread typically creates workers, distributes work, waits for completion, collects results and errors, handles signals/shutdown, and owns the UI in GUI apps. When main returns, many runtimes end the process (C/C++/Go) — others wait for non-daemon threads (Java, Python).

Joining versus detaching Detached threads cannot be joined, so you cannot learn when they finish or whether they failed, and they must not touch objects that may be destroyed. Prefer joining. Python's daemon=True threads are killed abruptly at interpreter exit — fine for helpers, dangerous for anything that writes data.

A thread pool keeps a set of worker threads and a task queue. Submitting a task is cheap; the pool reuses threads, bounds concurrency, and offers futures for results (ThreadPoolExecutor, ExecutorService, .NET ThreadPool, C++ libraries such as BS::thread_pool).

Higher-level runtimes schedule lightweight *tasks* (not threads) onto pools: .NET Task, Java ForkJoinPool, Rust's Tokio, Go's scheduler for goroutines. A task is a unit of work; a thread is an executor of tasks.

Futures and result handles A future is a handle to a result that will be available later: result()/get() blocks until it is ready, returns the value or re-raises the task's exception (Module 9).

Exceptions in worker threads An exception thrown in a thread does not propagate to the thread that started it. Behavior varies: Python prints it through threading.excepthook and the thread dies; Java calls the thread's UncaughtExceptionHandler; C++ calls std::terminate if an exception escapes the thread function; Go panics crash the whole program; C# unhandled exceptions in threads terminate the process. Futures capture exceptions and rethrow them at get()/result() — the reason to prefer them.

Worker cancellation and shutdown Threads cannot be safely killed from outside. Cancellation is cooperative: set a flag or a stop token that the worker checks (std::stop_token, threading.Event, CancellationToken, context.Context), wake it if it is waiting, then join it.

A worker that holds files, sockets or locks must release them on every exit path — RAII, try/finally, with, using, defer.

Thread naming and diagnostics Name threads (threading.Thread(name=…), pthread_setname_np, Thread.setName, Thread.Name) so debuggers, thread dumps, logs and profilers show *what* each thread does.

Limits on thread creation Each OS thread needs a stack and kernel structures. Limits: per-user process/thread limit (ulimit -u), /proc/sys/kernel/threads-max, address space, memory. Hitting them makes creation fail (EAGAIN, OutOfMemoryError: unable to create native thread).

Common beginner errors Forgetting to join; reading results before joining; passing the loop variable; sharing a mutable object without synchronization; swallowing worker exceptions; creating one thread per item for millions of items; calling run() instead of start() in Java/Python (runs the function in the *current* thread).

Building a safe task runner Combine the rules: bounded workers, a queue, futures for results and errors, a with scope that always shuts down and joins. Implemented below.

Why It Matters in Real Systems

Most production concurrency bugs at this level are lifetime bugs (dangling arguments, unjoined threads), lost errors (silent worker crashes), and unbounded thread creation (memory exhaustion under load).

Key Terms

Wait for a thread to finish Let a thread run unowned Thread that does not keep the process alive

Reusable worker threads fed from a queue Handle to a result available later Cooperative cancellation signal How a lambda refers to outer variables

Diagram or Execution Timeline

flowchart LR
  M[main: submit 4 tasks] --> Q[pool queue]
  Q --> W1[worker 1]
  Q --> W2[worker 2]
  W1 --> F[futures: value or exception]
  W2 --> F
  F --> M2[main: result / handle error]
  M2 --> S[shutdown: join workers]

Syntax and API Overview

The specification's C++ example (G++ 11.2, -std=c++20 -pthread):

The order of these two lines is guaranteed: join() returns only after worker has completed, so its output happens before the second line. Without the join (or with other threads printing concurrently), writes to std::cout from different threads could interleave. A joinable std::thread must be joined or detached before it is destroyed — otherwise std::terminate is called. C++20's std::jthread joins automatically in its destructor and carries a std::stop_token (shown below).

#include <iostream>
#include <thread>

void worker() {
    std::cout << "Worker started\n";
}

int main() {
    std::thread t(worker);
    t.join();
    std::cout << "Main continues after join\n";
}
Worker started
Main continues after join

Complete Code Example

Results, exceptions, closure capture, naming and a safe task runner in CPython 3.13.1:

import threading
from concurrent.futures import ThreadPoolExecutor


# 1. closure capture: late binding vs bound argument
late = [lambda: i for i in range(3)]
bound = [lambda i=i: i for i in range(3)]
print("late-bound closures:", [f() for f in late], "| bound:", [f() for f in bound])

# 2. results through per-thread slots + join
results = [None] * 3
def square_into(index, value):
    results[index] = value * value       # each thread writes only its own slot
threads = [threading.Thread(target=square_into, args=(k, k + 2), name=f"squarer-{k}") for k in range(3)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print("results after join:", results, "| names:", [t.name for t in threads])

# 3. an exception in a plain thread does not reach the starter
errors = []
threading.excepthook = lambda args: errors.append(f"{args.thread.name}: {args.exc_type.__name__}: {args.exc_value}")
bad = threading.Thread(target=lambda: 1 / 0, name="divider")
bad.start()
bad.join()
print("plain thread failure was only reported to excepthook:", errors)

# 4. futures capture exceptions and re-raise them in the caller
def parse(text):
    return int(text)
with ThreadPoolExecutor(max_workers=2, t
…

Line-by-Line Explanation

[lambda: i for i in range(3)] creates three closures over the *same* variable i, which ends at 2; lambda i=i: i binds the current value as a default argument.

Each worker writes only results[index]; reading results after all joins is safe.

An exception in a plain Thread is passed to threading.excepthook (by default printed to stderr) and the thread ends; the starting thread receives nothing.

pool.submit returns a Future; result(timeout=5) waits up to 5 s and re-raises the worker's exception in the caller.

Leaving the with block calls shutdown(wait=True), which joins the pool's workers.

Output

This output is deterministic: every print happens in the main thread after the relevant join or result().

late-bound closures: [2, 2, 2] | bound: [0, 1, 2]
results after join: [4, 9, 16] | names: ['squarer-0', 'squarer-1', 'squarer-2']
plain thread failure was only reported to excepthook: ['divider: ZeroDivisionError: division by zero']
parse('10') -> 10
parse('x') raised in the worker, re-raised here: invalid literal for int() with base 10: 'x'
parse('30') -> 30
pool shut down; worker threads alive: 0

Interleaving or State Trace

For section 4 with two workers: parser_0 takes "10" and parser_1 takes "x" (in whichever order the pool dispatches); the main thread blocks in futures["10"].result() until it is done, then in futures["x"].result(), which re-raises ValueError; meanwhile a free worker takes "30". The *printing* order is fixed by the main thread's loop, not by completion order.

Correctness and Safety Analysis

The runner is safe because: workers never share mutable state; results travel through futures (which publish safely); errors are surfaced; the with block guarantees shutdown and joining even if the body raises.

Stop tokens and automatic joining in C++20 (std::jthread):

ticks is read only after the jthread destructor has joined the worker, so there is no race. The exact number of ticks varies run to run, so the program prints only whether it is positive.

#include <chrono>
#include <iostream>
#include <thread>

int main() {
    using namespace std::chrono_literals;
    int ticks = 0;
    {
        std::jthread worker([&ticks](std::stop_token st) {
            while (!st.stop_requested()) {        // cooperative cancellation point
                ++ticks;
                std::this_thread::sleep_for(10ms);
            }
            std::cout << "worker saw the stop request and cleaned up\n";
        });
        std::this_thread::sleep_for(55ms);
        worker.request_stop();                    // ask, do not kill
    }                                             // ~jthread: joins automatically
    std::cout << "after scope: worker joined, ticks > 0: " << std::boolalpha << (ticks > 0) << '\n';
}
worker saw the stop request and cleaned up
after scope: worker joined, ticks > 0: true

Common Mistakes and Misconceptions

Calling t.run() instead of t.start() (Python/Java) — no new thread.

Reading results before joining.

Assuming exceptions in workers will crash or notify the main thread.

Using daemon=True or detach() to "fix" shutdown hangs, losing work silently.

One thread per task for unbounded input.

Failure Modes and Debugging Steps

Program never exits: a non-daemon thread is still running or blocked — take a thread dump (py-spy dump, jstack, gdb thread apply all bt).

Missing results: a worker died with an exception — check excepthook/logs, or use futures.

RuntimeError: can't start new thread / EAGAIN: thread limit reached — use a pool.

Performance and Scalability Implications

Thread creation costs tens of microseconds and a stack; pools amortise it. Size pools to the workload (Module 15): around the number of cores for CPU-bound work, more for blocking I/O.

Security Considerations

Unbounded thread creation driven by requests is a denial-of-service vector. Thread names and logs should not include secrets.

Platform and Runtime Caveats

Python's ThreadPoolExecutor default max_workers is min(32, os.cpu_count() + 4) (3.8+).

C++ std::jthread/stop_token need C++20 (GCC 10+, Clang 17+ libc++ partially, MSVC 19.28+).

Java's ExecutorService is AutoCloseable (try-with-resources) only from Java 19.

Real-World Applications

Background workers in GUIs, parallel test runners, web crawlers, image thumbnailers, build tools — all built from these pieces.

Best Practices

Prefer pools and futures to raw threads.

Give every thread or pool an owner and a scope.

Bind loop variables explicitly when creating closures.

Surface every worker failure.

Make cancellation cooperative and test it.

Exercises

E1 · Beginner. Fix: threads = [Thread(target=lambda: print(i)) for i in range(3)].

Answer: Bind the value: Thread(target=lambda i=i: print(i)), or Thread(target=print, args=(i,)).

E2 · Intermediate. In the C++ jthread example, why is it safe for the worker to modify ticks and for main to read it afterwards without a mutex?

Answer: Only the worker accesses ticks while it runs; main reads it after the jthread destructor has joined the worker, and joining synchronizes-with the thread's completion, so all its writes are visible. (Reading ticks *during* the worker's run would be a data race.)

E3 · Advanced challenge. Extend the Python runner so the first failing task cancels all tasks that have not started yet.

Answer: Use concurrent.futures.wait(futures, return_when=FIRST_EXCEPTION), then call f.cancel() on every not-done future (it succeeds only for tasks still queued) and pool.shutdown(cancel_futures=True) (Python 3.9+). Running tasks must check a shared threading.Event to stop early.

Quiz

? What happens if an exception escapes a std::thread's function in C++?
- It is rethrown in the thread that calls join() :: That is std::future's behavior, not std::thread's.
+ std::terminate is called :: Exceptions must not escape a thread function.
- It is silently ignored :: C++ terminates the program.
- The thread restarts :: Threads never restart automatically.

? Why does [lambda: i for i in range(3)] produce [2, 2, 2] when called?
+ All three lambdas refer to the same variable i, which is 2 after the loop. :: Late binding.
- Python copies i into each lambda at creation. :: That only happens with a default argument.
- The GIL reorders the lambdas. :: Unrelated to the GIL.
- range(3) ends at 2 and lambdas only see the end. :: They see the variable's current value.

? Which keep worker failures from being lost? (select all that apply)
+ Submitting work to a pool and calling future.result() :: Futures capture and re-raise exceptions.
+ Installing an uncaught-exception handler that logs/escalates :: threading.excepthook, UncaughtExceptionHandler.
- Making the thread a daemon :: Daemon threads are killed silently at exit.
- Detaching the thread :: Nobody can observe its outcome.

Summary

Threads need an owner, valid inputs, a result channel, an error channel and a shutdown path. Join to wait and to see their effects. Futures and pools provide results, errors and lifecycle management with much less code; stop tokens and events make cancellation cooperative. Bind closure variables, name threads, and never create threads without bound.

Further Reading

Python docs: threading, concurrent.futures.

cppreference: std::thread, std::jthread, std::stop_token.

Brian Goetz et al., *Java Concurrency in Practice*, chapters 6–7.

Anthony Williams, *C++ Concurrency in Action*, 2nd ed., chapter 2.