设计模式(2)
一、观察者模式
1.讲解
观察者模式 : 定义了 对象之间 一对多 的 依赖 , 令 多个 观察者 对象 同时 监听 某一个 主题对象 , 当 主题对象 发生改变时 , 所有的 观察者 都会 收到通知 并更新 ;
观察者 有多个 , 被观察的 主题对象 只有一个 ;
观察者模式适用场景 : 当你的业务符合订阅-发布这种场景时就考虑这个模式。
如 : 在购物网站 , 多个用户关注某商品后 , 当商品降价时 , 就会自动通知关注该商品的用户 ;
主题对象 : 商品是主题对象 ;
观察者 : 用户是观察者 ;
观察者注册 : 用户关注 , 相当于注册观察者 ;
通知触发条件 : 商品降价 ;
观察者模式优点 :
抽象耦合 : 在 观察者 和 被观察者 之间 , 建立了一个 抽象的 耦合 ; 由于 耦合 是抽象的 , 可以很容易 扩展 观察者 和 被观察者 ;
广播通信 : 观察者模式 支持 广播通信 , 类似于消息广播 , 如果需要接收消息 , 只需要注册一下即可 ;
低耦合:主题不需要知道观察者的具体实现,只知道它实现了一个“通知接口”。
可扩展:新增观察者无需修改主题代码。
观察者模式缺点 :
依赖过多 : 观察者 之间 细节依赖 过多 , 会增加 时间消耗 和 程序的复杂程度 ;
这里的 细节依赖 指的是 触发机制 , 触发链条 ; 如果 观察者设置过多 , 每次触发都要花很长时间去处理通知 ;
循环调用 : 避免 循环调用 , 观察者 与 被观察者 之间 绝对不允许循环依赖 , 否则会触发 二者 之间的循环调用 , 导致系统崩溃 ;
观察者过多时,通知可能耗时。
通知是广播式的,可能导致不必要的更新。
Subject(主题)├── attach(observer)├── detach(observer)└── notify()Observer(观察者接口)└── update()ConcreteObserverA / ConcreteObserverB(具体观察者)
2.示例
公众号订阅:
公众号是 主题(Subject)
订阅它的粉丝是 观察者(Observer)
当公众号发新文章(状态变化),它会自动通知所有粉丝(观察者),粉丝收到通知后可以选择查看。
.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define MAX_OBSERVERS 10/* --- 抽象观察者接口 --- */
typedef struct Observer {void (*update)(struct Observer *self, const char *message);
} Observer;/* --- 主题(Subject) --- */
typedef struct {Observer *observers[MAX_OBSERVERS];int count;
} Subject;void subject_init(Subject *s) { //参数Subject *s其实就是相当于C++的this指针s->count = 0;
}void subject_attach(Subject *s, Observer *o) {if (s->count < MAX_OBSERVERS) {s->observers[s->count++] = o;}
}void subject_detach(Subject *s, Observer *o) {for (int i = 0; i < s->count; i++) {if (s->observers[i] == o) {for (int j = i; j < s->count - 1; j++) {s->observers[j] = s->observers[j + 1];}s->count--;break;}}
}void subject_notify(Subject *s, const char *message) {for (int i = 0; i < s->count; i++) {s->observers[i]->update(s->observers[i], message);}
}/* --- 具体观察者:用户 --- */
typedef struct {Observer base;char name[20];
} User;void user_update(Observer *self, const char *message) {User *u = (User *)self;printf("[用户 %s 收到推送] %s\n", u->name, message);
}User *user_create(const char *name) {User *u = (User *)malloc(sizeof(User));strcpy(u->name, name);u->base.update = user_update;return u;
}void user_destroy(User *u) {free(u);
}/* --- 演示 --- */
int main() {Subject wechat;subject_init(&wechat);User *alice = user_create("Alice");User *bob = user_create("Bob");User *charlie = user_create("Charlie");subject_attach(&wechat, (Observer *)alice);subject_attach(&wechat, (Observer *)bob);subject_attach(&wechat, (Observer *)charlie);subject_notify(&wechat, "今天更新了一篇新文章!");printf("---- Bob取消订阅 ----\n");subject_detach(&wechat, (Observer *)bob);subject_notify(&wechat, "第二篇新文章上线啦!");user_destroy(alice);user_destroy(bob);user_destroy(charlie);return 0;
}
[用户 Alice 收到推送] 今天更新了一篇新文章!
[用户 Bob 收到推送] 今天更新了一篇新文章!
[用户 Charlie 收到推送] 今天更新了一篇新文章!
---- Bob取消订阅 ----
[用户 Alice 收到推送] 第二篇新文章上线啦!
[用户 Charlie 收到推送] 第二篇新文章上线啦!
在 C 中的实现方式:用 结构体 + 函数指针 模拟接口;用 数组或链表 存储观察者列表。
3.RT-Thread中的应用
在RT-Thread中主要体现在回调。
①
例子:
rt_object_detach_hook
rt_object_attach_hook
Subject:对象管理器(
rt_object
系统)Observer:用户注册的 hook 函数
当某个对象 attach/detach 时,所有注册的 hook 都会被调用。
#define RT_OBJECT_HOOK_CALL(func, argv) \do { \if ((func) != RT_NULL) \func argv; \} while (0)
这里主题是对象生命周期变化,hook 就是观察者。
②
例子:
rt_err_t rt_device_set_rx_indicate(rt_device_t dev, rt_err_t (*rx_ind)(rt_device_t dev, rt_size_t size));
Subject:设备驱动
Observer:上层应用注册的
rx_ind
回调当设备收到数据时,驱动会主动调用
rx_ind
通知应用层。
二、单例模式
1.讲解
什么是单例模式
单例模式是指在整个系统生命周期内,保证一个类只能产生一个实例,确保该类的唯一性。
为什么需要单例模式
两个原因:
节省资源。一个类只有一个实例,不存在多份实例,节省资源。
方便控制。在一些操作公共资源的场景时,避免了多个对象引起的复杂操作。
但是在实现单例模式时,需要考虑到线程安全的问题。
线程安全
什么是线程安全?
在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
如何保证线程安全?
给共享的资源加把锁,保证每个资源变量每时每刻至多被一个线程占用。
让线程也拥有资源,不用去共享进程中的资源。如:使用threadlocal可以为每个线程维护一个私有的本地变量。
单例模式分类
单例模式可以分为 懒汉式 和 饿汉式 ,两者之间的区别在于创建实例的时间不同。
懒汉式
系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例。这种方式要考虑线程安全。
延迟加载:第一次用到实例时才创建对象。
节省内存:如果程序运行过程中没用到,就不会占内存。
缺点:多线程环境下需要加锁,否则可能创建多个实例。
优点:启动速度快,占用内存少。
缺点:第一次访问时可能有延迟;多线程实现复杂。
饿汉式
系统一运行,就初始化创建实例,当需要时,直接调用即可。这种方式本身就线程安全,没有多线程的线程安全问题。
类加载/程序启动时 就创建实例(即使可能永远用不到)。
优点:线程安全(因为程序初始化阶段就完成实例化)。
缺点:可能浪费内存(如果不使用这个对象)。
优点:实现简单,无需加锁。
缺点:浪费内存,启动时会有创建成本。
单例类的特点
构造函数和析构函数为私有类型,目的是禁止外部构造和析构。
拷贝构造函数和赋值构造函数是私有类型,目的是禁止外部拷贝和赋值,确保实例的唯一性。
类中有一个获取实例的静态方法,可以全局访问。
2.示例
主要讲解三种懒汉模式(不适用静态变量、使用全局静态变量、使用静态局部变量)以及一种饿汉模式(全局静态变量)。
①不使用静态变量
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>/* ===================== 懒汉模式 ===================== */
typedef struct {int value;
} ConfigManager_Lazy;ConfigManager_Lazy* get_config_manager_lazy(void) {static ConfigManager_Lazy *instance = NULL;static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;if (instance == NULL) { // 第一次检查pthread_mutex_lock(&mutex);if (instance == NULL) { // 双重检查锁(DCL)instance = malloc(sizeof(ConfigManager_Lazy));instance->value = 0;printf("[懒汉] 创建实例\n");}pthread_mutex_unlock(&mutex);}return instance;
}/* ===================== 饿汉模式 ===================== */
typedef struct {int value;
} ConfigManager_Eager;/* 程序启动时就初始化 */
static ConfigManager_Eager eager_instance = { .value = 0 };ConfigManager_Eager* get_config_manager_eager(void) {return &eager_instance;
}/* ===================== 测试 ===================== */
void* test_lazy(void* arg) {ConfigManager_Lazy *cfg = get_config_manager_lazy();printf("[懒汉] 线程 %ld value=%d\n", (long)arg, cfg->value);cfg->value++;return NULL;
}void* test_eager(void* arg) {ConfigManager_Eager *cfg = get_config_manager_eager();printf("[饿汉] 线程 %ld value=%d\n", (long)arg, cfg->value);cfg->value++;return NULL;
}int main() {pthread_t t1, t2;/* 测试懒汉模式 */pthread_create(&t1, NULL, test_lazy, (void*)1);pthread_create(&t2, NULL, test_lazy, (void*)2);pthread_join(t1, NULL);pthread_join(t2, NULL);/* 测试饿汉模式 */pthread_create(&t1, NULL, test_eager, (void*)1);pthread_create(&t2, NULL, test_eager, (void*)2);pthread_join(t1, NULL);pthread_join(t2, NULL);return 0;
}
懒汉模式为什么光有malloc,没有free释放。
这个实例整个程序运行周期只存在一个,所以大多数实现直接在程序结束时由操作系统回收内存。
在很多 C 项目(尤其是嵌入式或守护进程)里,单例对象会伴随进程的整个生命周期,开发者会不手动释放,因为:
进程退出时,OS 会释放所有分配的堆内存。
单例对象通常是全局共享资源,提前释放可能导致其他模块访问到悬空指针。
那什么时候需要手动释放?
如果你的单例对象:
生命周期小于进程(例如需要动态卸载某个模块)
会多次创建/销毁(比如在单元测试中反复运行)
程序是一个库(library),内存释放由调用方管理
那么就需要提供一个显式的销毁接口:
void destroy_config_manager_lazy(void) {static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_lock(&mutex);ConfigManager_Lazy *instance = get_config_manager_lazy();if (instance != NULL) {free(instance);instance = NULL; // 防止悬空指针printf("[懒汉] 销毁实例\n");}pthread_mutex_unlock(&mutex);
}
应用程序型:可以不写
free
,让 OS 接管。库/插件型:必须写
destroy_instance()
释放内存。多次初始化场景:一定要释放并清空指针,防止内存泄漏或悬空指针。
下面的程序在刚才的懒汉模式代码上加上线程安全释放逻辑,这样单例既能保持唯一性,又能在需要时回收内存。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>typedef struct {int value;
} ConfigManager_Lazy;/* 静态全局变量保存单例实例和锁 */
static ConfigManager_Lazy *lazy_instance = NULL;
static pthread_mutex_t lazy_mutex = PTHREAD_MUTEX_INITIALIZER;/* 获取实例(懒汉模式 + 双重检查锁) */
ConfigManager_Lazy* get_config_manager_lazy(void) {if (lazy_instance == NULL) {pthread_mutex_lock(&lazy_mutex);if (lazy_instance == NULL) { // 双重检查lazy_instance = malloc(sizeof(ConfigManager_Lazy));if (lazy_instance == NULL) {fprintf(stderr, "内存分配失败!\n");exit(EXIT_FAILURE);}lazy_instance->value = 0;printf("[懒汉] 创建实例\n");}pthread_mutex_unlock(&lazy_mutex);}return lazy_instance;
}/* 释放实例 */
void destroy_config_manager_lazy(void) {pthread_mutex_lock(&lazy_mutex);if (lazy_instance != NULL) {free(lazy_instance);lazy_instance = NULL;printf("[懒汉] 销毁实例\n");}pthread_mutex_unlock(&lazy_mutex);
}/* 测试线程函数 */
void* test_lazy(void* arg) {ConfigManager_Lazy *cfg = get_config_manager_lazy();printf("[懒汉] 线程 %ld value=%d\n", (long)arg, cfg->value);cfg->value++;return NULL;
}int main() {pthread_t t1, t2;pthread_create(&t1, NULL, test_lazy, (void*)1);pthread_create(&t2, NULL, test_lazy, (void*)2);pthread_join(t1, NULL);pthread_join(t2, NULL);/* 手动销毁实例 */destroy_config_manager_lazy();/* 再次获取实例(会重新创建) */ConfigManager_Lazy *cfg = get_config_manager_lazy();printf("[懒汉] main线程 value=%d\n", cfg->value);destroy_config_manager_lazy();return 0;
}
- 生命周期可控:随时可以销毁并重新创建。
防悬空指针:释放后置
lazy_instance = NULL
。
②使用全局静态变量:这就是典型的 C 语言版单例:全局唯一实例 + 全局访问点。
#include <stdio.h>
#include <pthread.h>typedef struct {int value;
} ConfigManager_Static;/* 静态全局变量保存实例和状态 */
static ConfigManager_Static static_instance;
static int is_initialized = 0;
static pthread_mutex_t static_mutex = PTHREAD_MUTEX_INITIALIZER;/* 获取实例(懒汉式,但用 static) */
ConfigManager_Static* get_config_manager_static(void) {if (!is_initialized) {pthread_mutex_lock(&static_mutex);if (!is_initialized) { // 双重检查static_instance.value = 0;is_initialized = 1;printf("[懒汉-静态] 初始化实例\n");}pthread_mutex_unlock(&static_mutex);}return &static_instance;
}/* 销毁实例(其实只是重置状态) */
void destroy_config_manager_static(void) {pthread_mutex_lock(&static_mutex);if (is_initialized) {is_initialized = 0;printf("[懒汉-静态] 重置实例\n");}pthread_mutex_unlock(&static_mutex);
}/* 测试线程函数 */
void* test_static(void* arg) {ConfigManager_Static *cfg = get_config_manager_static();printf("[懒汉-静态] 线程 %ld value=%d\n", (long)arg, cfg->value);cfg->value++;return NULL;
}int main() {pthread_t t1, t2;pthread_create(&t1, NULL, test_static, (void*)1);pthread_create(&t2, NULL, test_static, (void*)2);pthread_join(t1, NULL);pthread_join(t2, NULL);/* 重置 */destroy_config_manager_static();/* 再次获取实例 */ConfigManager_Static *cfg = get_config_manager_static();printf("[懒汉-静态] main线程 value=%d\n", cfg->value);destroy_config_manager_static();return 0;
}
③下面就讲一下:局部静态变量懒汉单例
这是 C 里单例模式最推荐的懒汉式写法之一。
这样做的特点是:
不需要全局变量(外部文件看不到)
实例指针只在函数内可见(封装性好)
跨多次调用能保持唯一实例(因为是 static)
C99 以后局部 static 初始化是线程安全的(在 C++11 里是强制线程安全的,C 里一般还是要加锁以保证兼容性)
#include <stdio.h>
#include <pthread.h>typedef struct {int value;
} ConfigManager;/* 互斥锁,保证多线程安全 */
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;ConfigManager* get_config_manager(void) {/* 局部 static 变量,只有第一次调用会初始化 */static ConfigManager instance;static int is_initialized = 0;if (!is_initialized) {pthread_mutex_lock(&mutex);if (!is_initialized) { // 双重检查instance.value = 0;is_initialized = 1;printf("[懒汉-局部static] 初始化实例\n");}pthread_mutex_unlock(&mutex);}return &instance;
}/* 测试线程函数 */
void* thread_func(void* arg) {ConfigManager* cfg = get_config_manager();printf("线程%ld: value = %d\n", (long)arg, cfg->value);cfg->value++;return NULL;
}int main() {pthread_t t1, t2;pthread_create(&t1, NULL, thread_func, (void*)1);pthread_create(&t2, NULL, thread_func, (void*)2);pthread_join(t1, NULL);pthread_join(t2, NULL);ConfigManager* cfg = get_config_manager();printf("主线程: value = %d\n", cfg->value);return 0;
}
3.RT-Thread中的应用
①内核全局对象管理器(rt_object_information
)
单例角色:
每类对象(线程、信号量、设备等)只有一个全局的对象信息管理器。
static struct rt_object_information _object_container[RT_Object_Info_Unknown] = {/* Thread */{RT_Object_Class_Thread, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Thread), sizeof(struct rt_thread)},/* Semaphore */{RT_Object_Class_Semaphore, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Semaphore), sizeof(struct rt_semaphore)},...
};
这些 _object_container[]
是全局唯一的,所有地方通过 rt_object_get_information()
访问它们。
②驱动层的硬件控制实例
例如 Pin 驱动 _hw_pin
:
static struct rt_device_pin _hw_pin;
它是全局唯一的 Pin 控制对象,注册到设备框架后,所有地方都是访问同一个 _hw_pin
。
单例模式在 RT-Thread 中的设计目的
保证系统关键模块全局唯一性(调度器、时钟、对象管理器、控制台等)
避免多实例带来的资源冲突
方便提供统一的全局访问入口
在RT-Thread 中的单例模式通常具备:
static
全局变量(限制作用域)唯一初始化函数(init 或 register 函数)
全局访问 API(get/set)