AlgoInsight

Free lesson · Operating Systems

Shared variable

Easy10 min readoperating-systemsosconcurrency

A Process Can Do More Than One Thing at a Time

In Module 01, you learned that a process is an independent execution unit with its own memory space, file descriptors, and system resources. A process runs a program. But what happens when that program needs to do multiple things simultaneously?

Consider a web browser. While it is rendering a web page, it also needs to download images in the background, respond to your mouse clicks, play a video, and run JavaScript animations - all at the same time. If a process could only do one thing at a time, the browser would have to finish downloading every image before it could respond to your click. That would make the web unusable.

The solution is threads. A thread is a unit of execution within a process. A single process can have multiple threads, and each thread runs independently - it has its own path through the code, its own position in the program (its own program counter), and its own stack for function calls. But all threads within a process share the same memory space - the same heap, the same global variables, the same open files.

This sharing is the key insight. Threads are sometimes called "lightweight processes" because they give you the ability to do multiple things concurrently without the overhead of creating entirely separate processes. Creating a new thread is much cheaper than creating a new process, and switching between threads within the same process is much cheaper than switching between separate processes.

graph TD
    subgraph PROCESS["Process (PID 1000)"]
        subgraph SHARED["Shared Resources"]
            CODE["Code (Text Segment)"]
            HEAP["Heap Memory"]
            GLOBALS["Global Variables"]
            FILES["Open File Descriptors"]
        end
        
        subgraph T1["Thread 1"]
            T1PC["Program Counter"]
            T1REGS["Registers"]
            T1STACK["Stack"]
        end
        
        subgraph T2["Thread 2"]
            T2PC["Program Counter"]
            T2REGS["Registers"]
            T2STACK["Stack"]
        end
        
        subgraph T3["Thread 3"]
            T3PC["Program Counter"]
            T3REGS["Registers"]
            T3STACK["Stack"]
        end
    end
This diagram shows a single process containing three threads. The process has shared resources that all threads can access: the code segment, heap memory, global variables, and open file descriptors. Each thread has its own private resources: its own program counter, its own registers, and its own stack. The threads share the process's memory but maintain their own execution state.

What a Thread Has and What It Shares

To understand threads, you need to be clear about what each thread owns privately and what it shares with other threads in the same process.

Each thread has its own:

Program counter. Each thread is at a different point in the code. Thread 1 might be executing a function that handles user input. Thread 2 might be executing a function that downloads data from the network. They are running different parts of the same program simultaneously, so each needs its own program counter to track where it is.

Registers. Each thread has its own set of CPU register values. When the OS switches from one thread to another, it saves the current thread's register values and loads the next thread's register values - just like a context switch between processes, but lighter.

Stack. Each thread has its own call stack. When Thread 1 calls a function, its local variables and return address go on Thread 1's stack. When Thread 2 calls a function, they go on Thread 2's stack. If threads shared a stack, their function calls would interleave and corrupt each other.

Thread-local storage. Some systems allow threads to have private variables that are not visible to other threads. This is called thread-local storage (TLS). It is useful when a thread needs a private copy of a variable that would otherwise be shared.

All threads in a process share:

Code segment. All threads execute the same program. They may be in different functions at any given time, but they all have access to the same compiled code.

Heap. Dynamically allocated memory (from malloc, new, or object creation in managed languages) is shared. If Thread 1 allocates an object on the heap, Thread 2 can access it - and modify it. This sharing is both the power and the danger of threads.

Global variables. Any variable declared at the global or static scope is visible to all threads. If Thread 1 updates a global counter, Thread 2 sees the new value. If they both update it at the same time without synchronization, you get a race condition - a topic we cover in depth in Module 05.

Open file descriptors. If Thread 1 opens a file, Thread 2 can read from or write to the same file descriptor. Similarly for network sockets - all threads share the same set of open connections.

Process ID and permissions. All threads in a process share the same PID, the same user ID, the same group ID, and the same set of permissions. From the OS's perspective, a thread is not a separate security principal - the process is.

Important: The sharing is what makes threads both useful and dangerous. Sharing memory means threads can communicate instantly - Thread 1 can write data to a shared variable and Thread 2 can read it immediately, without any system call or IPC mechanism. This is much faster than inter-process communication. But sharing also means that two threads can modify the same data at the same time, leading to race conditions, data corruption, and bugs that are incredibly hard to reproduce and fix.

Why Use Threads at All?

If sharing memory is dangerous, why not just use multiple processes? There are four compelling reasons.

Performance. Thread creation is much faster than process creation. Creating a process requires duplicating page tables, setting up a new address space, and creating a new PCB. Creating a thread requires allocating a new stack and a small thread control structure - the address space, page tables, and file descriptors are already there. On Linux, creating a thread takes roughly 10-50 microseconds. Creating a process takes 50-200 microseconds (even with copy-on-write). For a web server creating thousands of execution units per second, this difference matters.

Communication. Threads share memory, so they can communicate by reading and writing shared variables. No system calls needed. Inter-process communication (IPC) requires mechanisms like pipes, sockets, or shared memory segments, all of which involve system calls and kernel overhead. Thread communication is essentially free - it is just a memory read or write.

Resource efficiency. Multiple threads within a process share the same code, heap, and file descriptors. If you spawn 100 processes to handle 100 concurrent requests, each process has its own copy of the program's code and data structures (even with COW, the page tables are duplicated). If you spawn 100 threads within one process, they all share one copy of everything except their stacks.

Responsiveness. In a user-facing application, threads let you keep the UI responsive while doing heavy work in the background. A single-threaded desktop application freezes when it does a long computation or waits for a network response. A multi-threaded application can keep the UI thread running while worker threads handle the heavy lifting.

graph LR
    subgraph MULTI_PROCESS["Multi-Process Approach"]
        P1["Process 1<br/>────<br/>Own address space<br/>Own page tables<br/>Own file table"]
        P2["Process 2<br/>────<br/>Own address space<br/>Own page tables<br/>Own file table"]
        P3["Process 3<br/>────<br/>Own address space<br/>Own page tables<br/>Own file table"]
    end
    
    subgraph MULTI_THREAD["Multi-Thread Approach"]
        subgraph PROC["Single Process"]
            T1T["Thread 1<br/>────<br/>Own stack only"]
            T2T["Thread 2<br/>────<br/>Own stack only"]
            T3T["Thread 3<br/>────<br/>Own stack only"]
            SH["Shared: address space,<br/>page tables, file table"]
        end
    end
This diagram compares multi-process and multi-thread approaches side by side. In the multi-process approach, each of three processes has its own address space, page tables, and file table - everything is duplicated. In the multi-thread approach, three threads live within a single process, each with only its own stack. They share the address space, page tables, and file table. This shows why threads are more resource-efficient.

The Danger of Sharing: A Preview

The shared memory that makes threads efficient also makes them dangerous. Here is a simple example that previews what can go wrong - Module 05 covers this in depth, but you need to see the risk now to understand why thread safety matters.

Imagine two threads both trying to increment a shared counter:

# Shared variable
counter = 0

# Thread 1 does:
counter = counter + 1

# Thread 2 does:
counter = counter + 1

# Expected result: counter = 2
# Actual result: counter might be 1!

How can incrementing a counter twice give you 1 instead of 2? Because counter = counter + 1 is not a single atomic operation. At the CPU level, it is three steps: read the current value of counter from memory into a register, add 1 to the register value, and write the register value back to memory.

If Thread 1 reads the value (0), then Thread 2 reads the value (0) before Thread 1 writes back, both threads compute 0 + 1 = 1, and both write 1 back to memory. The final value is 1, not 2. One increment was lost.

This is called a race condition, and it is one of the most important concepts in concurrent programming. Preventing race conditions requires synchronization - mechanisms like mutexes, semaphores, and atomic operations that ensure only one thread modifies shared data at a time. Module 05 is entirely about this topic.

Interview: The interviewer asks: "What is a thread?"

"A thread is a lightweight unit of execution within a process. Each thread has its own program counter, registers, and stack, but all threads within a process share the same address space - including the heap, global variables, code, and open file descriptors. This sharing makes thread creation faster and inter-thread communication cheaper than inter-process communication, but it also introduces the risk of race conditions when threads access shared data concurrently without proper synchronization."

This definition covers the three things interviewers care about: what a thread has (its own PC, registers, stack), what it shares (address space, heap, files), and the consequence of sharing (race condition risk).

How the OS Manages Threads

The OS creates and manages threads using mechanisms similar to processes. On Linux, threads are created using the clone() system call - the same call used for process creation, but with flags that tell the kernel to share the parent's address space instead of copying it. In fact, Linux does not distinguish between processes and threads at the kernel level - both are represented by task_struct structures. The only difference is which resources they share.

When the scheduler runs, it schedules threads, not processes. If a process has four threads, the scheduler treats each thread as a separate schedulable entity. On a machine with four CPU cores, all four threads could run simultaneously on different cores - this is true parallelism.

Each thread has a lightweight version of the PCB called a Thread Control Block (TCB). The TCB contains the thread's program counter, register values, stack pointer, state (running, ready, waiting), and scheduling information. The TCB is much smaller than a full PCB because it does not need to duplicate memory management or file information - those belong to the process, not the thread.

Tip: For the interview: On Linux, pthread_create() is the standard API for creating threads. Under the hood, it calls clone() with the CLONE_VM flag, which tells the kernel to share the virtual memory of the parent. Without CLONE_VM, clone() creates a new process (like fork()). With CLONE_VM, it creates a thread. This detail shows the interviewer that you understand threads and processes are fundamentally the same mechanism with different sharing settings.

Threads in Real Systems

Threads are not an abstract concept - they are how real systems handle concurrency. Here are a few examples you can reference in interviews:

Web servers. Apache httpd creates a thread (or process) for each incoming request. Tomcat uses a thread pool of 200 threads (by default) to handle concurrent requests. Each thread processes one HTTP request at a time - reading the request, executing the application logic, writing the response. Nginx takes a different approach, using an event-driven model with a small number of worker threads that each handle thousands of connections using I/O multiplexing. The choice between these models is a core system design decision, and understanding the threading tradeoffs behind each one is valuable in interviews.

Databases. PostgreSQL uses a process-per-connection model (each client connection gets its own process). MySQL uses a thread-per-connection model (each client connection gets its own thread within a single process). The choice affects memory usage, isolation, and performance characteristics. PostgreSQL's process model provides stronger isolation - a crash in one connection does not affect others. MySQL's thread model uses less memory per connection because threads share the address space. Both are valid designs with different tradeoffs.

Mobile applications. iOS and Android require that UI updates happen on the main thread. Long-running work (network requests, database queries, image processing) must happen on background threads to keep the UI responsive. If you block the main thread for more than a few seconds, the OS may kill your app for being unresponsive. Understanding this threading constraint is essential for anyone building mobile applications and comes up directly in mobile system design interviews.

JVM applications. Java, Kotlin, and Scala applications run inside the JVM, which manages threads using OS-level threads (one JVM thread maps to one OS thread). The JVM's garbage collector runs on its own threads, concurrent with your application threads. Understanding how these threads interact - particularly how garbage collection pauses affect all application threads - is important for JVM-based system design. This is why low-latency Java applications often tune GC settings carefully: garbage collection that stops all application threads (a "stop-the-world" pause) can cause latency spikes that violate service-level objectives.

Operating system internals. The OS kernel itself is multithreaded. The scheduler, memory manager, file system, and device drivers all run as threads within the kernel. When your program makes a system call, the kernel handles it on a kernel thread. On Linux, kernel threads are visible in the process list - they show up with names like [kworker], [ksoftirqd], and [kswapd]. These are the OS's own worker threads that handle background tasks like deferred work processing, software interrupt handling, and memory page reclamation. Understanding that the kernel itself is multithreaded rounds out your mental model of how threads work at every level of the system.

Exercise: Starter: Open your computer's activity monitor or task manager. Find a process that has multiple threads (most browsers and IDEs do). How many threads does it have? What might each thread be doing?

Intermediate: Write a program in Python or Java that creates two threads. Thread 1 prints "Hello from Thread 1" ten times. Thread 2 prints "Hello from Thread 2" ten times. Run the program multiple times. Is the order of the output the same each time? Why or why not?

Advanced: Write a program that creates two threads that both increment a shared counter 100,000 times. Print the final counter value. Run it 10 times. Is the result always 200,000? If not, you have just observed a race condition. What would you need to do to fix it? (Module 05 covers the answer, but try to reason about it now.)

Back to Operating Systems