Function Pointers + Structures = Observer Pattern in C? 5 Elegant Practical Source Code Examples

Function Pointers + Structures = Observer Pattern in C? 5 Elegant Practical Source Code Examples

Application of the Observer Pattern in C (Including Linux Kernel Examples)

1. Definition and Core Value of the Observer Pattern

The Observer Pattern is a behavioral design pattern that defines a one-to-many dependency between objects, so that when one object (the subject) changes state, all its dependents (the observers) are automatically notified and updated. This pattern decouples the subject from the observers, allowing both to evolve independently.

Significance: In scenarios requiring real-time response to state changes (such as event notifications and state synchronization), if observers directly depend on the implementation of the subject, it leads to tight coupling. Adding or removing observers would require modifying the subject’s code. The observer pattern abstracts the notification mechanism, allowing the subject to broadcast notifications without knowing the specific observers, significantly reducing coupling.

Problems Solved:

  • • Direct coupling between the subject and observers, where changes in one affect the other;
  • • Adding new observers requires modifying the subject’s notification logic, violating the “Open/Closed Principle”;
  • • In scenarios with multiple observers, the subject must manually maintain interactions with each observer, leading to redundant code.

2. Core Idea of Implementing the Observer Pattern in C

C language implements the pattern through structures to abstract the subject and observers + function pointers to define notification/update interfaces:

  1. 1. Define the observer: The structure includes a update function pointer (logic executed upon receiving a notification) and custom data;
  2. 2. Define the subject: The structure includes a list of observers (such as an array or linked list), and function pointers for attach (adding observers), detach (removing observers), and notify (notifying all observers);
  3. 3. Bind the relationship: Observers register with the subject through attach, and when the subject’s state changes, it calls notify, iterating through the observer list and triggering update.

Function Pointers + Structures = Observer Pattern in C? 5 Elegant Practical Source Code Examples

3. 5 Examples

Example 1: Basic Observer (Temperature Sensor and Alarm)

Simulate a temperature sensor (the subject) that automatically notifies an alarm and display (the observers) when its state changes.

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// Forward declaration
typedef struct Subject Subject;

// 1. Observer interface
typedef struct {
    void (*update)(struct Observer* self, int temp); // Logic executed upon receiving temperature update
    void* data; // Observer private data
} Observer;

// 2. Subject interface
typedef struct Subject {
    Observer* observers[5]; // Observer list
    int count; // Number of observers
    int current_temp; // Current temperature (the observed state)
    // Add observer
    void (*attach)(struct Subject* self, Observer* obs);
    // Remove observer
    void (*detach)(struct Subject* self, Observer* obs);
    // Notify all observers
    void (*notify)(struct Subject* self);
} Subject;

// Subject implementation: Add observer
static void subject_attach(Subject* self, Observer* obs) {
    if (self->count < 5) {
        self->observers[self->count++] = obs;
        printf("Observer attached. Total: %d\n", self->count);
    }
}

// Subject implementation: Remove observer
static void subject_detach(Subject* self, Observer* obs) {
    for (int i = 0; i < self->count; i++) {
        if (self->observers[i] == obs) {
            // Move subsequent observers to fill the gap
            for (int j = i; j < self->count-1; j++) {
                self->observers[j] = self->observers[j+1];
            }
            self->count--;
            printf("Observer detached. Total: %d\n", self->count);
            return;
        }
    }
}

// Subject implementation: Notify all observers
static void subject_notify(Subject* self) {
    printf("Notifying %d observers of temperature: %d°C\n", self->count, self->current_temp);
    for (int i = 0; i < self->count; i++) {
        self->observers[i]->update(self->observers[i], self->current_temp);
    }
}

// Create temperature sensor (the subject)
Subject* create_temp_sensor() {
    Subject* sensor = malloc(sizeof(Subject));
    sensor->count = 0;
    sensor->current_temp = 25; // Initial temperature
    sensor->attach = subject_attach;
    sensor->detach = subject_detach;
    sensor->notify = subject_notify;
    return sensor;
}

// Update sensor temperature (state change)
void temp_sensor_update(Subject* self, int new_temp) {
    self->current_temp = new_temp;
    printf("Sensor temperature updated to %d°C\n", new_temp);
    self->notify(self); // Notify observers after state change
}

// 3. Concrete observer 1: Alarm (temperature too high alarm)
static void alarm_update(Observer* self, int temp) {
    (void)self;
    if (temp > 35) {
        printf("Alarm: Temperature too high! %d°C\n", temp);
    }
}

Observer* create_alarm() {
    Observer* alarm = malloc(sizeof(Observer));
    alarm->update = alarm_update;
    alarm->data = NULL;
    return alarm;
}

// Concrete observer 2: Display (show current temperature)
static void display_update(Observer* self, int temp) {
    (void)self;
    printf("Display: Current temperature is %d°C\n", temp);
}

Observer* create_display() {
    Observer* display = malloc(sizeof(Observer));
    display->update = display_update;
    display->data = NULL;
    return display;
}

// Client usage
int main() {
    Subject* sensor = create_temp_sensor();
    Observer* alarm = create_alarm();
    Observer* display = create_display();

    // Register observers
    sensor->attach(sensor, alarm);
    sensor->attach(sensor, display);

    // Temperature change (trigger notification)
    printf("\n");
    temp_sensor_update(sensor, 30);
    printf("\n");
    temp_sensor_update(sensor, 40); // Trigger alarm

    // Remove display
    printf("\n");
    sensor->detach(sensor, display);
    temp_sensor_update(sensor, 38); // Only alarm responds

    // Free resources
    free(alarm);
    free(display);
    free(sensor);
    return 0;
}

The output of the above code is as follows

Observer attached. Total: 1
Observer attached. Total: 2

Sensor temperature updated to 30°C
Notifying 2 observers of temperature: 30°C
Display: Current temperature is 30°C

Sensor temperature updated to 40°C
Notifying 2 observers of temperature: 40°C
Alarm: Temperature too high! 40°C
Display: Current temperature is 40°C

Observer detached. Total: 1
Sensor temperature updated to 38°C
Notifying 1 observers of temperature: 38°C
Alarm: Temperature too high! 38°C

The corresponding UML diagram is as follows

Function Pointers + Structures = Observer Pattern in C? 5 Elegant Practical Source Code Examples

That is: when the state of the temperature sensor (the subject) changes, it automatically notifies all registered observers (alarm, display), and the observers execute their respective logic based on the temperature. Adding new observers (such as a logger) does not require modifying the sensor code.

Example 2: Event-Driven Observer (Key Event Dispatch)

Simulate key events (the subject), with multiple components (observers) listening and responding to different keys (such as volume keys, power keys).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Event types
typedef enum { KEY_VOLUME_UP, KEY_VOLUME_DOWN, KEY_POWER } KeyEvent;

// Forward declaration
typedef struct EventSubject EventSubject;

// 1. Observer interface (receives key events)
typedef struct {
    void (*on_event)(struct KeyObserver* self, KeyEvent event);
    const char* name; // Observer name
} KeyObserver;

// 2. Subject interface (key event source)
typedef struct EventSubject {
    KeyObserver* observers[4];
    int count;
    void (*subscribe)(struct EventSubject* self, KeyObserver* obs);
    void (*unsubscribe)(struct EventSubject* self, KeyObserver* obs);
    void (*emit_event)(struct EventSubject* self, KeyEvent event); // Trigger event
} EventSubject;

// Subject implementation: Subscribe
static void event_subscribe(EventSubject* self, KeyObserver* obs) {
    if (self->count < 4) {
        self->observers[self->count++] = obs;
        printf("Observer '%s' subscribed\n", obs->name);
    }
}

// Subject implementation: Unsubscribe
static void event_unsubscribe(EventSubject* self, KeyObserver* obs) {
    for (int i = 0; i < self->count; i++) {
        if (self->observers[i] == obs) {
            for (int j = i; j < self->count-1; j++) {
                self->observers[j] = self->observers[j+1];
            }
            self->count--;
            printf("Observer '%s' unsubscribed\n", obs->name);
            return;
        }
    }
}

// Subject implementation: Emit event and notify
static void event_emit(EventSubject* self, KeyEvent event) {
    const char* event_name[] = {"VOLUME_UP", "VOLUME_DOWN", "POWER"};
    printf("Emitting event: %s\n", event_name[event]);
    for (int i = 0; i < self->count; i++) {
        self->observers[i]->on_event(self->observers[i], event);
    }
}

// Create key event source
EventSubject* create_key_event_source() {
    EventSubject* source = malloc(sizeof(EventSubject));
    source->count = 0;
    source->subscribe = event_subscribe;
    source->unsubscribe = event_unsubscribe;
    source->emit_event = event_emit;
    return source;
}

// 3. Concrete observer 1: Volume Controller (responds to volume keys)
static void volume_on_event(KeyObserver* self, KeyEvent event) {
    switch (event) {
        case KEY_VOLUME_UP:
            printf("[%s] Volume increased\n", self->name);
            break;
        case KEY_VOLUME_DOWN:
            printf("[%s] Volume decreased\n", self->name);
            break;
        default: break; // Ignore power key
    }
}

KeyObserver* create_volume_controller() {
    KeyObserver* obs = malloc(sizeof(KeyObserver));
    obs->on_event = volume_on_event;
    obs->name = "VolumeController";
    return obs;
}

// Concrete observer 2: Power Manager (responds to power key)
static void power_on_event(KeyObserver* self, KeyEvent event) {
    if (event == KEY_POWER) {
        printf("[%s] Powering off...\n", self->name);
    }
}

KeyObserver* create_power_manager() {
    KeyObserver* obs = malloc(sizeof(KeyObserver));
    obs->on_event = power_on_event;
    obs->name = "PowerManager";
    return obs;
}

// Client usage
int main() {
    EventSubject* key_source = create_key_event_source();
    KeyObserver* volume = create_volume_controller();
    KeyObserver* power = create_power_manager();

    // Subscribe to events
    key_source->subscribe(key_source, volume);
    key_source->subscribe(key_source, power);

    // Simulate key events
    printf("\n");
    key_source->emit_event(key_source, KEY_VOLUME_UP);
    printf("\n");
    key_source->emit_event(key_source, KEY_POWER);
    printf("\n");
    key_source->emit_event(key_source, KEY_VOLUME_DOWN);

    // Unsubscribe volume controller
    printf("\n");
    key_source->unsubscribe(key_source, volume);
    key_source->emit_event(key_source, KEY_VOLUME_UP); // No response

    // Free resources
    free(volume);
    free(power);
    free(key_source);
    return 0;
}

The output of the above code is as follows

Observer 'VolumeController' subscribed
Observer 'PowerManager' subscribed

Emitting event: VOLUME_UP
[VolumeController] Volume increased

Emitting event: POWER
[PowerManager] Powering off...

Emitting event: VOLUME_DOWN
[VolumeController] Volume decreased

Observer 'VolumeController' unsubscribed
Emitting event: VOLUME_UP

The corresponding UML diagram is as follows

Function Pointers + Structures = Observer Pattern in C? 5 Elegant Practical Source Code Examples

That is: when the key event source (the subject) triggers an event, it only cares about the event type and not how the observers handle it; the observers respond to specific events as needed, achieving decoupling of event dispatch and handling.

Example 3: Linked List Observer (Dynamic Data Change Notification)

Simulate a linked list (the subject) that notifies a statistician (the observer) to update the element count and sum result when nodes are added/removed.

#include <stdio.h>
#include <stdlib.h>

// Linked list node
typedef struct Node {
    int value;
    struct Node* next;
} Node;

// Forward declaration
typedef struct ListSubject ListSubject;

// 1. Observer interface (listens to linked list changes)
typedef struct {
    void (*on_change)(struct ListObserver* self, ListSubject* list);
    const char* name;
} ListObserver;

// 2. Subject interface (observable linked list)
typedef struct ListSubject {
    Node* head;
    ListObserver* observers[2];
    int obs_count;
    // Linked list operations
    void (*add_node)(struct ListSubject* self, int value);
    void (*remove_node)(struct ListSubject* self, int value);
    // Observer management
    void (*add_observer)(struct ListSubject* self, ListObserver* obs);
    void (*remove_observer)(struct ListSubject* self, ListObserver* obs);
    // Notify observers
    void (*notify_change)(struct ListSubject* self);
} ListSubject;

// Helper function: Get linked list length
static int list_length(ListSubject* self) {
    int len = 0;
    Node* curr = self->head;
    while (curr) { len++; curr = curr->next; }
    return len;
}

// Helper function: Get linked list sum
static int list_sum(ListSubject* self) {
    int sum = 0;
    Node* curr = self->head;
    while (curr) { sum += curr->value; curr = curr->next; }
    return sum;
}

// Subject implementation: Add node
static void list_add(ListSubject* self, int value) {
    Node* node = malloc(sizeof(Node));
    node->value = value;
    node->next = self->head;
    self->head = node;
    printf("Added node with value %d\n", value);
    self->notify_change(self);
}

// Subject implementation: Remove node
static void list_remove(ListSubject* self, int value) {
    Node* curr = self->head;
    Node* prev = NULL;
    while (curr && curr->value != value) {
        prev = curr;
        curr = curr->next;
    }
    if (curr) {
        if (prev) prev->next = curr->next;
        else self->head = curr->next;
        free(curr);
        printf("Removed node with value %d\n", value);
        self->notify_change(self);
    } else {
        printf("Node %d not found\n", value);
    }
}

// Subject implementation: Add observer
static void list_add_obs(ListSubject* self, ListObserver* obs) {
    if (self->obs_count < 2) {
        self->observers[self->obs_count++] = obs;
        printf("Observer '%s' added to list\n", obs->name);
    }
}

// Subject implementation: Remove observer
static void list_remove_obs(ListSubject* self, ListObserver* obs) {
    for (int i = 0; i < self->obs_count; i++) {
        if (self->observers[i] == obs) {
            for (int j = i; j < self->obs_count-1; j++) {
                self->observers[j] = self->observers[j+1];
            }
            self->obs_count--;
            printf("Observer '%s' removed from list\n", obs->name);
            return;
        }
    }
}

// Subject implementation: Notify change
static void list_notify(ListSubject* self) {
    printf("Notifying list change to %d observers\n", self->obs_count);
    for (int i = 0; i < self->obs_count; i++) {
        self->observers[i]->on_change(self->observers[i], self);
    }
}

// Create observable linked list
ListSubject* create_observable_list() {
    ListSubject* list = malloc(sizeof(ListSubject));
    list->head = NULL;
    list->obs_count = 0;
    list->add_node = list_add;
    list->remove_node = list_remove;
    list->add_observer = list_add_obs;
    list->remove_observer = list_remove_obs;
    list->notify_change = list_notify;
    return list;
}

// 3. Concrete observer 1: Length Statistician
static void length_observer_on_change(ListObserver* self, ListSubject* list) {
    printf("[%s] Current length: %d\n", self->name, list_length(list));
}

ListObserver* create_length_observer() {
    ListObserver* obs = malloc(sizeof(ListObserver));
    obs->on_change = length_observer_on_change;
    obs->name = "LengthObserver";
    return obs;
}

// Concrete observer 2: Sum Statistician
static void sum_observer_on_change(ListObserver* self, ListSubject* list) {
    printf("[%s] Current sum: %d\n", self->name, list_sum(list));
}

ListObserver* create_sum_observer() {
    ListObserver* obs = malloc(sizeof(ListObserver));
    obs->on_change = sum_observer_on_change;
    obs->name = "SumObserver";
    return obs;
}

// Free linked list
void free_list(ListSubject* list) {
    Node* curr = list->head;
    while (curr) {
        Node* next = curr->next;
        free(curr);
        curr = next;
    }
    free(list);
}

// Client usage
int main() {
    ListSubject* list = create_observable_list();
    ListObserver* len_obs = create_length_observer();
    ListObserver* sum_obs = create_sum_observer();

    list->add_observer(list, len_obs);
    list->add_observer(list, sum_obs);

    // Operate linked list (trigger notification)
    printf("\n");
    list->add_node(list, 10);
    printf("\n");
    list->add_node(list, 20);
    printf("\n");
    list->remove_node(list, 10);

    // Free resources
    list->remove_observer(list, len_obs);
    list->remove_observer(list, sum_obs);
    free(len_obs);
    free(sum_obs);
    free_list(list);
    return 0;
}

The output of the above code is as follows

Observer 'LengthObserver' added to list
Observer 'SumObserver' added to list

Added node with value 10
Notifying list change to 2 observers
[LengthObserver] Current length: 1
[SumObserver] Current sum: 10

Added node with value 20
Notifying list change to 2 observers
[LengthObserver] Current length: 2
[SumObserver] Current sum: 30

Removed node with value 10
Notifying list change to 2 observers
[LengthObserver] Current length: 1
[SumObserver] Current sum: 20
Observer 'LengthObserver' removed from list
Observer 'SumObserver' removed from list

The corresponding UML diagram is as follows

Function Pointers + Structures = Observer Pattern in C? 5 Elegant Practical Source Code Examples

That is: every modification of the linked list (the subject) notifies the observers, allowing them to update statistics in real-time, avoiding redundant code that manually calls statistical functions after each linked list operation.

Example 4: Kernel-Style Observer (Device Status Listening)

Simulate the Linux kernel device model, where the device (the subject) notifies drivers and applications (the observers) when its status changes.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Device state
typedef enum { DEVICE_OFFLINE, DEVICE_ONLINE, DEVICE_ERROR } DeviceState;

// Forward declaration
typedef struct Device Device;

// 1. Observer interface (kernel style: using callback functions)
typedef struct {
    void (*callback)(struct DeviceObserver* self, Device* dev, DeviceState state);
    const char* name; // Observer identifier (e.g., driver name)
} DeviceObserver;

// 2. Subject interface (device)
typedef struct Device {
    char name[32];
    DeviceState state;
    DeviceObserver* observers[3];
    int num_observers;
    // Device operations
    void (*set_state)(struct Device* self, DeviceState state);
    // Observer management
    int (*register_observer)(struct Device* self, DeviceObserver* obs); // Kernel common int return value
    int (*unregister_observer)(struct Device* self, DeviceObserver* obs);
} Device;

// Subject implementation: Set state and notify
static void device_set_state(Device* self, DeviceState state) {
    if (self->state == state) return; // No state change, no need to notify
    self->state = state;
    const char* state_str[] = {"OFFLINE", "ONLINE", "ERROR"};
    printf("Device '%s' state changed to %s\n", self->name, state_str[state]);

    // Notify all observers (kernel style: traverse list/array)
    for (int i = 0; i < self->num_observers; i++) {
        self->observers[i]->callback(self->observers[i], self, state);
    }
}

// Subject implementation: Register observer (return 0 indicates success, common in kernel)
static int device_register_obs(Device* self, DeviceObserver* obs) {
    if (self->num_observers >= 3) return -1; // Exceeds limit
    self->observers[self->num_observers++] = obs;
    printf("Observer '%s' registered to device '%s'\n", obs->name, self->name);
    return 0;
}

// Subject implementation: Unregister observer
static int device_unregister_obs(Device* self, DeviceObserver* obs) {
    for (int i = 0; i < self->num_observers; i++) {
        if (self->observers[i] == obs) {
            memmove(&self->observers[i], &self->observers[i+1], 
                   (self->num_observers - i - 1) * sizeof(DeviceObserver*));
            self->num_observers--;
            printf("Observer '%s' unregistered from device '%s'\n", obs->name, self->name);
            return 0;
        }
    }
    return -1; // Observer not found
}

// Create device (the subject)
Device* create_device(const char* name) {
    Device* dev = malloc(sizeof(Device));
    strncpy(dev->name, name, sizeof(dev->name)-1);
    dev->state = DEVICE_OFFLINE;
    dev->num_observers = 0;
    dev->set_state = device_set_state;
    dev->register_observer = device_register_obs;
    dev->unregister_observer = device_unregister_obs;
    return dev;
}

// 3. Concrete observer 1: Device Driver (handles device state changes)
static void driver_callback(DeviceObserver* self, Device* dev, DeviceState state) {
    switch (state) {
        case DEVICE_ONLINE:
            printf("[%s] Initializing device '%s'\n", self->name, dev->name);
            break;
        case DEVICE_ERROR:
            printf("[%s] Handling error for device '%s'\n", self->name, dev->name);
            break;
        default: break;
    }
}

DeviceObserver* create_driver(const char* name) {
    DeviceObserver* obs = malloc(sizeof(DeviceObserver));
    obs->callback = driver_callback;
    obs->name = name;
    return obs;
}

// Concrete observer 2: Application (listens to device status)
static void app_callback(DeviceObserver* self, Device* dev, DeviceState state) {
    if (state == DEVICE_ONLINE) {
        printf("[%s] Device '%s' is online, starting service\n", self->name, dev->name);
    } else if (state == DEVICE_OFFLINE) {
        printf("[%s] Device '%s' is offline, stopping service\n", self->name, dev->name);
    }
}

DeviceObserver* create_app(const char* name) {
    DeviceObserver* obs = malloc(sizeof(DeviceObserver));
    obs->callback = app_callback;
    obs->name = name;
    return obs;
}

// Client usage (simulate kernel device management)
int main() {
    Device* disk = create_device("sda");
    DeviceObserver* disk_driver = create_driver("disk_driver");
    DeviceObserver* app = create_app("backup_app");

    // Register observers
    disk->register_observer(disk, disk_driver);
    disk->register_observer(disk, app);

    // Change device state
    printf("\n");
    disk->set_state(disk, DEVICE_ONLINE);
    printf("\n");
    disk->set_state(disk, DEVICE_ERROR);
    printf("\n");
    disk->set_state(disk, DEVICE_OFFLINE);

    // Unregister application observer
    printf("\n");
    disk->unregister_observer(disk, app);
    disk->set_state(disk, DEVICE_ONLINE); // Only driver responds

    // Free resources
    free(disk_driver);
    free(app);
    free(disk);
    return 0;
}

The output of the above code is as follows

Observer 'disk_driver' registered to device 'sda'
Observer 'backup_app' registered to device 'sda'

Device 'sda' state changed to ONLINE
[disk_driver] Initializing device 'sda'
[backup_app] Device 'sda' is online, starting service

Device 'sda' state changed to ERROR
[disk_driver] Handling error for device 'sda'

Device 'sda' state changed to OFFLINE
[backup_app] Device 'sda' is offline, stopping service

Observer 'backup_app' unregistered from device 'sda'
Device 'sda' state changed to ONLINE
[disk_driver] Initializing device 'sda'

The corresponding UML diagram is as follows

Function Pointers + Structures = Observer Pattern in C? 5 Elegant Practical Source Code Examples

That is: simulating the observer mechanism of the Linux kernel device model, when the device state changes, it notifies observers through callback functions, consistent with the event notification logic of kobject_uevent in the kernel.

Example 5: Thread-Safe Observer (Multi-threaded Event Notification)

Simulate a multi-threaded environment where the subject safely notifies observers of state changes (using mutexes to protect the observer list).

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

// Shared data state
typedef struct {
    int value;
} SharedState;

// Forward declaration
typedef struct ThreadSafeSubject ThreadSafeSubject;

// 1. Observer interface (thread-safe)
typedef struct {
    void (*update)(struct ThreadObserver* self, SharedState* state);
    pthread_t tid; // Observer thread ID
} ThreadObserver;

// 2. Subject interface (with mutex)
typedef struct ThreadSafeSubject {
    SharedState state;
    ThreadObserver* observers[2];
    int count;
    pthread_mutex_t mutex; // Mutex to protect observer list
    // Thread-safe observer management
    void (*attach)(struct ThreadSafeSubject* self, ThreadObserver* obs);
    void (*detach)(struct ThreadSafeSubject* self, ThreadObserver* obs);
    // Thread-safe notification
    void (*notify)(struct ThreadSafeSubject* self);
} ThreadSafeSubject;

// Subject implementation: Add observer (with lock)
static void ts_attach(ThreadSafeSubject* self, ThreadObserver* obs) {
    pthread_mutex_lock(&self->mutex);
    if (self->count < 2) {
        self->observers[self->count++] = obs;
        printf("Thread %lu attached as observer\n", obs->tid);
    }
    pthread_mutex_unlock(&self->mutex);
}

// Subject implementation: Remove observer (with lock)
static void ts_detach(ThreadSafeSubject* self, ThreadObserver* obs) {
    pthread_mutex_lock(&self->mutex);
    for (int i = 0; i < self->count; i++) {
        if (self->observers[i] == obs) {
            for (int j = i; j < self->count-1; j++) {
                self->observers[j] = self->observers[j+1];
            }
            self->count--;
            printf("Thread %lu detached from observer\n", obs->tid);
            break;
        }
    }
    pthread_mutex_unlock(&self->mutex);
}

// Subject implementation: Notify (with lock traversal)
static void ts_notify(ThreadSafeSubject* self) {
    pthread_mutex_lock(&self->mutex);
    printf("Notifying %d observers (current value: %d)\n", self->count, self->state.value);
    for (int i = 0; i < self->count; i++) {
        self->observers[i]->update(self->observers[i], &self->state);
    }
    pthread_mutex_unlock(&self->mutex);
}

// Create thread-safe subject
ThreadSafeSubject* create_thread_safe_subject() {
    ThreadSafeSubject* subj = malloc(sizeof(ThreadSafeSubject));
    subj->state.value = 0;
    subj->count = 0;
    pthread_mutex_init(&subj->mutex, NULL);
    subj->attach = ts_attach;
    subj->detach = ts_detach;
    subj->notify = ts_notify;
    return subj;
}

// Update shared state (simulate multi-thread modification)
void ts_update_state(ThreadSafeSubject* self, int new_value) {
    pthread_mutex_lock(&self->mutex);
    self->state.value = new_value;
    printf("State updated to %d\n", new_value);
    pthread_mutex_unlock(&self->mutex);
    self->notify(self);
}

// 3. Concrete observer: Thread function (loop waiting for notifications)
static void observer_update(ThreadObserver* self, SharedState* state) {
    printf("Thread %lu received update: value = %d\n", self->tid, state->value);
}

// Observer thread entry
void* observer_thread(void* arg) {
    ThreadSafeSubject* subj = (ThreadSafeSubject*)arg;
    ThreadObserver obs = {.update = observer_update, .tid = pthread_self()};

    // Register as observer
    subj->attach(subj, &obs);

    // Simulate work: wait for 3 notifications before exiting
    sleep(3);

    // Unregister observer
    subj->detach(subj, &obs);
    return NULL;
}

// Client usage (multi-threaded environment)
int main() {
    ThreadSafeSubject* subj = create_thread_safe_subject();
    pthread_t t1, t2;

    // Create two observer threads
    pthread_create(&t1, NULL, observer_thread, subj);
    pthread_create(&t2, NULL, observer_thread, subj);

    // Main thread updates state (trigger notifications)
    sleep(1); // Wait for threads to register
    ts_update_state(subj, 10);
    sleep(1);
    ts_update_state(subj, 20);
    sleep(1);
    ts_update_state(subj, 30);

    // Wait for threads to finish
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    // Free resources
    pthread_mutex_destroy(&subj->mutex);
    free(subj);
    return 0;
}

The output of the above code is as follows

Thread 128919168611904 attached as observer
Thread 128919160219200 attached as observer
State updated to 10
Notifying 2 observers (current value: 10)
Thread 128919168611904 received update: value = 10
Thread 128919160219200 received update: value = 10
State updated to 20
Notifying 2 observers (current value: 20)
Thread 128919168611904 received update: value = 20
Thread 128919160219200 received update: value = 20
Thread 128919168611904 detached from observer
Thread 128919160219200 detached from observer
State updated to 30
Notifying 0 observers (current value: 30)

The corresponding UML diagram is as follows

Function Pointers + Structures = Observer Pattern in C? 5 Elegant Practical Source Code Examples

That is: in a multi-threaded environment, the observer list’s addition, removal, and traversal operations are protected by mutexes to avoid memory errors caused by concurrent access, suitable for event notification scenarios in multi-threaded programs.

4. Application of the Observer Pattern in the Linux Kernel

  1. 1. kobject Event Notification (kobject_uevent): In the kernel device model, when the state of a kobject (the subject) changes (such as device addition/removal), it sends events through kobject_uevent, and uevent observers (such as udev) receive events and update device nodes, achieving synchronization between user space and kernel space.
  2. 2. Interrupt Handling (irqaction): When hardware interrupts (the subject) occur, the kernel traverses the registered irqaction structures (observer list) and calls the handler functions to handle the interrupts, allowing multiple drivers to register as observers for the same interrupt.
  3. 3. File System Notifications (inotify): User-space programs register for file/directory events (such as creation, modification) through inotify, and when the kernel (the subject) detects file changes, it sends events to registered observers, enabling real-time file monitoring.
  4. 4. Network Protocol Stack (netlink): Kernel modules or user-space programs register as observers through netlink sockets, and when network states change (such as routing updates, interface start/stop), the kernel sends messages to notify observers through netlink.
  5. 5. Work Queues (workqueue): Work queues are the kernel’s asynchronous execution mechanism, where work_struct (the observer) is registered to the work queue (the subject), and when the queue is awakened, the func functions of all work items are called, enabling batch task processing.

5. Implementation Considerations

  1. 1. Thread Safety: In a multi-threaded environment, modifications to the observer list (attach/detach) and traversals (notify) must be protected by mutexes to avoid list corruption or crashes due to concurrent access (such as deleting elements during iteration).
  2. 2. Avoid Circular Dependencies: Circular references between observers and subjects should be avoided (e.g., observers holding pointers to subjects while subjects also hold pointers to observers), as this may lead to memory leaks (unable to release resources).
  3. 3. Notification Order: Clearly define the notification order of observers (e.g., by registration order, priority order) to avoid logical errors caused by uncertain order (e.g., scenarios where one observer depends on another to execute first).
  4. 4. Resource Management: Ensure that observers are thoroughly removed from the subject’s list upon unregistration to avoid the subject continuing to send notifications to released observers (dangling pointer access).
  5. 5. Performance Optimization: In high-frequency notification scenarios (such as real-time data updates), reduce notification overhead by using batch notifications (accumulating multiple changes before notifying once) or asynchronous notifications (handling observer callbacks through a thread pool).

6. Additional Notes

  • • The difference between the Observer Pattern and the Publish-Subscribe Pattern: The Observer Pattern is direct notification (the subject directly calls the observer interface), while the Publish-Subscribe Pattern uses a message queue intermediary (observers subscribe to topics, publishers send messages to topics), achieving more thorough decoupling.
  • • Push vs Pull Model: The push model (the subject actively pushes complete data) is suitable for scenarios with small data volumes; the pull model (observers fetch data from the subject as needed) is suitable for scenarios with large data volumes, avoiding redundant transmission.
  • • Applicable Scenarios: Event-driven systems, state monitoring, data synchronization, UI component interaction (such as button click events), distributed system notifications, etc., where one-to-many responses are required.

Through the Observer Pattern, C language programs (especially in the Linux kernel) can implement a flexible event notification mechanism, significantly reducing coupling between modules, making the system easier to extend and maintain. This pattern is widely used in the kernel to achieve cross-module communication and is one of the core technologies for building high-cohesion, low-coupling systems.

Click to read the full article, thank you for sharing, saving, liking, and viewing.

Leave a Comment