Tutorial 2 — Techniques for Thread Synchronization
Duration: ~1 hour
Audience: Undergraduate students familiar with basic threading
Languages: C (POSIX Threads), optional Python demos
Environment: Linux / Windows (WSL recommended)
Learning Objectives
By the end of this tutorial, students will be able to:
- Use mutexes, condition variables, and barriers to coordinate threads.
- Identify and prevent race conditions and deadlocks.
- Understand how condition variables work internally.
- Apply synchronization techniques to real-world “smart environment” scenarios
such as smart farming, smart homes, and smart buildings.
Concept Recap
| Primitive | Purpose | Example Scenario |
|---|---|---|
| Mutex | Protect shared data | Shared bank account, temperature log |
| Condition Variable | Wait for a condition to be met | Producer–consumer, smart sensors |
| Barrier | Wait for all threads to reach a point | Periodic data aggregation |
| Atomic Ops | Fast counter / flag updates | Counting detected motion events |
Understanding Condition Variables
A condition variable lets threads wait for a condition to become true
while temporarily releasing a lock (mutex) so that other threads can change that condition.
Think of it as:
“If the fridge is empty, the consumer waits.
When the producer refills it, it notifies the consumer.”
The Three Key Operations
| Function | Description |
|---|---|
pthread_cond_wait(&cond, &mutex) |
Waits for a condition. Temporarily releases the mutex while sleeping, and reacquires it before returning. |
pthread_cond_signal(&cond) |
Wakes up one waiting thread. |
pthread_cond_broadcast(&cond) |
Wakes up all waiting threads. |
Typical Pattern
pthread_mutex_lock(&mtx);
while (!condition)
pthread_cond_wait(&cond, &mtx);
// perform work once condition true
pthread_mutex_unlock(&mtx);
Why the while loop?
Even after being signaled, a waiting thread must re-check the condition, because:
- Another thread may have modified it before this one resumed.
- Some systems allow spurious wakeups (wakeups without signals).
Hence we use:
while (condition_not_true)
pthread_cond_wait(&cond, &mutex);
Mini Example - cond_example.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int data_ready = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;
void* producer(void* arg) {
sleep(1);
pthread_mutex_lock(&m);
data_ready = 1;
printf("Producer: data is ready!\n");
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
return NULL;
}
void* consumer(void* arg) {
pthread_mutex_lock(&m);
while (!data_ready) {
printf("Consumer: waiting...\n");
pthread_cond_wait(&c, &m);
}
printf("Consumer: got the data!\n");
pthread_mutex_unlock(&m);
return NULL;
}
int main() {
pthread_t prod, cons;
pthread_create(&cons, NULL, consumer, NULL);
pthread_create(&prod, NULL, producer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
}
Build & Run
gcc -pthread -o cond_example cond_example.c
./cond_example
Output
Consumer: waiting...
Producer: data is ready!
Consumer: got the data!
✅ The consumer waits efficiently — it’s not busy-waiting.
Part 1 — Smart Farm Sensor Fusion (using a Barrier)
Scenario
A smart farm has multiple sensors (temperature, humidity, soil moisture). Each thread reads data from one sensor. After all sensors report, the system fuses the data to make an irrigation decision.
Code — smartfarm_barrier.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define NUM_SENSORS 3
typedef struct {
int id; // sensor id
} SensorArg;
pthread_barrier_t barrier;
int readings[NUM_SENSORS];
void* sensor(void* arg) {
SensorArg* a = (SensorArg*)arg;
int id = a->id;
// Simulate variable reading time
sleep(1 + id);
readings[id] = 20 + id * 5; // fake data - we could use rand here with
// To get new random readings on each run.
printf("Sensor %d reports %d C\n", id, readings[id]);
// Wait for all sensors to finish their reading
pthread_barrier_wait(&barrier);
// One thread (e.g., id 0) performs fusion/decision after the barrier
if (id == 0) {
int sum = 0;
for (int i = 0; i < NUM_SENSORS; i++) sum += readings[i];
int avg = sum / NUM_SENSORS;
printf("Fusion thread: average temp = %d C. Output: %s\n",
avg, (avg > 25 ? "ON" : "OFF"));
}
return NULL;
}
int main(void) {
pthread_t threads[NUM_SENSORS];
SensorArg args[NUM_SENSORS];
pthread_barrier_init(&barrier, NULL, NUM_SENSORS);
for (int i = 0; i < NUM_SENSORS; i++) {
args[i].id = i;
pthread_create(&threads[i], NULL, sensor, &args[i]);
}
for (int i = 0; i < NUM_SENSORS; i++) {
pthread_join(threads[i], NULL);
}
pthread_barrier_destroy(&barrier);
return 0;
}
Build & Run:
gcc -pthread -o smartfarm_barrier smartfarm_barrier.c
./smartfarm_barrier
Discussion
What happens if the barrier is removed?
Part 2 — Smart Home Producer–Consumer (using Condition Variables)
Scenario
A smart-home gateway receives sensor events (producer) and processes them (consumer). We’ll implement a simple bounded buffer using a mutex and condition variables.
Code — smart_home_pc.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
#define NUM_EVENTS 20
int buffer[BUFFER_SIZE];
int count = 0, in = 0, out = 0;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
void* producer(void* arg) {
for (int i = 0; i < NUM_EVENTS; i++) {
pthread_mutex_lock(&mtx);
while (count == BUFFER_SIZE)
pthread_cond_wait(¬_full, &mtx);
buffer[in] = i;
in = (in + 1) % BUFFER_SIZE;
count++;
printf("[Producer] Sensor event %d produced (count=%d)\n", i, count);
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&mtx);
usleep(100000);
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < NUM_EVENTS; i++) {
pthread_mutex_lock(&mtx);
while (count == 0)
pthread_cond_wait(¬_empty, &mtx);
int item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
printf(" [Consumer] processed event %d (count=%d)\n", item, count);
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&mtx);
usleep(150000);
}
return NULL;
}
int main() {
pthread_t prod, cons;
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
}
Build & Run:
gcc -pthread -o smart_home_pc smart_home_pc.c
./smart_home_pc
Output
One of the possible outputs.
PS D:\UOP\Course\lab\lab2> ./smart_home
[Producer] Sensor event 0 produced (count=1)
[Consumer] processed event 0 (count=0)
[Producer] Sensor event 1 produced (count=1)
[Consumer] processed event 1 (count=0)
[Producer] Sensor event 2 produced (count=1)
[Producer] Sensor event 3 produced (count=2)
[Consumer] processed event 2 (count=1)
[Producer] Sensor event 4 produced (count=2)
[Consumer] processed event 3 (count=1)
[Producer] Sensor event 5 produced (count=2)
[Producer] Sensor event 6 produced (count=3)
[Consumer] processed event 4 (count=2)
[Producer] Sensor event 7 produced (count=3)
[Consumer] processed event 5 (count=2)
[Producer] Sensor event 8 produced (count=3)
[Consumer] processed event 6 (count=2)
[Producer] Sensor event 9 produced (count=3)
[Producer] Sensor event 10 produced (count=4)
[Consumer] processed event 7 (count=3)
[Producer] Sensor event 11 produced (count=4)
[Consumer] processed event 8 (count=3)
[Producer] Sensor event 12 produced (count=4)
[Producer] Sensor event 13 produced (count=5)
[Consumer] processed event 9 (count=4)
[Producer] Sensor event 14 produced (count=5)
[Consumer] processed event 10 (count=4)
[Producer] Sensor event 15 produced (count=5)
[Consumer] processed event 11 (count=4)
[Producer] Sensor event 16 produced (count=5)
[Consumer] processed event 12 (count=4)
[Producer] Sensor event 17 produced (count=5)
[Consumer] processed event 13 (count=4)
[Producer] Sensor event 18 produced (count=5)
[Consumer] processed event 14 (count=4)
[Producer] Sensor event 19 produced (count=5)
[Consumer] processed event 15 (count=4)
[Consumer] processed event 16 (count=3)
[Consumer] processed event 17 (count=2)
[Consumer] processed event 18 (count=1)
[Consumer] processed event 19 (count=0)
Part 3 — Smart Greenhouse: Producer, Consumer and Monitor
Scenario
A smart greenhouse has multiple temperature sensors (producers) generating readings, actuators (consumers) adjusting conditions, and a monitor thread that periodically checks the latest temperature to detect unsafe trends.
Code — smart_greenhouse.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
// ============================ Config =============================
#define BUFFER_SIZE 5
#define NUM_READINGS 5
#define SAFE_TEMP_MIN 18
#define SAFE_TEMP_MAX 28
// ANSI colors
#define RED "\033[1;31m" // Producer (Sensor)
#define BLUE "\033[1;34m" // Actuator (Consumer)
#define GREEN "\033[1;32m" // Monitor
#define RESET "\033[0m"
// =========================== Commands ============================
typedef enum {
CMD_NONE = 0,
CMD_COOL_ON,
CMD_HEAT_ON,
CMD_STABLE,
CMD_SHUTDOWN
} command_t;
// ======================= Shared: readings buf ====================
static int buffer[BUFFER_SIZE];
static int count = 0;
static int producer_done = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
static pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
// ==================== Shared: monitor→actuator cmd ===============
static command_t cmd_mailbox = CMD_NONE;
static int cmd_has_msg = 0;
static pthread_mutex_t cmd_mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cmd_cv = PTHREAD_COND_INITIALIZER;
// ============================ Producer ===========================
static void* producer(void* arg) {
int in_idx = 0;
for (int i = 0; i < NUM_READINGS; i++) {
int temp = 15 + rand() % 15; // [15..29]
// push temp into ring buffer
pthread_mutex_lock(&mtx);
while (count == BUFFER_SIZE)
pthread_cond_wait(¬_full, &mtx);
buffer[in_idx] = temp;
in_idx = (in_idx + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&mtx);
// Print outside lock
printf(RED "[Sensor] Produced temp = %dC (count=%d)\n" RESET, temp, count);
usleep(150000); //Modifying this will change the rate of how fast are genereated
}
// mark done and wake any waiters
pthread_mutex_lock(&mtx);
producer_done = 1;
pthread_cond_broadcast(¬_empty);
pthread_mutex_unlock(&mtx);
return NULL;
}
// ============================ Monitor ============================
// Consumes readings from buffer, computes decision, signals actuator.
static void* monitor(void* arg) {
int out_idx = 0;
while (1) {
int temp = 0;
int done = 0;
// Pop one reading (or exit if empty & producer_done)
pthread_mutex_lock(&mtx);
while (count == 0 && !producer_done)
pthread_cond_wait(¬_empty, &mtx);
if (count == 0 && producer_done) {
done = 1;
} else {
temp = buffer[out_idx];
out_idx = (out_idx + 1) % BUFFER_SIZE;
count--;
pthread_cond_signal(¬_full);
}
pthread_mutex_unlock(&mtx);
if (done) break;
// Decide command based on temp
command_t cmd;
if (temp > SAFE_TEMP_MAX) cmd = CMD_COOL_ON;
else if (temp < SAFE_TEMP_MIN) cmd = CMD_HEAT_ON;
else cmd = CMD_STABLE;
// Announce decision (outside locks)
if (cmd == CMD_COOL_ON)
printf(GREEN " [Monitor] temp=%dC . COOL ON\n" RESET, temp);
else if (cmd == CMD_HEAT_ON)
printf(GREEN " [Monitor] temp=%dC . HEAT ON\n" RESET, temp);
else
printf(GREEN " [Monitor] temp=%dC . STABLE\n" RESET, temp);
// Post command to actuator (single-message mailbox)
pthread_mutex_lock(&cmd_mtx);
while (cmd_has_msg) // wait until previous command is taken
pthread_cond_wait(&cmd_cv, &cmd_mtx);
cmd_mailbox = cmd;
cmd_has_msg = 1;
pthread_cond_signal(&cmd_cv);
pthread_mutex_unlock(&cmd_mtx);
}
// Send shutdown command
pthread_mutex_lock(&cmd_mtx);
while (cmd_has_msg)
pthread_cond_wait(&cmd_cv, &cmd_mtx);
cmd_mailbox = CMD_SHUTDOWN;
cmd_has_msg = 1;
pthread_cond_signal(&cmd_cv);
pthread_mutex_unlock(&cmd_mtx);
return NULL;
}
// ============================ Actuator ===========================
// Waits for commands from Monitor and performs actions.
static void* actuator(void* arg) {
(void)arg;
while (1) {
command_t cmd;
// Wait for a command
pthread_mutex_lock(&cmd_mtx);
while (!cmd_has_msg)
pthread_cond_wait(&cmd_cv, &cmd_mtx);
cmd = cmd_mailbox;
cmd_has_msg = 0;
pthread_cond_signal(&cmd_cv); // let monitor post next
pthread_mutex_unlock(&cmd_mtx);
// Act on command
if (cmd == CMD_SHUTDOWN) {
printf(BLUE " [Actuator] Shutdown received. Exiting.\n" RESET);
break;
} else if (cmd == CMD_COOL_ON) {
printf(BLUE " [Actuator] Cooling ON\n" RESET);
} else if (cmd == CMD_HEAT_ON) {
printf(BLUE " [Actuator] Heating ON\n" RESET);
} else if (cmd == CMD_STABLE) {
printf(BLUE " [Actuator] Systems stable\n" RESET);
}
usleep(200000);
}
return NULL;
}
// ============================== Main ============================
int main(void) {
srand((unsigned)time(NULL));
pthread_t prod_th, mon_th, act_th;
pthread_create(&prod_th, NULL, producer, NULL);
pthread_create(&mon_th, NULL, monitor, NULL);
pthread_create(&act_th, NULL, actuator, NULL);
pthread_join(prod_th, NULL);
pthread_join(mon_th, NULL);
pthread_join(act_th, NULL);
printf("\nMain: All threads finished. Exiting...\n");
return 0;
}
Build & Run
gcc -pthread -o smart_greenhouse smart_greenhouse.c
./smart_greenhouse
Output
[Sensor] Produced temp = 26C (count=1)
[Monitor] temp=26C . STABLE
[Actuator] Systems stable
[Sensor] Produced temp = 17C (count=1)
[Monitor] temp=17C . HEAT ON
[Actuator] Heating ON
[Sensor] Produced temp = 19C (count=1)
[Monitor] temp=19C . STABLE
[Actuator] Systems stable
[Sensor] Produced temp = 25C (count=1)
[Monitor] temp=25C . STABLE
[Actuator] Systems stable
[Sensor] Produced temp = 29C (count=0)
[Monitor] temp=29C . COOL ON
[Actuator] Cooling ON
[Actuator] Shutdown received. Exiting.
Expected Behavior
- The sensor produces readings.
- The monitor consumes temperatures from the sensor buffer.
- The Monitor decides what to do (cool, heat, stable).
- Monitor signals the Actuator via a dedicated mailbox + condition variable, avoiding busy-waiting and minimizing lock contention.
- The actuator consumes perform the action.
Key Takeaways
- Synchronization ensures correctness but adds overhead.
- Choose the right primitive:
- 🧩 Mutex → exclusive access
- 🔄 Condition Variables → coordination between threads
- 🚧 Barriers → phase synchronization for groups of threads
- Real-world smart systems (smart homes, farms, factories) rely on these to avoid data races and inconsistent decisions.
Appendix — Python Versions of the Synchronization Experiments
Setup
# (Optional) create a virtual environment
python -m venv .venv
# Windows
Run this command before activating your venv:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.venv\Scripts\activate
# Linux/macOS
source .venv/bin/activate
# Install colorama (for colored terminal output)
pip install colorama
If you don’t want colors, remove the colorama parts and the ANSI constants.
A) Smart Farm Sensor Fusion (Barrier)
Idea: Same as the C barrier example — multiple sensors wait at a barrier, and after all arrive, one thread fuses the results.
File: smartfarm_barrier.py
import threading
import time
from random import randint
from colorama import init, Fore, Style
init(autoreset=True)
NUM_SENSORS = 3
readings = [None] * NUM_SENSORS
barrier = threading.Barrier(NUM_SENSORS)
def sensor(sensor_id: int):
time.sleep(0.5 + sensor_id * 0.4)
readings[sensor_id] = 20 + sensor_id * 5
print(f"{Fore.RED}[Sensor {sensor_id}] reports {readings[sensor_id]}C{Style.RESET_ALL}")
barrier.wait()
if sensor_id == 0:
avg = sum(readings) // NUM_SENSORS
decision = "ON" if avg > 25 else "OFF"
print(f"{Fore.GREEN}Fusion: avg={avg}C -> irrigation {decision}{Style.RESET_ALL}")
def main():
threads = [threading.Thread(target=sensor, args=(i,)) for i in range(NUM_SENSORS)]
for t in threads: t.start()
for t in threads: t.join()
if __name__ == "__main__":
main()
Run:
python smartfarm_barrier.py
B) Smart Home Producer–Consumer (Queue / Condition)
Idea: Same bounded-buffer behavior as the C version.
In Python, queue.Queue(maxsize=N) already handles the condition variable logic internally.
File: smart_home_pc.py
import time
import threading
import queue
from colorama import init, Fore, Style
init(autoreset=True)
BUFFER_SIZE = 5
NUM_EVENTS = 20
q = queue.Queue(maxsize=BUFFER_SIZE)
def producer():
for i in range(NUM_EVENTS):
q.put(i)
print(f"{Fore.RED}[Producer] Sensor event {i} produced (count={q.qsize()}){Style.RESET_ALL}")
time.sleep(0.1)
def consumer():
for _ in range(NUM_EVENTS):
item = q.get()
print(f"{Fore.BLUE} [Consumer] processed event {item} (count={q.qsize()}){Style.RESET_ALL}")
time.sleep(0.15)
q.task_done()
def main():
t_prod = threading.Thread(target=producer)
t_cons = threading.Thread(target=consumer)
t_prod.start(); t_cons.start()
t_prod.join(); q.join(); t_cons.join()
if __name__ == "__main__":
main()
Run:
python smart_home_pc.py
C) Smart Greenhouse — Monitor Consumes and Commands Actuator
Idea:
- Sensor (Producer) pushes temperatures into a readings queue.
- Monitor consumes readings, decides actions, and posts commands to a command queue.
- Actuator (Consumer) processes commands until shutdown.
File: smart_greenhouse.py
import threading
import queue
import time
from random import randint
from colorama import init, Fore, Style
init(autoreset=True)
READINGS = queue.Queue(maxsize=5)
COMMANDS = queue.Queue(maxsize=1)
NUM_READINGS = 20
SAFE_MIN = 18
SAFE_MAX = 28
CMD_COOL_ON = "COOL_ON"
CMD_HEAT_ON = "HEAT_ON"
CMD_STABLE = "STABLE"
CMD_SHUTDOWN = "SHUTDOWN"
def sensor():
for _ in range(NUM_READINGS):
temp = randint(15, 29)
READINGS.put(temp)
print(f"{Fore.RED}[Sensor] Produced temp = {temp}C (count={READINGS.qsize()}){Style.RESET_ALL}")
time.sleep(0.15)
READINGS.put(None)
def monitor():
while True:
temp = READINGS.get()
if temp is None:
COMMANDS.put(CMD_SHUTDOWN)
print(f"{Fore.GREEN} [Monitor] SHUTDOWN sent{Style.RESET_ALL}")
break
if temp > SAFE_MAX:
cmd, tag = CMD_COOL_ON, "COOL ON"
elif temp < SAFE_MIN:
cmd, tag = CMD_HEAT_ON, "HEAT ON"
else:
cmd, tag = CMD_STABLE, "STABLE"
print(f"{Fore.GREEN} [Monitor] temp={temp}C . {tag}{Style.RESET_ALL}")
COMMANDS.put(cmd)
def actuator():
while True:
cmd = COMMANDS.get()
if cmd == CMD_SHUTDOWN:
print(f"{Fore.BLUE} [Actuator] Shutdown received. Exiting.{Style.RESET_ALL}")
break
elif cmd == CMD_COOL_ON:
print(f"{Fore.BLUE} [Actuator] Cooling ON{Style.RESET_ALL}")
elif cmd == CMD_HEAT_ON:
print(f"{Fore.BLUE} [Actuator] Heating ON{Style.RESET_ALL}")
elif cmd == CMD_STABLE:
print(f"{Fore.BLUE} [Actuator] Systems stable{Style.RESET_ALL}")
time.sleep(0.2)
def main():
t_s = threading.Thread(target=sensor)
t_m = threading.Thread(target=monitor)
t_a = threading.Thread(target=actuator)
t_s.start(); t_m.start(); t_a.start()
t_s.join(); t_m.join(); t_a.join()
print("\nMain: All threads finished. Exiting...")
if __name__ == "__main__":
main()
Run:
python smart_greenhouse.py
D) Condition Variable Mini Example
Idea: Demonstrates threading.Condition with the while/wait() pattern, equivalent to the C example.
File: cond_mini_example.py
import threading
import time
from colorama import init, Fore, Style
init(autoreset=True)
data_ready = False
cond = threading.Condition()
def producer():
time.sleep(1)
with cond:
global data_ready
data_ready = True
print(f"{Fore.RED}Producer: data is ready!{Style.RESET_ALL}")
cond.notify()
def consumer():
with cond:
while not data_ready:
print(f"{Fore.BLUE}Consumer: waiting...{Style.RESET_ALL}")
cond.wait()
print(f"{Fore.BLUE}Consumer: got the data!{Style.RESET_ALL}")
def main():
t_c = threading.Thread(target=consumer)
t_p = threading.Thread(target=producer)
t_c.start(); t_p.start()
t_c.join(); t_p.join()
if __name__ == "__main__":
main()
Run:
python cond_mini_example.py
Parallels Between C and Python Implementations
| Concept | C API | Python Equivalent |
|---|---|---|
| Barrier | pthread_barrier_t |
threading.Barrier |
| Mutex | pthread_mutex_t |
threading.Lock() or Condition |
| Condition Variable | pthread_cond_t |
threading.Condition() |
| Producer–Consumer Queue | Manual + pthread_cond_wait |
queue.Queue(maxsize=N) |
| Thread | pthread_create() |
threading.Thread() |
| Sleep / Wait | sleep() / usleep() |
time.sleep() |
| Color Output | ANSI codes | colorama (cross-platform) |
✅ Key Takeaways for Python
- Threading concepts and patterns mirror those in C closely.
- Python’s
queue.Queuesimplifies synchronization for many use cases. - Condition variables (
threading.Condition) follow the samewhile+wait()rule. coloramaensures colored output works on Windows, macOS, and Linux.- Both languages share the same fundamental synchronization ideas — Python just abstracts them more.