Skip to main content
Operating Systems

Process Synchronization

Published: 2026-08-15
Level: undergraduate
Audience: Undergraduate students studying operating systems and process synchronization

Prerequisite Knowledge

This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.

Previously Covered in This Subject

  • Multiprogramming and multiprocessing — covered in Lecture 1 (multiprogramming and time-sharing systems)
  • Independent and cooperating processes — covered in Lecture 3 (the IPC models)
  • Shared memory and the bounded-buffer problem — covered in Lecture 3 (shared memory and the bounded buffer problem)
  • Concurrency versus parallel execution — covered in Lecture 4 (parallelism vs concurrency)
  • Round robin scheduling and the time quantum — covered in Lecture 6 (time quantum and context switching)
  • Priority scheduling, starvation, and aging — covered in Lecture 6 (priority scheduling — starvation and aging)

7.1 Why Process Synchronization?

7.1.1 The Basic Problem: n Processes, One Resource

Hook: Why does a whole queue of people line up for a single ticket counter, even though the counter is idle most of the time? Because the ticket seller can serve exactly one person at a time — and the same is true for the CPU and for almost every resource inside a computer.

Suppose processes are running, and all of them are trying to compete for the same resource. Only one process can access the resource at a time; every other process has to wait. It is quite possible for more than one process to be waiting — the waiters are not limited to one. Think of a printer shared by a lab full of students: many students may have queued their print jobs, but the printer prints one page at a time. Process synchronization is what we use to synchronize the access of resources by any number of processes, so that the competition is managed instead of chaotic.

Process synchronization means arranging the access of shared resources so that, when processes compete for one resource, exactly one of them gets it at any instant and the others wait in an orderly way. Without this arrangement, the competition is unmanaged — and unmanaged competition produces wrong results, as we will see in the echo example below.

Why bother at all? Because we always want the CPU to stay busy. Maximum utilization of the CPU, improving the efficiency of the system, is the constant goal. The first tool we have for that goal is scheduling, which we covered in the previous session: if scheduling is done properly, the CPU stays well used. Scheduling decides which process runs next; synchronization decides what the running processes are allowed to do at the same time. Scheduling alone is not enough — a well-scheduled system can still corrupt its own data if two processes collide on a shared variable. That is where this session begins.

7.1.2 Recap: Scheduling and the Optimal Time Quantum

We saw many scheduling algorithms, in this order: first-come-first-serve (FCFS), then shortest job first or shortest remaining time first (we can call it either name, depending on whether we use the non-preemptive or preemptive version of the algorithm), followed by priority scheduling — this is still a "shortest job" idea in spirit, but access to the CPU is decided by priority rather than length. The last algorithm is round robin, which is the combination of FCFS plus a time quantum.

In round robin, the process that comes is placed automatically in the ready queue. From the ready queue we take the process to the CPU for execution, and the execution takes place only for a particular time quantum — also called the time slice, whichever name you prefer. The time slice may be three milliseconds or four milliseconds. When the slice runs out, the CPU is taken away from the process and handed to the next one in the ready queue, which runs for its own slice.

The size of the time quantum matters a lot. If the time quantum increases, a single process itself will take more time, which makes the other processes wait for a long time; that should not happen. If the time quantum is very small — suppose we say the time quantum is only one millisecond — then a job cannot execute for more than one millisecond, so every time it is taken out of the CPU and the next process comes in. Many context switches take place, and the overhead involved in the context switching becomes more than the time spent executing a process. That is the reason the time quantum should always be optimal: large enough that one process does not hog the CPU, small enough that context switching overhead does not dominate.

A context switch is the work the operating system does to save one process's state (its registers, program counter, and so on) and load the next process's state. It is pure overhead — no useful instructions run during a switch. A one-millisecond quantum spent mostly switching means the CPU's time is going to the switch, not to the processes. This is the same trade-off the scheduler faces everywhere: sharing the CPU costs a little bit of the CPU itself.

Exam note: The interplay between time quantum size and context switching overhead is a favorite question topic — too large a quantum starves the others, too small a quantum drowns the system in context switches. In an exam, name both failure modes: a big quantum lets one process monopolize the CPU, and a tiny quantum makes switching overhead dominate useful execution.

7.1.3 Multiprogramming, Multiprocessing, and Concurrent Execution

We always have multiprogramming and multiprocessing. Multiprogramming means: how many programs are present in the memory. Multiprocessing means: how many processes are in execution. If we have to perform multiprogramming, multiprocessing, or multitasking — whatever it is — we need to see how to achieve it in a uniprocessor system or in a multiprocessor system, and that is exactly what synchronization is about: in what way can we synchronize a larger number of processes, and at the same time perform operations concurrently?

Concurrent execution (many tasks making progress at the same time, from the system's point of view) must not be confused with parallel execution (many tasks literally running at the same instant). The difference is decided by the hardware underneath:

In a uniprocessor system there is only one processor, so all the processes get interleaved: the first process starts, completes after a particular point of time, the second process starts and ends after its time period, then maybe the first process starts again — whichever process is free, or whichever comes according to the scheduling policy, comes for execution. It looks as if all processes are executing simultaneously, but they are not; a uniprocessor can execute only one process at a time. The time taken to switch is so fast that the illusion of simultaneity is convincing.

In a multiprocessor system, by contrast, the processes are overlapped. Why? Because we have more than one processor. A multiprocessor environment means a single chip with multiple cores, and each core can have many hardware threads; each thread is capable of executing a particular process. So processes are overlapped across the cores: when one is in execution, before it completes, the second will start and execute; similarly, before the second completes, the first may start again. But remember, these executions all happen on different processors — that is the difference. By overlapping we achieve concurrent execution, and that is the advantage of the multiprocessor system.

Picture both cases on a time axis, with the horizontal axis measured in milliseconds and one row per process:

  • Uniprocessor (interleaved): process A runs, then process B runs, then process C, then back to A — each row is a sequence of non-overlapping blocks. At every single instant exactly one block is active. The rows never overlap because there is only one CPU to fill.
  • Multiprocessor (overlapped): the blocks in the A, B, and C rows do overlap in time, because each row has its own processor (or hardware thread). Two processes genuinely execute in the same instant, on different cores.

The one-sentence takeaway from the picture: interleaving fakes simultaneity on one CPU; overlapping delivers real simultaneity on many. This distinction decides how hard synchronization is — on a single CPU a simple trick like disabling interrupts can protect shared data, while on multiple CPUs the same trick no longer works, as we will see in Section 7.5.

Real-world: every modern CPU chip is exactly this structure — multiple cores, each with multiple hardware threads — and the OS lets processes run concurrently across them.

But there is a disadvantage too. Interleaving and overlapping automatically improve efficiency, yet we cannot predict the results if we do not control things properly. If we are not controlling the processes, or not synchronizing them — synchronizing means controlling the time or the rate of arrival of processes and the usage of the system by processes — then the result may be unpredictable. Efficiency and correctness pull in opposite directions here: the more processes we overlap, the more chances they have to collide, and the more carefully their access must be synchronized.

7.1.4 The Echo Example: Shared Code with Unpredictable Results

Here is a concrete sample to make the problem visible. Consider a small procedure, an echo program, with two variables: in and out. We take the value for in from the keyboard and assign it to out, then display out. Two processes, P1 and P2, share the same code — both processes execute this same program.

Worked example — the echo program. The shared code reads:

read in;          /* take a character from the keyboard into in */
... some lines ...                                  /* four or five other instructions */
out = in;         /* copy the input character into out */
print out;        /* display the character that was copied */

P1 and P2 both run this code, and both share the same two variables in and out. Suppose P1 comes first, and its keyboard input is the character A. P1 reads A into in, then executes the four or five lines between the read and the copy. While P1 is busy with those lines, P2 also starts: P2 reads its own input — say C — into the same in variable, overwriting A.

Event in holds out holds Displayed
P1 reads A into in A (old)
P2 reads C into in C (old)
P1 executes out = in C C
P1 prints out C C C
P2 executes out = in; prints C C C

P1 meant to echo its own A, but it actually echoes C — P2's character. The first time we run the system the last displayed value may be A, the next time it may be C; there is no consistency. We cannot predict the actual value of out at the end.

Sense-check: the code itself is perfectly correct when run alone — run it once, and it echoes exactly what was typed. The corruption appears only because the timing of two processes overlaps: P1's read and P2's read happen at different times, but they land in the same variable, so P2's write silently cancels P1's. The bug lives in the sharing, not in the code.

This is the problem when two or more processes try to run the same code or share the same variable. It is not a matter of code alone — it may be a variable, a line, or a whole program. Whenever the same program or variable is going to be shared between processes, we need synchronization. Notice how the failure mode is timing: neither process "misbehaves", but the order in which their instructions interleave decides the result. This is the defining signature of a race condition, which we formalize in Section 7.3.

7.1.5 Where the Unpredictability Comes From

Why do we get unpredictable results? Three reasons were laid out.

The three sources of unpredictability:

  1. Unknown speed of execution. We do not know the speed of execution of the processes. Some processes execute fast and produce results within a fraction of a second — even a millisecond. A process can be preempted after any single instruction, so the interleaving between two processes is not fixed in advance.
  1. Very limited resources. The number of resources available is very limited. There is one printer, one file, one buffer — not one per process. Processes must take turns, and turns cannot be handed out to everyone at once.
  1. Shared, partly non-shareable resources. The resources are shared among various processes, and some resources are unshareable — non-shareable resources. A non-shareable resource simply cannot be given to two processes at the same instant, no matter how fast they run.

Because of these three facts, the outcome of an unmanaged system is a function of timing: the same two processes, run twice, can produce two different results. Synchronization exists to remove timing from the outcome — the result should depend on the program logic, never on the accident of when each process happened to run.

7.1.6 Shareable and Non-shareable Resources

A resource can be shared by more than one process at a time, or it cannot. The class was asked for real-time examples. The discussion is worth keeping exactly, because it corrects a common mistake about memory.

Q: What are some examples of shareable and non-shareable resources? Printers come to mind as non-shareable; one student proposes that memory is non-shareable too. Is memory really non-shareable?

A: A printer is non-shareable — it cannot be used by two people at the same time. Memory, however, is shareable: after a point of time, the memory is released from a program and allocated to some other program, so the same memory gets used by more than one process. The CPU is shareable too — more than one process competes for the CPU, and it is shared among them. I/O devices such as the keyboard, mouse, and monitor are non-shareable: each and every system has its own, and we cannot use one device with two or three systems at a time — we have to plug it in or plug it out and attach it to a particular system. System files are also non-shareable: they must be present in each and every system, so they cannot be shared among systems.

The memory correction is the important one. It seems natural to call memory non-shareable because two processes cannot own the same bytes at the same time — but that is exactly what sharing means at the OS level: the same physical memory is given to one program, then released when the program ends and given to another. Sequential reuse is still sharing. A printer, by contrast, cannot be reused this way at an arbitrary moment — a printing job is a continuous, exclusive act, and a single physical device serves one system (or one queued job) at a time.

Exam note: A question asking for examples of shareable versus non-shareable resources is a very likely short-answer item — memory and CPU are shareable; printers, I/O devices, and system files are not.

Where this shows up in the real world: a lab printer queue is a daily example of a non-shareable resource — two students cannot use the same printer at the same instant, and the spooler (the OS software that queues print jobs) is doing exactly the synchronization this lecture is about. Meanwhile, the memory of the same lab machine is silently shared by every running program: the browser's memory is reused by the editor after the browser closes, and the CPU cycles between all of them thousands of times per second.

Recap + bridge: Process synchronization manages the competition of processes for one resource so the CPU stays busy and results stay predictable. The danger is real — shared variables, like in and out in the echo program, can be corrupted by mere timing. Next, we look at the two kinds of processes that can collide — cooperating and independent — and the three problems their competition creates: deadlock, mutual exclusion, and starvation.

7.2 Cooperating Processes and the Problems of Concurrency

7.2.1 Independent Processes vs Cooperating Processes

Hook: Two chefs in one kitchen, each cooking a different dish, share the same stove. Neither chef plans to meet the other — yet if both reach for the same pan at the same moment, each dish changes. Processes behave exactly like this: independent by design, but coupled by the hardware they share.

Processes come in two kinds. Cooperating processes are the processes which are affected by other processes. If a process is going to change the value of a variable, and another process also uses the same variable — suppose variable A is used by both P1 and P2 — then these two are cooperating processes: whatever value is changed by either of the processes, both are affected. The word "cooperating" does not mean they deliberately work together; it means their behavior is coupled. Anything one of them writes to the shared variable is visible to the other, whether either of them wants that or not.

Independent processes are not affected by others. They have their own memory, their own variables, their own registers. When it comes to process synchronization, we always think about the cooperating processes. Independent processes can be scheduled in any order without any risk, because there is nothing shared between them — no variable, no buffer, no device — so their interleaving cannot change their results.

Dimension Independent processes Cooperating processes
Memory Each has its own Shared regions exist
Variables / registers Private to each process At least one variable is shared
Affected by others? No Yes — writes by one change what the other reads
Scheduling Any order is safe Order matters; interleaving can corrupt results
Role in this lecture Out of scope The target of process synchronization

When to pick which framing: when you are asked whether synchronization is needed, first ask whether any resource is shared. If yes — even one variable — the processes are cooperating, and synchronization is required.

One subtle point: the processes will never know the existence of the other processes. Many processes will be there in the middle, each will not know about the others. The only thing they know is that they are going to share the resources and compete for them. This is what makes the problem hard: there is no friendly handshake, no mutual awareness — each process simply issues instructions, and the system must keep those instructions from colliding. The synchronization mechanism is the only thing that knows about the whole crowd.

7.2.2 Competing for Resources: Deadlock, Mutual Exclusion, and Starvation

When processes compete for a resource, there are chances for any of three outcomes: deadlock, mutual exclusion, and starvation. These are the three classic problems of concurrency, and they will return repeatedly through this session and the next.

Deadlock happens when two or more processes are waiting for a particular resource which is currently used by some other process. Example: P1, P2, P3 are all waiting for resource A, but A is currently used by P4. All three have to wait until P4 releases A. All the other processes are deadlocked — they cannot do any useful work because they are waiting for A to be released by P4. After the release, anyone can use it — P1 or P2 or P3 — and which one wins depends on the type of synchronization we follow.

Notice what makes this picture a deadlock rather than an ordinary wait: the waiting processes cannot advance by themselves. Each one's progress depends on P4 — and P4 is not waiting for any of them, so once P4 finishes, the system unblocks. The blockage is temporary. (A stricter deadlock, which we will meet with semaphores in Section 7.6 and study fully in a later lecture, is a circle: every waiting process is blocked on a resource held by another waiting process, so nobody can ever release anything.) The essential lesson now: competition can park processes indefinitely if the system does not regulate who gets the resource and when.

Mutual exclusion means that at that time only one process can use the resource, while the other processes have to wait. It is not itself a failure — it is a requirement for correctness: if two processes updated the same file simultaneously, the file would end up containing a mixture of both writes. What the lecture flags is that mutual exclusion, once enforced, is exactly what makes the waiters wait: they are excluded, one at a time, by the process currently inside.

Starvation means that if a particular process is using the resource for a long time, then all the other processes have to wait indefinitely. Starvation should not occur. The difference between deadlock and starvation is who is stuck: in deadlock, the waiting processes can never proceed because the resource-holder can never proceed (or is a different one of them); in starvation, the holder is proceeding — it is just never finishing, or the scheduler keeps choosing others, so the waiters never get a turn. A starving process is not blocked forever by a rule; it is indefinitely postponed by events.

Why these three matter together: mutual exclusion is the tool, and deadlock and starvation are the side effects of using it badly. If we do not enforce mutual exclusion, data gets corrupted. If we enforce it without a fair policy for choosing the next process, someone starves; if processes hold resources and wait for each other, everyone deadlocks. Any synchronization scheme we design in the rest of this lecture must answer three questions at once: Who is inside? Who is next? Is the next chosen within a bounded time?

So these cooperating or competing processes can cause problems — and the problems occur when the processes try to execute concurrently. For that, we have to do some synchronization.

7.2.3 The Definition of Process Synchronization

Process synchronization means sharing the system resources by the processes in such a way that the concurrent access to the shared data is handled, thereby minimizing the chance of inconsistency. That is the main thing.

If many processes try to access a particular data item, the value that is going to be stored in that variable is not going to be consistent. Recall the echo program from Section 7.1: the final value of out changed depending on timing. Process synchronization exists precisely to make that value consistent — whichever process writes last, the reader should see one well-defined value, and every process should agree on what that value is. If we want to minimize this inconsistency, we go for process synchronization.

The phrase "minimizing the chance of inconsistency" is deliberate: synchronization does not make processes friends, and it does not make them faster. It makes their access to shared data orderly, so that the outcome no longer depends on the accident of interleaving. In the next section we build the precise vocabulary for doing this — the critical section, the producer–consumer problem, and the race condition — which are the vocabulary in which every solution in this session is expressed.

Recap + bridge: Cooperating processes share data and so affect each other, while independent processes cannot collide. Competition between cooperating processes creates the three problems of concurrency — deadlock, mutual exclusion, and starvation — and process synchronization is the answer that keeps shared data consistent. Next, we look at exactly where inconsistency is born: the critical section and the race condition.

7.3 The Critical Section, the Producer–Consumer Problem, and the Race Condition

7.3.1 The Critical Section

Hook: Two people updating the same whiteboard counter — one adds 1, one subtracts 1 — usually end up with a wrong total. The whiteboard has no memory of who is mid-edit. This lecture's problem is exactly that, happening thousands of times per second inside the operating system.

Before any solution, we need the terminology. A critical section is a part of the code — a code segment — where the shared variable or shared resources are accessed and executed. When a process executes that particular code segment, the execution takes place as an atomic action: atomic means either all of it will be completed fully, or nothing will be done. If P1 has taken the resource and is going to use this code segment, the code segment will be completed fully; only then will P1 release it. Otherwise no process will use it. So whichever process uses the critical section uses it fully, or not at all. At any point of time, only one process must be executing in the critical section.

Atomic (from the Greek for "not cuttable") is the master word of this session: an atomic operation is one the system cannot slice in half. It runs completely, or not at all — no other process's instructions can be interleaved into its middle. The critical section is atomic by design: a process enters it, executes it all, and only then lets anyone else in.

Remember this — it is very, very important. We need to define which part of the code is the critical section, and we should not imagine that the critical section is some common place that P1 and P2 both access. The critical section is a code which is going to be shared, but that does not mean it is one common place; each and every process has its own critical section.

Q: Is the critical section a common place, a single shared region that P1 and P2 both access?

A: No — you should not imagine that. Each and every process will have its own critical section. The problem arises when each process tries to access its critical section: then we have to enforce mutual exclusion. When one process is using the critical section, no other process can use its critical section until the process which is using it releases it. Just keep this in mind.

The point of the correction is subtle but examinable. There is not one physical "critical section room" that processes enter. Each process carries its own copy of the same code; P1's critical section is P1's instructions, P2's critical section is P2's instructions. What the processes share is the data those instructions touch. Mutual exclusion then means: while P1 is inside its own critical section, P2 must not enter its own — because the two code segments would be operating on the same shared data at the same time. One rule, two copies of code, one shared resource.

7.3.2 The Producer–Consumer Problem

The classic example is the producer–consumer problem. The producer is the one which produces items, and it keeps on producing items until the buffer size is full. If the buffer size is 10, only 10 items can be put into that buffer. Each time an item is produced, it is placed in the buffer and the buffer position is incremented to the next position. That is the work of the producer.

The consumer consumes items. There is a variable, the counter, which tells us, based on its value, whether we have items in the buffer or not. If the counter is zero, we do not have any item, and the consumer does nothing. If some items are there — suppose there are two items, so the counter value is two — the consumer can consume an item from the buffer. Think of the buffer like a stack: if you take an item out, only one item remains, so you have to decrement the counter as well. This process continues until the counter becomes zero. The buffer size is always limited.

The professor's stack picture is worth keeping: items enter at one end, and the counter counts how many are currently stacked inside. Pushing an item onto the stack adds 1 to the counter; popping an item removes 1. The counter is the single source of truth about the buffer — which is exactly why it must never lie, and exactly where the race condition will attack.

Worked example — producer and consumer with a buffer of size 10. Suppose the buffer currently holds 2 items, so the counter reads 2. The producer produces an item and places it in the buffer; the counter is incremented from 2 to 3, and the buffer position moves to the next free slot. The producer keeps producing until the buffer is full: after 7 more items, all 10 slots hold items and the counter reads 10. The producer now waits — no slot is free. Meanwhile the consumer consumes an item: it checks the counter (10, so items exist), takes one item out, and decrements the counter to 9. Only one item remains in the sense that the consumer must decrement for every single removal — consume 9 more items and the counter reaches 0, the buffer is empty, and now it is the consumer's turn to wait, doing nothing until the producer produces again.

Sense-check: at every moment the counter equals the true number of items in the buffer — provided the increment and decrement operations are performed correctly. The counter's honesty is the whole foundation: the producer only writes while it can, and the consumer only reads when the counter says there is something to read. If the counter ever disagrees with the buffer, one side will try to consume an empty buffer or the other will overflow a full one.

7.3.3 Race Condition: The Counter Walkthrough

Now the trouble. Where does the race condition come from? Suppose we are going to execute counter++. We cannot directly use the variable. What we do: we put the counter value in a register, increment the register, and then assign the register value back to the counter. Similarly for counter--: we place the value in a register, decrement it, and assign it back.

Why the register? For cached execution — that is the main reason. Otherwise we would have to bring the value from the memory every time and operate on it, which takes time. The CPU works on registers, not directly on memory: loading a value from memory into a register, changing the register, and storing it back is far faster than repeated memory traffic. But this speed has a price — the update is no longer a single step. What looks like one operation, counter++, is really three:

The operations, written out:

where is the producer's working register, is the consumer's working register, and the arrow means "is assigned". Each line is one machine instruction, and any two lines from the two processes can be interleaved in any order.

Now imagine the producer and the consumer execute these lines concurrently — at the same time. Take the initial counter value as 5. Follow the interleaving step by step:

Worked example — the race condition trace. Initial counter = 5. The producer runs counter++ while the consumer runs counter--, interleaved instruction by instruction:

Step Producer Consumer counter
1 register₁ ← counter → 5 5
2 register₁ ← register₁ + 1 → 6 5
3 register₂ ← counter → 5 5
4 register₂ ← register₂ − 1 → 4 5
5 counter ← register₁ → 6 6
6 counter ← register₂ → 4 4

Final result: counter = 4. One produce + one consume should return the counter to 5, but the final value is 4 — one item has vanished.

Where the value got lost: in step 3 the consumer reads the counter as 5, because at that moment the producer's value 6 is still sitting in the register — it has not been assigned permanently to the counter. Only later does the producer write 6 (step 5), and then the consumer writes 4 (step 6), which overwrites the 6. The final value depends on which write lands last, and here we can already see that the data are inconsistent. The counter value should be consistent: let it be 6 or let it be 4, but for both the consumer and the producer the value should be the same — it should not be 6 for one and 4 for the other.

Sense-check: one increment and one decrement must net zero. Starting at 5, the only correct outcomes are 5 (if the two updates serialize cleanly) — but the interleaving produced 4, and a different interleaving would produce 6. Both are wrong; neither equals 5.

This type of situation — executing in a way that leads to inconsistent data — is the race condition. Once the data become inconsistent, both processes cannot execute correctly. The name comes from the race between the two updates: whichever one finishes last wins the value, and the winner is decided by timing alone, never by logic. The race is conditioned on the interleaving — that is why the failure is called a race condition.

7.3.4 The Critical Section Problem: Entry, Exit, and Remainder

How do we prevent the race condition? By using the critical section — we solve the critical section problem. The code which was given earlier, the section that both processes run, is the critical section. Suppose we have number of processes, from to . Each process will have a critical section — that is the key point. The producer has this code, the consumer has this code: the producer tries to increment the counter, and before it assigns the value to the counter, the consumer starts and does its work — that is where the race condition comes from. The section of code present in each and every process is called the critical section. The process in the critical section may be changing a variable, updating a table, or writing a file — any of these counts.

When a process is in the critical section, no other process should be in its critical section. That is the problem we have to solve: we have to design a protocol. Whenever a process wants to enter the critical section, it has to ask permission to enter — the entry section. After completing the critical section execution, it has to exit that section — the exit section. Then the remainder sections follow.

The anatomy of a process that uses a critical section:

entry section       ← ask permission to enter the critical section
critical section    ← use the shared variable / resource (atomic)
exit section        ← announce that the critical section is free
remainder section   ← do all the work that touches nothing shared

Picture the structure: if P1 and P2 are there, P1 will try to enter. In the entry section a condition is checked: if no other process is using the critical section, P1 is allowed to enter and use it. Later it has to exit it, then the remainder sections are carried out. When it exits, the next process can check the condition and enter. The remainder section is deliberately outside the synchronization machinery: it touches no shared data, so processes may run it simultaneously and in any order — the exclusion applies only to the critical section itself.

This is especially challenging for the preemptive type of kernels. In real-time programming it will be very, very challenging. A preemptive kernel lets the scheduler pull a process off the CPU at any moment — including in the middle of its critical section. If the preempted process was holding a shared resource, every other process that wants that resource is stuck until the kernel reschedules the holder. In real-time systems, where deadlines are absolute, such an interruption can miss a deadline entirely — which is why real-time kernels use the priority mechanisms we meet in Section 7.6.

7.3.5 The Three Requirements: Mutual Exclusion, Progress, Bounded Waiting

In order to solve the critical section problem — that is, to synchronize the processes — we have three requirements, and any solution must achieve all three. Any protocol for entry and exit sections is judged against these three; a solution that fails any of them is not a solution.

  1. Mutual exclusion. If a process is executing in the critical section, no other process can be executing in its critical section. Very, very important. This is the requirement that stops the race condition: while the producer is updating the counter inside its critical section, the consumer's update must wait.
  1. Progress. If no process is executing in the critical section and there exist some processes that wish to enter the critical section, then the selection of the process that will enter next cannot be postponed indefinitely. Concretely: if P1, P2, P3 all wish to enter, any one of these three processes can enter — it is not that P1 has to wait while only P2 or only P3 goes. The entry cannot be postponed indefinitely; progress will take place. Progress is about the group: as long as some process that wants in is allowed in, the system is not frozen.
  1. Bounded waiting. There is a bound — how many times the processes are allowed to enter the critical section after a process has made a request to enter and before the request is granted. We do not know the relative speed of the processes; we have to assume that the processes execute at some non-zero speed. We can assume, say, that after two or three milliseconds P1 completes and then P2 or P3 can come — but we cannot claim one fixed speed for every process. Bounded waiting puts a limit on how long a particular process has to wait, or how many times other processes may enter after the request was made and before it is granted.

The three requirements guard three different failures. Mutual exclusion guards correctness — without it, the counter corrupts. Progress guards liveliness of the system — without it, everyone starves together. Bounded waiting guards fairness to the individual — without it, a specific process can be skipped forever while the system keeps moving, which is exactly the starvation of Section 7.2. Note the hidden assumption in requirement 3: processes run at some non-zero speed. The requirements do not assume a fixed speed, only that no process is frozen — a process must eventually finish its critical section so the bound can actually be honored.

If all three are satisfied by any approach, then we can say this particular approach is suitable for process synchronization. Many approaches exist — hardware-based approaches and software-based approaches — and all three requirements must be satisfied by whichever one we use. The rest of this session is a tour of such approaches: Peterson's solution (software), test-and-set and swap (hardware), and semaphores (the tool built on top).

Pitfalls to avoid with the critical section:

  • Thinking the critical section is one shared room: each process has its own copy of the code; the exclusion is about the shared data, not a shared location.
  • Treating counter++ as a single step: it is three instructions (load, increment, store), and interleaving those three lines is precisely what creates the race.
  • Protecting only the critical section and leaving the entry/exit logic sloppy: the protocol around the section is where mutual exclusion, progress, and bounded waiting actually live.
  • Forgetting that a solution must pass all three requirements: meeting mutual exclusion alone (for example, disabling interrupts) can still starve a process, and the professor will ask you to say which requirements each approach satisfies — and why.

Recap + bridge: The critical section is the code that touches shared data, executed atomically; the producer–consumer problem gives it a concrete shape (the counter and the buffer), and the race condition shows the exact interleaving that corrupts the counter. Solving the critical section problem means building entry/exit protocols that satisfy mutual exclusion, progress, and bounded waiting. Next we see the first such solution — Peterson's, written entirely in software.

7.4 Peterson's Solution: The Software Approach

7.4.1 The Two Shared Variables: Turn and Flag

Hook: Two roommates sharing one kitchen: each can go in only after signaling "I want in" and politely giving the other the right of way. Peterson's solution is that politeness, written in nothing but ordinary variables — the first critical-section solution that needs no special help from the machine at all.

Peterson's solution is a two-process solution — it is applicable only if we have two processes. It assumes two atomic instructions, load and store: if we start a load or a store instruction, it will not be interrupted in any way; either it will be completed fully, or it will not be done. In other words, reading a variable into a register and writing a variable back to memory are each indivisible. That is the only assumption the algorithm needs — everything else is built from ordinary software.

Purpose. Peterson's solution solves the critical-section problem — entry and exit sections that satisfy mutual exclusion, progress, and bounded waiting — using only shared variables, with no special instructions from the machine. It is the classic software-based approach, and it works for exactly two processes.

Inputs. Two processes, and . They share two data items:

  • turn — a variable saying whose turn it is: if I say turn = 1, it means process 1 is about to enter the critical section — the turn is for that process. If turn = 0, the turn is for P0.
  • flag[2] — an array of two flags, one per process, each 0 or 1. If flag[i] is set true, process is ready to enter the critical section.

For a process , the symbol always means the other process: . So when , , and vice versa.

Output. A protocol where at most one process is inside its critical section at a time, neither process is ever postponed indefinitely, and each process enters within a bounded number of the other's entries.

The semantics to remember, and the difference you should understand between turn and flag: if flag[0] is set true, process 0 is ready to enter the critical section; if flag[1] is set true, process 1 is ready. Whichever process is ready, the turn points to the other: if flag[0] = true, the turn will be 1 — the next turn is for P1 to enter; if flag[1] = true, the turn will be 0. The turn should be alternating; only then can one process execute in the critical section at a time and mutual exclusion be achieved.

Note on flag semantics (reconciled): the session's spoken wording — "if the flag is set as zero, that particular process is ready to enter" — inverts the usual meaning. The standard reading, confirmed by the reference treatment, is flag true = ready to enter: flag[i] = true means wants to enter the critical section. Keep that direction: true is the raised hand, false is the lowered hand.

The two variables play different roles, and the difference matters on the exam. flag is intent: it says "I want in". turn is tie-breaking: when both want in at the same instant, turn decides which one actually goes. A process can set its own flag, and it can set turn — but it cannot set the other's flag. Each process can only lower its own raised hand.

7.4.2 Walking Through the Entry Condition

Start with . P0 sets flag[0] = true, and since , it sets turn = 1. Now the entry point of the critical section checks the condition:

do {
    flag[i] = true;
    turn = j;
    while (flag[j] && turn == j)   /* spin while the other is ready and it is its turn */
        ;                          /* wait */
    /* critical section */
    flag[i] = false;
    /* remainder section */
} while (true);

Read the code in order. First P0 raises its own hand (flag[0] = true) — "I want in". Then it gives the turn to the other (turn = 1) — "after me, you may go". Then it checks the entry condition; the spin-wait is the whole entry section. Written as math, the entry condition is:

where is the other process, means "and", and means "not". So P0 spins only when both halves hold at once: the other process is ready and the turn belongs to it. As soon as either half becomes false — the other lowered its hand, or the turn is P0's — the conjunction is false and P0 walks in. After the critical section, P0 lowers its own hand in the exit section (flag[0] = false) and then runs its remainder section.

Note on the while-condition (reconciled): the verbal walkthrough in the session states the condition's polarity in both directions at different moments. The code is the authority, and it matches the standard treatment: spin while is true AND turn equals ; enter when the conjunction becomes false. The empty body with the semicolon is intentional — the process stays in place, repeatedly re-evaluating the condition, until it becomes false.

Worked example — P0 enters, P1 waits, P1 enters. Two processes, P0 (, ) and P1. Suppose P1 is not interested yet, so flag[1] = false.

  1. P0 executes flag[0] = true — hand raised.
  2. P0 executes turn = 1 — it yields the turn to P1.
  3. P0 evaluates the entry condition . With flag[1] = false, the whole conjunction is false, so P0 leaves the waiting loop and enters the critical section.
  4. P0 runs the critical section and the exit section: flag[0] = false — hand lowered.
  5. Now P1 (which wanted to enter) evaluates its own condition: . flag[0] is false, so P1 enters immediately.

Now suppose instead both want in at the same time: P0 sets flag[0] = true and turn = 1; P1 sets flag[1] = true and turn = 0. Both flags are true, but only one turn assignment survived — say turn = 0 (P1's write came last). Then P1's condition is , which is true, so P1 spins; P0's condition is , false, so P0 enters. When P0 finishes, it lowers its flag; P1's condition becomes false and P1 enters.

Sense-check: in every interleaving, the tie-break goes to whoever lost the race on turn, and the loser waits for the winner to exit — so the two can never be inside at the same time, and whichever one waits is guaranteed the section next.

If the entry condition is true — meaning the other process is ready and it is its turn — the waiting loop keeps spinning (note the semicolon: the process remains in the same place) until flag[i] becomes false, which happens only after the other process finishes its critical section. Spinning here costs CPU cycles, but with only two processes and a short critical section, the wait is brief; later we will see why busy waiting must generally be avoided (Section 7.6.3).

7.4.3 Checking the Three Requirements

All three requirements hold for Peterson's solution.

Mutual exclusion is present. Suppose P0 has entered the critical section. The turn value is 1 (P0 set it), and flag[1] — with P0 inside — is false, or if P1 also wants in, the turn makes the condition false for P1. P1 cannot enter while P0 is in execution: here itself the condition becomes false for P1. When P0 completes, P1 can come; and if P0 wants to execute again after that, it has to wait in turn. The argument in one line: to enter, a process needs the other's flag false or the turn to be its own; when one process is inside, the other can satisfy neither.

Progress is satisfied. As soon as P0 completes and makes its flag false, immediately P1 can enter the critical section. The entry is not postponed indefinitely. If both are waiting at the entry at once, the turn variable has a definite value, so exactly one of the two enters; the section is never left idle while a process wants it.

Bounded waiting is met. There is a bound — a limit on how many processes are allowed to enter the critical section after the request is made and before it is granted. With two processes the bound is one, so bounded waiting is satisfied: once P1 requests entry, P0 can enter at most once more (the very entry that is in progress) before P1's turn is granted, because the moment P0 exits it must lower its flag and P1's condition fails.

All three conditions are satisfied, so Peterson's solution should be the best solution — but it is a software solution, and it is applicable only for two processes.

7.4.4 The Limitation: Two Processes Only

When Peterson's solution applies — and when it breaks:

  • It applies to exactly two processes. The whole scheme is built on "the other process is "; with a third process there is no single "other", and the alternation argument collapses.
  • It assumes atomic load and store. On modern CPU architectures, instructions are reordered and memory writes are buffered, so this assumption may not hold — the reference text notes that Peterson's solution is not guaranteed to work on modern hardware. The algorithm remains the canonical software solution for teaching and for two-process embedded systems with well-behaved memory.
  • Beyond two processes, and wherever the hardware cases are very critical or complicated for the developers of the application programs to handle, we have to go for a different type of synchronization approach — hardware instructions. First we check whether it is a uniprocessor or a multiprocessor system: the answer decides which hardware approach can work.

Recap + bridge: Peterson's solution proves that mutual exclusion, progress, and bounded waiting can be achieved in pure software — with two flags and one turn — but only for two processes and only under an atomicity assumption modern hardware may not honor. So the next step is to ask the hardware for help: atomic instructions that the CPU guarantees, starting with disabling interrupts, then test-and-set and swap.

7.5 Hardware-Based Solutions

7.5.1 Disabling Interrupts: Uniprocessor vs Multiprocessor

Hook: The simplest way to stop someone from interrupting your story is to lock the door. For a single CPU, the OS can literally do this — but in a room full of CPUs, locking one door is pointless.

In a uniprocessor system, if an interrupt occurs, we can disable the interrupt — the interrupt can be a software-based interrupt or a hardware-based interrupt — and this can be handled efficiently. Why does disabling interrupts protect a critical section? Because on a single CPU, the only way another process can run is by an interrupt or a trap that hands control to the scheduler. If interrupts are switched off, nothing can preempt the running process, so its entry–critical–exit sequence runs as one uninterrupted unit. Nonpreemptive kernels use exactly this approach for their short internal critical sections.

But handling interrupts in a multiprocessor system is generally not efficient. Imagine P1, P2, P3 — three processors, not processes. We do not know when the interrupt will occur in each and every processor. At the time the interrupts occur, we have to disable all the interrupts immediately, and that takes some time — there is an overhead. Disabling the interrupts in each and every processor is very inefficient, and the system is not scalable either: we are just talking about three processors; imagine five processors or ten processors and what would happen.

Why interrupt disabling fails at scale: on a multiprocessor, a process on processor 1 can enter its critical section while a process on processor 2 runs into its critical section for the same data — disabling interrupts only silences processors 3, 4, 5, and so on, but not the other one running right now. To make it work, the kernel must broadcast an interrupt-disable message to every processor, wait for all acknowledgements, and later reverse the whole process. That message-passing round trip delays every critical-section entry, and the delay grows with the number of processors — which is why the approach is "not scalable". Disabling interrupts is a single-CPU trick; multi-CPU systems need hardware that makes one instruction atomic on its own, so that no broadcast is needed.

So we go for some other type of approach: hardware instructions, which are again atomic instructions. The instructions are test-and-set and swap.

7.5.2 The Test-and-Set Instruction

Purpose. Test-and-set provides a hardware-guaranteed atomic "read the old value AND set the value to true" in one step — the building block of a lock. The lock is a Boolean variable; a process acquires the lock to enter the critical section, and releases it when it comes out.

Inputs & Outputs. Input: the address of the lock variable. Output: the old value of the lock. The instruction is a function:

where *target is the value stored at the passed address, (the return variable) is the old value of the lock, and means "is assigned".

Test-and-set works with the help of a lock. If a processor has acquired the lock, it can execute the test-and-set instruction, and using that, the critical section can be achieved: the processor can enter the critical section and use it, and later it can make the lock false and come out. We acquire the lock in order to enter the critical section; once the critical section is completed, we release the lock; then the remainder section is executed. The remainder section is not a problem, because it is not critical and it is not shareable.

The lock is initialized to false. We pass the address of the lock to the entry section — the entry section holds the hardware-based test-and-set instruction. Only the address is passed, not the value; even if something changes, when we pass the address it is reflected in the return value. Passing the address matters: the instruction must modify the original lock in memory, not a copy, so that every processor sees the same lock. Whatever the original value was, at the end the lock is true — because we are testing and setting it as true. The usage pattern:

while (TestAndSet(&lock))    /* spin while the lock is held */
    ;                        /* wait */
/* critical section */
lock = false;                /* release the lock */
/* remainder section */

Note on the entry polarity (reconciled): the session says "while the condition is true we can enter" in one place and "if this is false it cannot enter" in another. The code and the standard reference agree: spin while TestAndSet(&lock) returns true — the lock was already held — and enter the critical section when it returns false. The polarity of the return value is the whole mechanism: true means "busy", false means "free".

Worked example — three processes, one lock. Lock starts false.

  • P0 arrives first. P0 executes TestAndSet(&lock): it saves the old value into rv (rv = false), sets lock = true, and returns false. The while condition is false, so P0 enters the critical section and does its work.
  • P1 arrives while P0 is inside. P1 executes TestAndSet(&lock): rv = true (the lock was already true), the lock stays true, and the return is true. The while condition is true, so P1 spins in place, re-executing the instruction again and again.
  • P2 arrives while P0 is still inside. Same story as P1: rv = true, so P2 spins too.
  • P0 finishes. P0 executes lock = false — the release. The next time P1's spinning TestAndSet runs, it sees lock = false, returns false, and P1 enters. P2 keeps spinning until P1 releases.

Sense-check: at every instant at most one process is inside the critical section (only the process whose TestAndSet returned false), and the lock is always set true again by the first contender after a release — no process can slip in while another is inside.

7.5.3 The Swap Instruction

If test-and-set alone is not enough for the hardware-based architectures, we can go for the swap instruction.

Purpose. Swap achieves the same lock by a different atomic primitive: it exchanges the contents of two variables in one indivisible step. Each process keeps a local key and repeatedly swaps it with the shared lock until the key comes back false.

Inputs & Outputs. Inputs: the addresses of two variables, lock and key. Output: the two variables' values exchanged. The key is a local boolean variable initialized to true; the lock is initialized to false. We pass the addresses of lock and key; whatever value is present at those addresses is taken — here true and false — and since it is a swap instruction, the two values get swapped. Because we pass the addresses, the values in the variables are actually changed: the key becomes false (the old lock value) and the lock becomes true. The usage pattern:

do {
    key = true;
    while (key == true)        /* spin while key is still true */
        swap(&lock, &key);
    /* critical section */
    lock = false;              /* release the lock */
    /* remainder section */
} while (true);

If after the swap the key is false, the condition is false, the process comes out of the spin and enters the critical section. Then it executes the critical section, and afterwards the lock is set to false — only when the lock is set to false can any other process compete for the critical section and execute. Then it can execute its remainder section.

Worked example — the swap walkthrough. Shared lock = false; P0's local key = true.

  • P0 executes key = true and checks while (key == true) — yes, so it executes swap(&lock, &key): the value at the lock's address (false) moves into key, and the value at the key's address (true) moves into lock. Now key = false, lock = true.
  • P0 re-checks while (key == true)key is false now, so P0 leaves the spin and enters the critical section.
  • P1 arrives with its own key = true and spins: every swap hands P1 the current lock value. While P0 is inside, the lock is true, so P1's swaps keep returning true to its key, and it keeps spinning.
  • P0 finishes the critical section and sets lock = false. P1's next swap takes the false value into its key, its loop condition fails, and P1 enters.

Sense-check: the swap is fair in exactly the same way test-and-set is: the first process to pull a false key is the one that enters, and once inside, the lock carries the value true, so every other process's swap returns true until the holder releases.

We can use both test-and-set and swap, or test-and-set alone — but we have to check whether all three requirements of the critical section problem are solved.

7.5.4 Bounded Waiting with Test-and-Set

Using test-and-set, let us see how the bounded waiting problem is solved. Mutual exclusion is solved anyway: at a time only one process will be in the critical section — if P0 is in the critical section, P1 has to wait; if P1 is in the critical section, P0 has to wait. Progress is also achieved: whenever the lock is free, the first spinning process to win the instruction race enters, so the section is never idle while someone wants it. The remaining question is bounded waiting.

The plain test-and-set loop has no fairness: a slow or unlucky process could spin forever while others keep winning the race — that is starvation. To guarantee a bound, the algorithm adds a waiting array, one entry per process, and replaces blind racing with a polite hand-off. For that we need an array — a waiting array, one entry per process:

do {
    waiting[i] = true;
    key = true;
    while (waiting[i] && key)
        key = TestAndSet(&lock);
    waiting[i] = false;

    /* critical section */

    j = (i + 1) % n;
    while ((j != i) && !waiting[j])
        j = (j + 1) % n;
    if (j == i)
        lock = false;
    else
        waiting[j] = false;

    /* remainder section */
} while (true);

The idea in words: a process only races for the lock while it is actually waiting (waiting[i] true). Once it wins (key becomes false), it clears its waiting flag and enters. On exit, it does not just drop the lock for anyone — it scans the waiting array in cyclic order and hand-picks the next waiting process, granting it the turn; if nobody is waiting, it releases the lock directly.

Walk it through with processes and . We set waiting[0] = true and key = true; both conditions are true, so we enter the inner line, execute TestAndSet — whatever the lock was, true or false, at last the returned value is going to be true, so key = true — and then waiting[0] = false. Since waiting[i] is now false, process 0 need not wait; it can enter the critical section.

Worked example — the exit scan with n = 4, P0 leaving. Processes 0–3 share lock and waiting[4]. P0 has just finished its critical section and executes the exit section:

  • . Check the loop condition: (1 ≠ 0) and !waiting[1] — if process 1 is not waiting, the negation is true, so the scan advances: .
  • Check again: , and if waiting[2] is true (process 2 is waiting), then !waiting[2] is false and the loop stops. .
  • Since (2 ≠ 0), P0 executes waiting[2] = false — it grants process 2 the turn. Process 2's while (waiting[i] && key) loop now sees waiting[2] false and process 2 enters the critical section without ever racing for the lock.
  • If instead no process had been waiting at all, the scan would walk the full circle back to (), and P0 would execute lock = false, releasing the lock for future arrivals.

Sense-check: exactly one next entrant is chosen — the first waiting process in cyclic order — and the hand-off is direct, so no process races ahead of an older waiter. The scan visits at most entries, so the grant happens within other entries after a request.

Note on the scan (reconciled): the session's walkthrough says "we put waiting of two is equal to false" while describing the scan. The standard interpretation, confirmed by the reference code, is exactly that: once the scan finds a waiting process — here waiting[2] — it grants that process the turn by setting waiting[j] = false, which lets the designated process enter. The scan is a polite selection, not a race.

This is how long a particular process can wait after it has made the request and before the request is granted — that is exactly what the bounded waiting requirement checks, and here the bound is entries in the array. So test-and-set and swap, used together, give us hardware-based process synchronization.

Recap + bridge: Disabling interrupts works only on a single CPU; multiprocessors need atomic instructions instead. Test-and-set and swap both implement the lock, and the waiting-array version of test-and-set adds bounded waiting with a bound of . The hardware works — but the instructions are complicated for application programmers to manage by hand, which is why the next step wraps them in a simple tool: the semaphore.

7.6 Semaphores

7.6.1 What a Semaphore Is

Hook: A traffic light does not stop and start each driver individually — it encodes the rule "one direction at a time" in a single signal that everyone reads. The semaphore is exactly that for processes: one integer that encodes the whole synchronization rule.

Even the hardware instructions leave us with too many variables to manage. Test-and-set needs a lock, swap needs a lock and a key, and bounded waiting needs a whole waiting array — every programmer would have to hand-build these every time. So what we can do is have a synchronization tool — a simple solution for process synchronization. That tool is the semaphore.

Purpose. The semaphore is a synchronization tool: one integer variable that wraps the entry/exit protocol into two standard operations, so application programmers never touch hardware instructions again. The semaphore is an integer variable — call it . (The letter is just a convention; it does not mean we have to use .) It is capable of performing two operations: wait and signal. These two operations modify the value of , and both operations are atomic.

The wait operation — the entry section:

We check the value of : whether it is less than or equal to zero. If this condition is true, we do a no-operation — the process which executes this wait(S) has to wait to enter the critical section; it checks the condition and waits. After that, is reduced: if it was 1, it will become 0.

The signal operation — the exit section:

Signal just increments the value of — it gives a signal for the next process to enter the critical section. We use the semaphore as a synchronization tool.

The mental model: think of as the number of free "passes" for the resource. A process that wants to enter must first take a pass (wait: if no pass is free — — it stands aside), and a process that leaves returns a pass (signal). With initialized to 1, there is exactly one pass, so only one process can be inside — mutual exclusion, with no explicit lock variable at all.

Two points to keep firmly in mind. First, each and every process must implement both operations — it should not be the wait operation alone; both wait and signal have to be implemented in every process. That is very important. A process that only waits but never signals steals a pass forever and permanently shrinks the semaphore; a process that only signals manufactures passes out of thin air. The pair is a contract: every entry is paid for by some exit.

Second, we must guarantee that no two processes can execute wait and signal at the same time. If a process is executing wait(S), the other processes should not execute the wait on the same semaphore variable. Example: if is a semaphore variable and P1 executes wait(P), P2 cannot do the same thing at the same time. So we place wait and signal both in the critical section; some process will be executing it, or waiting to enter the critical section — either way, the operations are protected. In other words, the semaphore's own implementation is protected by the very same mutual exclusion it provides to applications — the wait/signal code itself is short, so protecting it costs almost nothing.

7.6.2 Counting and Binary Semaphores

There are two types of semaphores: counting semaphore and binary semaphore.

A counting semaphore is an integer value which can range over any unrestricted value — there is no restricted value; it can hold any integer value. A binary semaphore can take only two values: either 0 or 1.

When to use which. The binary semaphore is very easy to use: with only 0 and 1, it behaves exactly like the lock we built with test-and-set — the value 1 means "free, one pass available" and 0 means "taken". Many systems call binary semaphores mutex locks for exactly this reason: they provide mutual exclusion. The counting semaphore is the general version: initialize to the number of identical resources available (say 5 printers), and each wait(S) books one of them while each signal(S) returns one — when reaches 0, all resources are busy and further waiters block until a signal raises the count again.

Note (reconciled): the session's phrase "we can implement the counting semaphore" is read here as: the binary semaphore is the easier building block, so when counting logic is needed, it is natural to build on top of it — and conversely, a binary semaphore is just a counting semaphore restricted to the values 0 and 1.

Binary semaphore Counting semaphore
Value range Only 0 or 1 Any integer
Use Mutual exclusion (a mutex lock) Controlling a pool of identical resources
Initial value 1 (free) or 0 (taken) The number of available resources
Complexity Simple General

7.6.3 Avoiding Busy Waiting: Block and Wakeup

What is busy waiting? If some process is executing in the critical section and some other process is waiting for a long time, or executing a spin lock — spinning in the same place instead of going into the critical section and executing it — that is busy waiting. We have to always avoid busy waiting. A spinning process burns CPU cycles doing nothing but re-checking a condition — on a single CPU, it even steals cycles from the very process it is waiting for, which can stretch the wait indefinitely.

What we can do instead: each semaphore implements a waiting queue with two data items — value and pointer. The value is an integer; the pointer is an address that points to the next record present in the list. The queue supports two operations: block and wakeup.

The block operation blocks the process from entering the critical section and puts it in the waiting queue. If P1, P2, P3 are three processes and P1 is executing in the critical section, then when P2 and P3 come they will be blocked — placed in the queue. The wakeup operation removes one of the processes present in the queue — whichever came first, P2 or P3 — and places it in the ready queue so it can go for execution. Blocked processes consume no CPU: they are asleep on the queue, not spinning, and the scheduler simply does not pick them.

We implement wait and signal in terms of the value and the address:

The no-busy-waiting mechanism, step by step. The semaphore variable is a common variable with these two entries, value and pointer. When a process requests to enter the critical section, the value of the semaphore is decremented; if the value becomes less than zero, the process is added to the list and executes the block operation. On the signal side, the value is incremented; whenever this value is less than or equal to zero, a process is removed from the list, the wakeup operation is invoked, and the next process is allowed to enter the critical section and gets executed. This is how the no-busy-waiting implementation works.

A useful consequence of this design: if the semaphore value is negative, its magnitude tells you how many processes are waiting. With , two processes are asleep on the queue. The first signal raises the value to and wakes one of them; a second signal raises it to 0 and wakes the other; a third signal would make it 1 — a free pass again.

7.6.4 Deadlock with Semaphores

Because of this, there are chances for deadlock and starvation — as we said earlier. Deadlock happens when two or more processes are waiting indefinitely for an event that can be caused only by one of the waiting processes.

Worked example — deadlock between two processes. Take two processes, P0 and P1, and two semaphore variables, and , both initialized to 1. Both processes can access both semaphores — but remember, both processes cannot execute the wait operation on the same variable at the same time. The interleaving:

  1. P0 executes wait(S) goes from 1 to 0, and P0 holds the S-pass.
  2. P1 executes wait(Q) goes from 1 to 0, and P1 holds the Q-pass.
  3. P0 executes wait(Q) is already 0, so P0 blocks, waiting for P1 to signal(Q).
  4. P1 executes wait(S) is already 0, so P1 blocks, waiting for P0 to signal(S).

Now both have executed their waits: P0 waits for P1 to release Q — P0 can enter the critical section only when the signal comes from P1 in the form of signal(Q). Similarly, P1 has executed wait(S) and waits for P0 to execute signal(S); only when P0 executes signal(S) can P1 enter the critical section. P0 is waiting for signal(Q), and P1 is waiting for signal(S) — and each of those signals can be produced only by the process that is waiting for the other. Both are struck; neither can perform any of the operations, because both have executed waits on each other, and each wait has made the other process wait to enter the critical section.

Sense-check: the state is a perfect circle — P0 holds S and needs Q; P1 holds Q and needs S. Every signal that could break the circle comes from inside the circle. No process can finish, so no process can ever release — deadlock, permanent.

The general shape to recognize: deadlock is a circular dependency between resources. Section 7.2's example was a temporary wait (everyone waits on P4, who is outside the waiters); this semaphore example is the permanent kind (each waiter waits on another waiter). In a later lecture we study the four conditions that create such circles and the strategies to break them.

7.6.5 Starvation, Priority Inversion, and Priority Inheritance

Starvation with semaphores: sometimes a process may enter into the critical section and stay long in the same process. In that case we have to suspend it and remove it. If the same process is using the critical section for a long time, that is starvation. We can solve this starvation problem. On the semaphore side, the risk is built into the waiting list: if blocked processes are removed from the list in LIFO order (last in, first out), a process that arrived early can be overtaken forever — an indefinite wait. A FIFO queue avoids that, which is why the queue order is not a detail but a fairness decision.

Which process will take a long time? The most important starvation scenario is the one caused by priorities. Suppose we have priority-based processes: P0 has the lowest priority, call it L; P1 has medium priority; P2 has the highest priority, call it H. Suppose P0, the low-priority process, is currently holding the critical section. If P1 comes — P1 is runnable, which means it is ready to enter the critical section — then, since it is priority-based scheduling, P0 has to go out of the CPU. But what happens here is priority inversion: a low-priority process ends up blocking a higher-priority one.

Worked example — priority inversion and its fix. Priorities: P0 = L (low), P1 = M (medium), P2 = H (high). P0 is inside the critical section, holding it.

  1. P1 (medium) becomes runnable. Under priority scheduling, P1 preempts P0 — P0 is pulled off the CPU even though it still holds the critical section.
  2. P2 (high) becomes runnable and wants the critical section. P2 finds the section held by P0 — but P0 is not running; P1 is. P2, the highest priority process, waits on the lowest priority process, while a medium priority process keeps the CPU. That is the inversion: H ends up blocked behind L, with M in the way. The wait duration depends on M's behavior, not on L — it can stretch arbitrarily, which is why this is called unbounded inversion.

The fix — priority inheritance. The low-priority process inherits the higher priority from the higher-priority process that is waiting on it: L becomes H. Since it inherits the higher priority, it will execute and will not relinquish the CPU as long as it is executed. After it completes, P0 automatically comes out of the CPU. Now — this is the subtle part — at this time we have two processes, P1 and P2. The next one to enter will not be P1; it will be only P2, because P2 has the highest priority here and it is priority-based scheduling. This is called the priority inheritance protocol, and by this we can solve the starvation problem.

Sense-check: with inheritance, P0 runs to completion without interference (its priority is now H, above M), releases the critical section, and its priority drops back to L. Only then does the scheduler choose the next process — and the next is P2 (H), exactly the process that was waiting. M does not cut in, because M was never part of the wait.

Real-world: priority inheritance is exactly the mechanism real-time operating systems use to stop low-priority tasks from indefinitely blocking high-priority ones. The best-known case is the NASA Mars Pathfinder mission in 1997: shortly after the Sojourner rover started operating, a high-priority task was repeatedly blocked by a low-priority task holding a shared resource while medium-priority tasks preempted it — a textbook priority inversion that caused the spacecraft's computer to keep resetting. The fix was switching on VxWorks' priority inheritance flag, and the mission proceeded normally.

7.6.6 Why Semaphores Come Last

Putting it all together: the software-based solution, Peterson's solution, works only for two processes. So we went for hardware-based solutions using the test-and-set and swap instructions. But since those are based on hardware, they were very complicated for application programmers. We need some other tool which is very simple and effective — so we go for the semaphore.

The whole arc in one picture: Peterson's solution (software, 2 processes only) → test-and-set and swap (hardware, all processes, but fiddly) → semaphore (one integer, two atomic operations, busy-waiting removed by block/wakeup queues). The semaphore is the tool that application programmers actually use — it sits on top of the hardware instructions, so the complexity is paid once, inside the operating system, instead of in every application.

We know the different types of semaphores. The semaphore executes only two operations: wait and signal. In the wait operation the value of the semaphore variable is decremented; in the signal operation the value of the semaphore variable is incremented. Whenever a process executes the signal operation, it means it has come out of the critical section — the signal is in the exit section — and that allows the next process waiting in the queue to enter the critical section. By this we confirm that all the requirements are satisfied: mutual exclusion, progress, and bounded waiting — bounded waiting meaning there is a bound or limit on how many number of processes can enter after making a request and before the request is granted.

Exam note: Semaphore questions cover the wait and signal operations (decrement / increment, both atomic), counting versus binary semaphores, and the block/wakeup queue that removes busy waiting. Also be ready to explain how the semaphore can still deadlock — the and circle — and how priority inheritance cures priority inversion.

The classical problems of synchronization — the bounded-buffer, the readers–writers problem, the dining philosophers, and the rest — which we will see in the next session, are built on top of these tools.

Exam Guidance Summary

  • The mid-semester exam is on 16th March. Everything covered up to and including the 8th contact session (the next session) is in the syllabus: the portion taken in this session and the completion of process synchronization in the next session will be there for the mid-semester. The portion taken in the 9th contact session will not be included, even though the 9th session falls before the exam date.
  • Process synchronization is a two-session topic: the requirements and solutions (software, hardware, semaphores) come in this session; the classical synchronization problems are completed in the next session. The syllabus for the mid-semester is still process synchronization. Plan your revision as one block: the tools from this session are the raw material of next session's problems.

Exam note: Expect the three requirements of the critical section problem — mutual exclusion, progress, and bounded waiting — and be able to say which approach satisfies them and why. Keep the summary table in mind:

  • Peterson's solution — software, two-process-only; satisfies all three (bounded-waiting bound of one).
  • Test-and-set and swap — hardware instructions; the plain version lacks bounded waiting; adding the waiting array gives the bound.
  • Semaphore — the simple tool built on top; wait/signal, counting versus binary, and the block/wakeup queue.

Exam note: Know shareable versus non-shareable resources — memory and CPU are shareable; printers, I/O devices (keyboard, mouse, monitor), and system files are non-shareable. A short-answer item on this is very likely, and the memory case is the classic trap.

  • Quiz correction: one quiz question about "maximum CPU utilization by using..." was found to have two possible answers (the options about maximum turnaround time and maximum response time were both undesirable), but the quiz allowed only one answer. The instructor will add the marking mask for multiple answers later; it was pointed out by a majority of students. If you marked either of those two options, the corrected marking mask will credit the question once it is added.

Key Industry Applications

  • Real-world: multiprocessor chips — a single chip with multiple cores, each core with many hardware threads — are how modern systems achieve concurrent execution; each thread is capable of executing a process. Every smartphone processor, desktop CPU, and cloud server CPU is built this way, and the operating system's scheduler and synchronization layer are what let thousands of threads share those cores safely.
  • Real-world: priority inheritance protocol is used in real-time operating systems to prevent low-priority processes from indefinitely blocking high-priority ones (the priority inversion problem). The mechanism is a standard feature of real-time kernels (for example, VxWorks' priority inheritance flag, the very fix that saved NASA's Mars Pathfinder mission in 1997), and it is equally built into the mutexes of general-purpose systems.
  • Real-world: printer sharing in a lab or office is a daily example of a non-shareable resource: two users cannot use the same printer at the same instant, while the memory and CPU of a machine are genuinely shared among many processes. The print spooler that queues jobs is the synchronization system at work, and the same pattern recurs in every exclusive device and every shared database row.
  • Real-world: semaphores are the classic synchronization tool for producers and consumers, and the no-busy-waiting queue (block and wakeup) is the model behind modern blocking synchronization in operating systems — mutexes, condition variables, and the futex primitives of Linux all trace their design to the semaphore's waiting queue. When an application thread calls a lock that "sleeps" instead of burning CPU, it is using the block/wakeup idea from this lecture.

OS Lecture 7 notes · Process Synchronization

Operating Systems· undergraduate· 2026-08-15

Sections Breakdown

17.1 Why Process Synchronization?

Why process synchronization is needed: n processes competing for one resource, the scheduling recap, interleaving versus overlapping, the echo example, and shareable versus non-shareable resources.

27.2 Cooperating Processes and the Problems of Concurrency

Cooperating and independent processes, and the three problems of concurrency: deadlock, mutual exclusion, and starvation.

37.3 The Critical Section, the Producer–Consumer Problem, and the Race Condition

The critical section, the producer–consumer problem, the race condition walkthrough, and the three requirements: mutual exclusion, progress, and bounded waiting.

47.4 Peterson's Solution: The Software Approach

Peterson's solution: the turn and flag variables, the entry condition, the three requirements, and the two-process limitation.

57.5 Hardware-Based Solutions

Hardware solutions: disabling interrupts, the test-and-set and swap instructions, and bounded waiting with the waiting array.

67.6 Semaphores

Semaphores: the wait and signal operations, counting versus binary semaphores, block and wakeup queues, deadlock, priority inversion, and priority inheritance.

7Exam Guidance Summary

Exam strategy: mid-semester syllabus boundaries, the three requirements, and the shareable versus non-shareable classification.

8Key Industry Applications

Real-world connections: multiprocessor chips, the priority inheritance protocol, printer spooling, and semaphore-based blocking synchronization.

Undergraduate students studying operating systems and process synchronization

Exam Revision Notes

Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.

Why Process Synchronization?

Must-know: Shareable resources: memory and CPU. Non-shareable: printers, I/O devices (keyboard, mouse, monitor), and system files. Optimal time quantum: large enough that one process does not hog the CPU, small enough that context-switch overhead does not dominate.

⚠️ Top pitfall: Calling memory non-shareable: memory IS shareable because it is released from one program and allocated to another; sequential reuse counts as sharing.

Self-check: Why is a one-millisecond time quantum bad for round robin? Because context-switching overhead becomes larger than the time spent executing the process.

Connects to: 7.2 Cooperating Processes and the Problems of Concurrency; 7.3 The Critical Section, the Producer–Consumer Problem, and the Race Condition

Cooperating Processes and the Problems of Concurrency

Must-know: Cooperating processes share variables and are affected by each other; independent processes have their own memory, variables, and registers. Competition creates deadlock, mutual exclusion, and starvation. Process synchronization minimizes the chance of inconsistency in shared data.

⚠️ Top pitfall: Confusing starvation with deadlock: in starvation the resource holder keeps running and the waiter is indefinitely postponed; in deadlock the waiting processes can never proceed.

Self-check: Are two processes that each use their own private variable cooperating or independent? Independent - nothing is shared, so their interleaving cannot change results.

Connects to: 7.1 Why Process Synchronization?; 7.3 The Critical Section, the Producer–Consumer Problem, and the Race Condition

The Critical Section, the Producer–Consumer Problem, and the Race Condition

Must-know: The three requirements of the critical section problem: mutual exclusion (only one process in its critical section at a time), progress (if no one is inside and some processes want in, the choice of who enters cannot be postponed indefinitely), and bounded waiting (a bound on how many times other processes may enter between a process's request and its grant).

⚠️ Top pitfall: Imagining the critical section is one common place P1 and P2 both access - each process has its own critical section; exclusion applies to the shared data, and must hold while one process is inside.

Self-check: Why does counter++ need synchronization? Because it is three instructions (load counter into a register, increment the register, store back), and interleaving with counter-- can lose an update, leaving the counter at 4 or 6 instead of 5.

Connects to: 7.1 Why Process Synchronization?; 7.2 Cooperating Processes and the Problems of Concurrency; 7.4 Peterson's Solution: The Software Approach

Peterson's Solution: The Software Approach

Must-know: Peterson's solution is a software solution for two processes only: each process sets its own flag true, sets turn = j, then spins while flag[j] && turn == j. Flag semantics: flag[i] = true means Pi is ready to enter (true = raised hand). All three requirements are satisfied; with two processes the bounded-waiting bound is one.

⚠️ Top pitfall: Inverting the flag semantics: flag true (not zero) means the process is ready to enter. Also, imagining the critical section is a common place - each process has its own critical section and its own copy of this code.

Self-check: In Peterson's solution, what does turn = j accomplish? It gives the turn to the other process, so if both want to enter at the same time, the tie is broken and only one can enter - the loser spins until the winner exits and lowers its flag.

Connects to: 7.3 The Critical Section, the Producer–Consumer Problem, and the Race Condition; 7.5 Hardware-Based Solutions

Hardware-Based Solutions

Must-know: Test-and-set: spin while TestAndSet(&lock) returns true, enter when it returns false; the lock is released with lock = false. Swap: local key = true, spin while key, swap(&lock, &key), enter when key becomes false. Disabling interrupts works only for a uniprocessor - it is inefficient and not scalable on multiprocessors. The waiting-array algorithm gives bounded waiting with bound n - 1.

⚠️ Top pitfall: Reversing the test-and-set polarity: you spin while the instruction returns true (lock held) and enter when it returns false - not the other way round.

Self-check: Why does disabling interrupts not work well on a multiprocessor? Because interrupts must be disabled on every processor via a broadcast message with acknowledgement overhead, which is slow and does not scale as processors are added.

Connects to: 7.4 Peterson's Solution: The Software Approach; 7.6 Semaphores

Semaphores

Must-know: wait(S): while S <= 0 do nothing, then S = S - 1. signal(S): S = S + 1. Both atomic; every process implements both. With a queue: wait decrements and blocks when S.value < 0; signal increments and wakes when S.value <= 0 (a negative value's magnitude is the number of waiting processes). Deadlock example: P0 waits on Q while P1 waits on S. Priority inheritance: L inherits H, runs to completion, then P2 (H) enters before P1 (M).

⚠️ Top pitfall: A process that implements only wait and not signal steals a pass forever; and two processes executing wait on the same semaphore at the same time breaks atomicity - wait and signal must themselves be protected as a critical section.

Self-check: In the queue-based implementation, what does S.value = -2 mean? Two processes are blocked on the semaphore's waiting queue; two signals (one each) raise the value to 0 and wake both.

Connects to: 7.3 The Critical Section, the Producer–Consumer Problem, and the Race Condition; 7.4 Peterson's Solution: The Software Approach; 7.5 Hardware-Based Solutions

Exam Guidance Summary

Must-know: Mid-semester on 16th March covers through the 8th session only. Three requirements of the critical section problem: mutual exclusion, progress, bounded waiting. Peterson = software, two-process; test-and-set and swap = hardware; semaphore = simple tool on top. Shareable: memory and CPU. Non-shareable: printers, I/O devices, system files.

⚠️ Top pitfall: Misremembering memory as non-shareable: memory is released and reallocated, so it is shareable; the shareable/non-shareable classification is a likely short-answer item.

Self-check: Which parts of process synchronization are inside the mid-semester syllabus? Everything through the 8th contact session - this session's solutions plus next session's classical synchronization problems; the 9th session is excluded.

Connects to: 7.1 Why Process Synchronization?; 7.3 The Critical Section, the Producer–Consumer Problem, and the Race Condition; 7.4 Peterson's Solution: The Software Approach; 7.6 Semaphores

Key Industry Applications

Must-know: Priority inheritance is used in real-time operating systems (e.g., VxWorks) to stop low-priority processes from indefinitely blocking high-priority ones; semaphore block/wakeup queues are the model behind modern blocking locks and futexes.

Self-check: What real-world incident made priority inversion famous? The NASA Mars Pathfinder (1997): a high-priority task was blocked by a low-priority task holding a shared resource while medium-priority tasks preempted it, causing computer resets until priority inheritance was enabled.

Connects to: 7.1 Why Process Synchronization?; 7.5 Hardware-Based Solutions; 7.6 Semaphores

Was this lecture useful?

Loading comments…
🤖

BitsNotes AI Assistant

Subject Notes Assistant

Configure AI Chat

Choose how to access the chatbot
Have your own API key?

Switch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.

🔑 Enter API key above to fetch live models from provider, or enter model name manually.
OpenAI-Compatible API Support

Choose any provider preset (Gemini, DeepSeek, Kimi, GLM, MiniMax, Qwen, OpenAI, Groq, Ollama, etc.) or enter a custom endpoint URL.

Security & Privacy First

Your API key is sent directly from your browser to your specified provider. BitsNotes servers never store or see your key.