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.