# Lecture 12 - Locks

Source: https://wisconsin.twango.dev/sp26-cs537/lectures/lecture-12

# Lecture 12 - Locks

**Instructor:** Tej Chajed
**Topics:** Locks, mutual exclusion, atomic operations, spinlocks, test-and-set, compare-and-swap, ticket locks

> [!abstract]- Learning Outcomes
> - Compare lock implementations based on several goals
> - Explain how hardware and OS support lock implementation

---

## Why Locks?

Without locks, concurrent threads accessing shared data produce incorrect results due to race conditions. Locks ensure **mutual exclusion** — only one thread executes a critical section at a time.

### Timeline View with Locks

Consider two threads both incrementing a shared variable `*balance`:

```text
Thread 1                    Thread 2
mutex_Lock()                mutex_Lock()

mov 0x123, %eax
add $0x1, %eax
mov %eax, 0x123

mutex_unlock()
                            mov 0x123, %eax
                            add $0x2, %eax
                            mov %eax, 0x123

                            mutex_unlock()
```

How much is added to shared variable? **3: correct!**

> [!note] Lecture Note
> With locks, `mutex_Lock()` / `*balance += 1` / `mutex_Unlock()` and `mutex_Lock()` / `*balance += 2` / `mutex_Unlock()` run sequentially, producing the correct result.

---

## Lock Implementation Goals

### Correctness

- **Mutual exclusion** — only one thread in the critical section at a time
- **Progress** (deadlock-free) — if several simultaneous requests, must allow one to proceed
- **Bounded** (starvation-free) — must eventually allow each waiting thread to enter

### Fairness

Each thread waits for the same amount of time.

### Performance

CPU is not used unnecessarily.

---

## Implementing Synchronization

> **Atomic operation:** No other instructions can be interleaved.

Three approaches to implementing locks:

1. Disable interrupts
2. Locks using loads/stores
3. Using special hardware instructions

---

## Approach 1: Disable Interrupts

Turn off interrupts for critical sections:
- Prevents the dispatcher from running another thread
- Code between interrupts executes atomically

```c
void acquire(lockT *l) {
    disableInterrupts();
}

void release(lockT *l) {
    enableInterrupts();
}
```

**Disadvantages:**
- Only works on **uniprocessors** (other CPUs are unaffected by disabling local interrupts)
- Process can keep control of CPU for arbitrary length
- Cannot perform other necessary work (e.g., handling I/O)

---

## Approach 2: Locks with Load + Store

Use a single **shared** lock variable:

```c
// shared variable
boolean lock = false;

void acquire(Boolean *lock) {
    while (*lock) /* wait */ ;
    *lock = true;
}

void release(Boolean *lock) {
    *lock = false;
}
```

> [!note] Lecture Note
> `false` means free, `true` means held. On release, you could check `if (*lock == false)` and throw an error for "misuse of lock API."

### Race Condition with Load and Store

**Does this work?** No — testing the lock and setting the lock are **not atomic**.

```text
*lock == 0 initially

Thread 1              Thread 2
while(*lock == 1)
                      while(*lock == 1)
                      *lock = 1
*lock = 1
```

**Both threads grab the lock!** The problem is that the check and the set are two separate operations that can be interleaved.

---

## Atomic Hardware Instructions

To implement synchronization correctly, we need hardware support. Examples of atomic operations:

- Code between interrupts on uniprocessors (disable timer interrupts, don't do any I/O)
- **Special hardware instructions:**
  - `xchg` (exchange) — also called Test&Set
  - `CompareAndSwap` (CAS)
  - `FetchAndAdd` (FAA)

> [!note] Lecture Note
> Loads and stores of single words are atomic individually, but combining them (test-then-set) is not atomic. The special hardware instructions solve this.

---

## Test-and-Set (`xchg`)

The `xchg` instruction atomically swaps a value at an address and returns the old value:

```c
// xchg(int *addr, int newval)
// return what was pointed to by addr
// at the same time, store newval into addr
int xchg(int *addr, int newval) {
    int old = *addr;
    *addr = newval;
    return old;
}
```

In x86 assembly, this is a single instruction:

```asm
movl 4(%esp), %edx
movl 8(%esp), %eax
xchgl (%edx), %eax
ret
```

### Spinlock with `xchg`

```c
typedef struct __lock_t {
    int flag;
} lock_t;

void init(lock_t *lock) {
    lock->flag = 0;
}

void acquire(lock_t *lock) {
    while (xchg(&lock->flag, 1) == 1)
        ; // spin-wait (do nothing)
}

void release(lock_t *lock) {
    lock->flag = 0;
}
```

> [!note] Lecture Note
> `flag = 0` means free, `flag = 1` means held. When `xchg` returns 0, we know we own the lock (we atomically set it to 1 and got back 0). When `xchg` returns 1, the lock was already held — we learn we do not own it, but the value stays 1 (no harm done), so we retry.

---

## Compare-and-Swap (`CompareAndSwap`)

```c
int CompareAndSwap(int *addr, int expected, int new) {
    int actual = *addr;
    if (actual == expected) {
        *addr = new;
        return (1);  // swap happens
    }
    return 0;         // nothing happens
}
```

> [!note] Lecture Note
> If the value matches `expected`, the swap happens and returns 1. Otherwise nothing changes and it returns 0.

### Spinlock with `CompareAndSwap`

```c
void acquire(lock_t *lock) {
    while (CompareAndSwap(&lock->flag, 0, 1) == 0)
        ; // spin-wait (do nothing)
}
```

This tries to atomically change `flag` from 0 to 1. If `flag` is already 1 (held), `CompareAndSwap` returns 0 and the thread spins.

---

## Basic Spinlocks are Unfair

With a basic spinlock (using `xchg` or CAS), thread A can repeatedly acquire the lock while thread B spins and waits:

```text
Time:  0    20   40   60   80  100  120  140  160
       [A] [B]  [A]  [B]  [A] [B]  [A]  [B]
            spin      spin      spin      spin
```

- A unlocks and immediately re-locks before B can acquire
- B wastes its entire time slice spinning
- **The scheduler is unaware of locks/unlocks!**

---

## Ticket Locks (Fair Spinlock)

**Idea:** Reserve each thread's turn to use a lock. Each thread spins until their turn.

Uses a new atomic primitive, `FetchAndAdd`:

```c
int FetchAndAdd(int *ptr) {
    int old = *ptr;
    *ptr = old + 1;
    return old;
}
```

Lock state consists of two fields: `ticket` and `turn`.

- **Acquire:** Grab a ticket (via FAA); spin while thread's ticket != turn
- **Release:** Advance to next turn

> [!note] Lecture Note
> FAA gives each thread its own ticket number. Example: A calls `lock()` and gets ticket 0 (owner). B calls `lock()` and gets ticket 1. C calls `lock()` and gets ticket 2. When A calls `unlock()`, turn advances to 1, so B runs next. This guarantees FIFO ordering.

### Ticket Lock Implementation

```c
typedef struct __lock_t {
    int ticket;
    int turn;
} lock_t;

void lock_init(lock_t *lock) {
    lock->ticket = 0;
    lock->turn = 0;
}

void acquire(lock_t *lock) {
    int myturn = FetchAndAdd(&lock->ticket);
    // spin
    while (lock->turn != myturn);
}

void release(lock_t *lock) {
    FetchAndAdd(&lock->turn);
}
```

### Ticket Lock Example

```text
A lock():    gets ticket 0, spins until turn = 0 -> runs
B lock():    gets ticket 1, spins until turn = 1
C lock():    gets ticket 2, spins until turn = 2
A unlock():  turn++ (turn = 1)
B           runs
A lock():    gets ticket 3, spins until turn = 3
B unlock():  turn++ (turn = 2)
C           runs
C unlock():  turn++ (turn = 3)
A           runs
A unlock():  turn++ (turn = 4)
C lock():    gets ticket 4, runs
```

Ticket locks guarantee **bounded waiting** and **fairness** — threads are served in FIFO order.

---

## Summary: Lock Implementations Compared

| Implementation | Mutual Exclusion | Progress | Bounded Waiting | Fairness | Performance |
|----------------|:---:|:---:|:---:|:---:|:---:|
| Disable interrupts | Yes (uniprocessor only) | Yes | Yes | N/A | Poor (blocks all interrupts) |
| Load/Store spinlock | **No** | N/A | N/A | No | N/A |
| `xchg` spinlock | Yes | Yes | No | No | Wastes CPU spinning |
| CAS spinlock | Yes | Yes | No | No | Wastes CPU spinning |
| Ticket lock | Yes | Yes | Yes | Yes (FIFO) | Wastes CPU spinning |

---

## Related

- **Navigation:** [[lecture-11|← Lecture 11]] · [[lecture-13|Lecture 13 →]]
- **Exam:** [[exams/midterm-2/index|Midterm 2]]
- **Course:** [[README|CS 537 Map of Content]]
