Meeting Program Objectives: Sequence

Why Is the Order of Code Instructions Important?

The order of execution in a program determines its functionality.

Incorrect sequencing can lead to errors such as infinite loops, deadlocks, and incorrect outputs.

Impact of Instruction Order on Functionality

Issue Cause Example
Infinite Loop Loop condition is always true. while (True): print("Looping")
Deadlock Two processes wait for each other indefinitely. Occurs in concurrent programming.
Incorrect Output Instructions executed in the wrong order. Printing before variable assignment.

Example: Correcting Code Sequence

Incorrect

print("Value:", num)   # uses num before assignment -> NameError
num = 10               # assigned too late

Corrected

num = 10               # assign first
print("Value:", num)   # then use

Incorrect

public class IncorrectSequence {
    public static void main(String[] args) {
        System.out.println("Value: " + num); // num not initialised
        int num = 10;
    }
}

Corrected

public class CorrectSequence {
    public static void main(String[] args) {
        int num = 10; // assign first
        System.out.println("Value: " + num); // then use
    }
}

Ways to Avoid Errors

Check Loop Conditions

Before running a loop, list the expected number of iterations or exit criteria. Use trace tables or print statements to verify that the loop variable changes as intended.

i = 0
while i < 5:
    print(i)
    i += 1  # increment ensures the loop terminates
int i = 0;
while (i < 5) {
    System.out.println(i);
    i++; // increment ensures the loop terminates
}

Initialise Variables Properly

Always declare and assign initial values before using variables. Identify each variable’s purpose and set a starting value to avoid uninitialised reads.

count = 0  # initialise before use
for num in data:
    count += 1
int count = 0;  // initialise before use
for (int num : data) {
    count++;
}

Manage Concurrency & Deadlocks

Circular Wait

A circular wait happens when each thread holds one resource and waits for the next resource in a ring. No one can move because everyone is waiting on someone else.

Scenario: Two students each grab one of two shared tablets for a group task. Each needs both tablets to continue. Student A waits for Tablet 2; Student B waits for Tablet 1. Neither lets go - the task stalls.

Resource Starvation

Starvation is when a thread is repeatedly passed over and never gets the resource it needs. The system is still running, but one participant never progresses.

Scenario: A school 3D printer with strict priority rules keeps serving “urgent” jobs. A normal-priority print sits in the queue all lesson and never starts.

Self-Deadlock

Self-deadlock occurs when a thread locks a non-reentrant lock and then tries to lock it again (directly or via a function it calls). It waits on itself forever.

Scenario: You lock a classroom door, then try to lock the same door again without unlocking first - you’re stuck by your own lock.

Circular Deadlock (and Fixes)

Two threads contend for the same two locks in opposite order (deadlock), then two remedies: a global lock order and a try-lock with timeout/back-off.

# Circular deadlock with status updates (Python)
# NOTE: Intentionally deadlocks. Stop with Ctrl+C.

import threading, time

class NamedLock:
    def __init__(self, name):
        self.name = name
        self._lock = threading.Lock()
    def acquire(self, blocking=True, timeout=-1):
        return self._lock.acquire(blocking, timeout)
    def release(self):
        self._lock.release()
    def __str__(self):
        return self.name

lockA = NamedLock("Lock-A")
lockB = NamedLock("Lock-B")

def run(name, first, second):
    print(f"[{name}] requesting {first}")
    first.acquire()
    print(f"[{name}] acquired {first}")
    time.sleep(0.2)  # encourage overlap

    print(f"[{name}] requesting {second}")
    if not second.acquire(blocking=False):
        print(f"[{name}] waiting for {second} ...")
        second.acquire()  # blocks here -> deadlock
    print(f"[{name}] acquired {second} (both locks)")

t1 = threading.Thread(target=run, args=("T1", lockA, lockB), daemon=True)
t2 = threading.Thread(target=run, args=("T2", lockB, lockA), daemon=True)
t1.start(); t2.start()

# These joins will block due to deadlock
t1.join(); t2.join()
# Python — Solution 1: Global Lock Order (A -> B)
import threading, time

lockA = threading.Lock()
lockB = threading.Lock()

def do_work_ordered(name):
    print(f"[{name}] requesting Lock-A then Lock-B")
    # All threads use the same order -> no cycle
    with lockA:
        print(f"[{name}] acquired Lock-A")
        time.sleep(0.1)
        with lockB:
            print(f"[{name}] acquired Lock-B (both) — working")
            time.sleep(0.1)
    print(f"[{name}] released both locks — done")

t1 = threading.Thread(target=do_work_ordered, args=("T1",))
t2 = threading.Thread(target=do_work_ordered, args=("T2",))
t1.start(); t2.start(); t1.join(); t2.join()
# Python — Solution 2: Try-lock + timeout + backoff
import threading, time, random

lockA = threading.Lock()
lockB = threading.Lock()

def work_trylock(name, first, first_name, second, second_name):
    backoff = 0.05
    while True:
        print(f"[{name}] trying {first_name}")
        got_first = first.acquire(timeout=0.2)
        if not got_first:
            print(f"[{name}] couldn't get {first_name}; retry soon")
            time.sleep(backoff); backoff = min(0.2, backoff*2)
            continue

        print(f"[{name}] got {first_name}; trying {second_name}")
        got_second = second.acquire(timeout=0.2)
        if got_second:
            print(f"[{name}] got {second_name}; working")
            time.sleep(0.1)
            second.release(); first.release()
            print(f"[{name}] released both — done")
            break
        else:
            print(f"[{name}] timeout on {second_name}; releasing {first_name} and backing off")
            first.release()
            time.sleep(backoff + random.random()*0.05)
            backoff = min(0.3, backoff*2)

# Threads prefer opposite orders, but backoff prevents deadlock
t1 = threading.Thread(target=work_trylock, args=("T1", lockA, "Lock-A", lockB, "Lock-B"))
t2 = threading.Thread(target=work_trylock, args=("T2", lockB, "Lock-B", lockA, "Lock-A"))
t1.start(); t2.start(); t1.join(); t2.join()
// Circular deadlock with status updates (Java)
// NOTE: Intentionally deadlocks.

import java.util.concurrent.locks.ReentrantLock;

public class DeadlockStatusDemo {
    private static final ReentrantLock lockA = new ReentrantLock();
    private static final ReentrantLock lockB = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> run("T1", lockA, "Lock-A", lockB, "Lock-B"));
        Thread t2 = new Thread(() -> run("T2", lockB, "Lock-B", lockA, "Lock-A"));

        t1.start(); t2.start();
        // Blocks forever due to deadlock (comment out if needed)
        t1.join();  t2.join();
    }

    private static void run(String name, ReentrantLock first, String firstName,
                            ReentrantLock second, String secondName) {
        System.out.println("[" + name + "] requesting " + firstName);
        first.lock();
        System.out.println("[" + name + "] acquired " + firstName);

        sleep(200); // encourage overlap

        System.out.println("[" + name + "] requesting " + secondName);
        if (!second.tryLock()) {
            System.out.println("[" + name + "] waiting for " + secondName + " ...");
            second.lock(); // blocks here -> deadlock
        }
        System.out.println("[" + name + "] acquired " + secondName + " (both locks)");
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }
}
// Java — Solution 1: Global Lock Order (A -> B)
import java.util.concurrent.locks.ReentrantLock;

public class OrderedLocksDemo {
    private static final ReentrantLock lockA = new ReentrantLock();
    private static final ReentrantLock lockB = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> doWorkOrdered("T1"));
        Thread t2 = new Thread(() -> doWorkOrdered("T2"));
        t1.start(); t2.start(); t1.join(); t2.join();
    }

    static void doWorkOrdered(String name) {
        System.out.println("[" + name + "] requesting Lock-A then Lock-B");
        lockA.lock();
        try {
            System.out.println("[" + name + "] acquired Lock-A");
            sleep(100);
            lockB.lock();
            try {
                System.out.println("[" + name + "] acquired Lock-B (both) — working");
                sleep(100);
            } finally {
                lockB.unlock();
            }
        } finally {
            lockA.unlock();
            System.out.println("[" + name + "] released both locks — done");
        }
    }

    static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException ignored) {} }
}
// Java — Solution 2: tryLock(timeout) + backoff
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class TryLockBackoffDemo {
    private static final ReentrantLock lockA = new ReentrantLock();
    private static final ReentrantLock lockB = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> workTry("T1", lockA, "Lock-A", lockB, "Lock-B"));
        Thread t2 = new Thread(() -> workTry("T2", lockB, "Lock-B", lockA, "Lock-A"));
        t1.start(); t2.start(); t1.join(); t2.join();
    }

    static void workTry(String name, ReentrantLock first, String firstName,
                        ReentrantLock second, String secondName) {
        long backoffMs = 50;
        while (true) {
            System.out.println("[" + name + "] trying " + firstName);
            boolean gotFirst = false, gotSecond = false;
            try {
                gotFirst = first.tryLock(200, TimeUnit.MILLISECONDS);
                if (!gotFirst) { sleep(backoffMs); backoffMs = Math.min(200, backoffMs * 2); continue; }

                System.out.println("[" + name + "] got " + firstName + "; trying " + secondName");
                gotSecond = second.tryLock(200, TimeUnit.MILLISECONDS);
                if (gotSecond) {
                    System.out.println("[" + name + "] got " + secondName + " — working");
                    sleep(100);
                    return; // success; finally block will release
                } else {
                    System.out.println("[" + name + "] timeout on " + secondName + "; backing off");
                }
            } catch (InterruptedException ignored) {
            } finally {
                if (gotSecond) second.unlock();
                if (gotFirst)  first.unlock();
            }
            sleep(backoffMs);
            backoffMs = Math.min(300, backoffMs * 2);
        }
    }

    static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException ignored) {} }
}

 Key Takeaways

  • Instruction order directly affects program behavior and correctness.
  • Common pitfalls: infinite loops, deadlocks, and undefined variables.
  • Always initialise variables and verify loop logic to avoid errors.