MySql数据库连接池
C++数据库连接池
- 前言
- 1.MySql API 函数讲解
- 1.1 连接数据库的步骤
- 1.2 MySQL C API
- 1.2.1 初始化连接环境
- 1.2.2 连接mysql服务器
- 1.2.3 执行sql语句
- 1.2.4 获取结果集
- 1.2.5 得到结果集的列数
- 1.2.6 获取表头 -> 列名(字段名)
- 1.2.7 得到结果集中各个字段的长度(字节为单位)
- 1.2.8 遍历结果集 (结果集的下一行)
- 1.2.9 资源回收
- 1.2.10 字符编码
- 1.2.11 事务操作
- 1.2.12 打印错误信息
- 1.3、实例代码
- 1.4、C中的变量插入数据库中
- 1.4.1 动态SQL
- 1.4.2 预处理语句 (推荐使用)
- mysql_stmt_init 函数
- mysql_stmt_prepare 函数
- 演示代码
- 数据库连接池
- 连接池的设计
- 细节分析
- 项目代码如下:
- 1. 用C API 封装成C++类
- 2. 连接池设计
- 3. 主函数(测试)
- 4. 代码说明
- 1. 条件变量 wait_for
- 2. shared_ptr 自定义删除器
- 3. 条件变量个数问题
- 4. 单例模式
- 1. 懒汉模式(Lazy Initialization)
- (1)基础懒汉模式(非线程安全)
- (2)线程安全的懒汉模式(加锁)
- (3)C++11 线程安全懒汉模式(`std::call_once`)
- 2. 饿汉模式(Eager Initialization)
- (1)基础饿汉模式
- (2)C++11 改进版(局部静态变量)
- 3. 懒汉 vs 饿汉模式对比
- 4. 推荐方案
前言
- 这里就不带大家去安装MySql以及配置VS的环境了,如果需要就去看之前的博客:MySql安装以及QT、VS连接数据库。
- 这里提醒大家一点,如果配置环境之后,依旧无法运行程序,那么可能是 缺少安全模块的动态库,其实这个库文件就是
openssl
的动态库,我们无需自己安装,官方是有提供的。首先打开官网下载地址:https://dev.mysql.com/downloads/
- 这里提醒大家一点,如果配置环境之后,依旧无法运行程序,那么可能是 缺少安全模块的动态库,其实这个库文件就是
下载使用C++连接数据库需要的组件(建议下载和本地 mysql 动态库一致的版本):
可以根据自己的实际需求选择安装版或者绿色版以及32bit或64bit。安装或解压完毕之后,我们就可以得到这样一个目录:
进入到lib64目录:
目录中的 libcrypto-1_1-x64.dll
和 libssl-1_1-x64.dll
就是我们需要的动态库,把这 两个动态库拷贝到和连接数据库的可执行程序同一级目录中就可以了。
- 关于基础的MySql的命令,请看这里的博文链接MySql基础命令
1.MySql API 函数讲解
1.1 连接数据库的步骤
众所周知,MySQL数据库是一个典型的 C/S结构,即:客户端和服务器端
。如果我们部署好了MySQL服务器,想要在客户端访问服务器端的数据,在编写程序的时候就可以通过官方提供的C语言的API来实现。
在程序中连接MySql服务器,主要分为已经几个步骤:
初始化连接环境
连接mysql的服务器,需要提供如下连接数据:
- 服务器的IP地址
- 服务器监听的端口(默认端口是3306)前面安装的时候有提示
- 连接服务器使用的用户名(默认是 root),和这个用户对应的密码
- 要操作的数据库的名字
连接已经建立, 后续操作就是对数据库数据的添删查改
- 这些操作都是需要通过sql语句来完成的
- 数据查询:通过调用api 执行一个查询的sql语句
- 数据修改(添加/删除/更新):通过调用api 执行一个修改数据的sql语句
如果要进行数据 添加/ 删除/ 更新,需要进行事务的处理
需要对执行的结果进行判断
- 成功:提交事务
- 失败:数据回滚
数据库的读操作 -> 查询 -> 得到结果集
遍历结果集 -> 得到了要查询的数据
释放资源
1.2 MySQL C API
对于以上的操作步骤,在MySQL提供的API中都有对应的操作函数,下面,为大家介绍一下这些API函数的使用。
1.2.1 初始化连接环境
MYSQL *mysql_init(MYSQL *mysql) ;
参数
mysql
:一般设置为null
返回值: 该函数将分配、初始化、并返回新对象
- 通过返回的这个对象去连接MySQL的服务器
mysql_init()
返回的 MYSQL*
指针 指向一个内部数据结构,该结构存储了与 MySQL 服务器连接相关的所有状态和控制信息。 虽然 MySQL 官方文档并未完全公开其具体字段(因为它是一个不透明类型,用户无需直接访问其内部)。
总结
MYSQL*
指针指向的内存是一个连接控制块,封装了 MySQL 客户端与服务器交互所需的所有上下文信息。开发者无需关心其具体布局,只需通过 API 函数安全地操作它。重点是:
- 始终检查返回值(如
NULL
表示失败)。 - 及时释放资源(
mysql_close
)。 - 依赖官方 API 而非直接访问内部字段。
1.2.2 连接mysql服务器
MYSQL *mysql_real_connect(MYSQL *mysql, // mysql_init() 函数的返回值const char *host, // mysql服务器的主机地址, 写IP地址即可// localhost, null -> 代表本地连接const char *user, // 连接mysql服务器的用户名, 默认: root const char *passwd, // 连接mysql服务器用户对应的密码, root用户的密码const char *db, // 要使用的数据库的名字unsigned int port, // 连接的mysql服务器监听的端口// 如果==0, 使用mysql的默认端口3306, !=0, 使用指定的这个端口const char *unix_socket,// 本地套接字, 不使用指定为 NULLunsigned long client_flag); // 通常指定为0
返回值:
- 成功: 返回MYSQL* 连接句柄, 对于成功的连接,返回值与第1个参数的值相同。返回值指向的内存和第一个参数指针指向的内存一样
- 失败,返回NULL。
句柄: 是windows中的一个概念, 句柄可以理解为一个实例(或者对象)
1.2.3 执行sql语句
// 执行一个sql语句, 添删查改的sql语句都可以
int mysql_query(MYSQL *mysql, const char *query);
参数:
- mysql:
mysql_real_connect()
的返回值- query: 一个
可以执行的sql语句
, 结尾的位置不需要加\0
;返回值:
- 如果查询成功,返回0。如果是查询, 结果集在mysql 对象中
- 如果出现错误,返回非0值。
1.2.4 获取结果集
MYSQL_RES *mysql_store_result(MYSQL *mysql);
- 作用:
从服务器检索完整的结果集并将其存储在客户端内存中
MYSQL_RES
对应一块内存, 里边保存着这个查询之后得到的结果集- 如何将行和列的数据从结果集中取出, 需要使用其他函数
- 返回值: 具有多个结果的
MYSQL_RES
结果集合。
- 如果出现错误,返回NULL。
- 返回的是
MYSQL_RES
类型的指针,可以理解是指向MYSQL_RES
数组的首地址
1.2.5 得到结果集的列数
unsigned int mysql_num_fields(MYSQL_RES *result)
- 从结果集中列的个数
- 参数: 调用
mysql_store_result()
得到的返回值- 返回值: 结果集中的列数
1.2.6 获取表头 -> 列名(字段名)
MYSQL_FIELD *mysql_fetch_fields(MYSQL_RES *result);
- 参数: 调用
mysql_store_result() 得到的返回值
- 返回值:
MYSQL_FIELD*
指向一个结构体- 通过查询官方文档, 返回是一个结构体的数组
- 通过这个函数得到结果集中所有列的名字
返回值MYSQL_FIELD
对应的是一个结构体,在mysql.h
中定义如下:
// mysql.h
// 结果集中的每一个列对应一个 MYSQL_FIELD
typedef struct st_mysql_field {char *name; /* 列名-> 字段的名字 */char *org_name; /* Original column name, if an alias */char *table; /* Table of column if column was a field */char *org_table; /* Org table name, if table was an alias */char *db; /* Database for table */char *catalog; /* Catalog for table */char *def; /* Default value (set by mysql_list_fields) */unsigned long length; /* Width of column (create length) */unsigned long max_length; /* Max width for selected set */unsigned int name_length;unsigned int org_name_length; unsigned int table_length;unsigned int org_table_length;unsigned int db_length;unsigned int catalog_length;unsigned int def_length;unsigned int flags; /* Div flags */unsigned int decimals; /* Number of decimals in field */unsigned int charsetnr; /* Character set */enum enum_field_types type; /* Type of field. See mysql_com.h for types */void *extension;
} MYSQL_FIELD;
函数的使用举例:
// 得到存储头信息的数组的地址
MYSQL_FIELD* fields = mysql_fetch_fields(res);
// 得到列数
int num = mysql_num_fields(res);
// 遍历得到每一列的列名
for(int i=0; i<num; ++i)
{printf("当前列的名字: %s\n", fields[i].name);
}
1.2.7 得到结果集中各个字段的长度(字节为单位)
unsigned long *mysql_fetch_lengths(MYSQL_RES *result);
返回当前行中各列数据的实际长度(以字节为单位,以数组形式返回):
- 如果打算复制字段值,使用该函数 能避免调用
strlen()
。- 如果结果集包含二进制数据,必须使用该函数来确定数据的大小,原因在于,对于包含
NULL
字符的任何字段,strlen()
将返回错误的结果。参数:
- result: 通过查询得到的结果集
返回值:
- 无符号长整数的数组 表示各列的大小。如果出现错误,返回NULL。
示例程序:
MYSQL_ROW row;
unsigned long *lengths;
unsigned int num_fields;//遍历结果集的下一行
row = mysql_fetch_row(result);
if (row)
{num_fields = mysql_num_fields(result); // 得到列数lengths = mysql_fetch_lengths(result);for(int i = 0; i < num_fields; i++){printf("Column %u is %lu bytes in length.\n", i, lengths[i]);}
}
1.2.8 遍历结果集 (结果集的下一行)
typedef char** MYSQL_ROW;MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);
- 遍历结果集的下一行 ,如果想遍历整个结果集, 需要对该函数进行
循环调用
- 返回值是
二级指针char **
,指向一个什么类型的内存呢?
- 指向一个指针数组, 类型是数组 , 里边的每个元素都是指针,
char*
类型char* [];
数组中的字符串对应的一列数据- 需要 对
MYSQL_ROW
遍历就可以得到每一列的值,MYSQL_ROW
实际上就是单行数据集结果- 如果要遍历整个结果集, 需要循环调用这个函数
参数:
- result: 通过查询得到的结果集
返回值:
- 成功: 得到了当前记录中每个字段的值
- 失败:
NULL, 说明数据已经读完了
1.2.9 资源回收
// 释放结果集
void mysql_free_result(MYSQL_RES *result);// 关闭mysql实例
void mysql_close(MYSQL *mysql);
1.2.10 字符编码
// 获取api默认使用的字符编码
// 为当前连接返回默认的字符集。
const char *mysql_character_set_name(MYSQL *mysql)
// 返回值: 默认字符集。 // 设置api使用的字符集
// 第二个参数 csname 就是要设置的字符集 -> 支持中文: utf8
int mysql_set_character_set(MYSQL *mysql, char *csname);
1.2.11 事务操作
// mysql中默认会进行事务的提交
// 因为自动提交事务, 会对我们的操作造成影响
// 如果我们操作的步骤比较多, 集合的开始和结束需要用户自己去设置, 需要改为手动方式提交事务
my_bool mysql_autocommit(MYSQL *mysql, my_bool mode)
//参数:
// 如果模式为“1”,启用autocommit模式;如果模式为“0”,禁止autocommit模式。
//返回值
// 如果成功,返回0,如果出现错误,返回非0值。// 事务提交
my_bool mysql_commit(MYSQL *mysql);
//返回值: 成功: 0, 失败: 非0// 数据回滚
my_bool mysql_rollback(MYSQL *mysql)
//返回值: 成功: 0, 失败: 非0
1.2.12 打印错误信息
// 返回错误的描述
const char *mysql_error(MYSQL *mysql);// 返回错误的编号
unsigned int mysql_errno(MYSQL *mysql);
1.3、实例代码
需要的头文件
#include <mysql.h>
测试程序:
#include <stdio.h>
#include <mysql.h>int main()
{// 1. 初始化连接环境MYSQL* mysql = mysql_init(NULL);if(mysql == NULL)\{printf("mysql_init() error\n");return -1;}// 2. 连接数据库服务器mysql = mysql_real_connect(mysql, "localhost", "root", "root", "scott", 0, NULL, 0);if(mysql == NULL){printf("mysql_real_connect() error\n");return -1;}printf("mysql api使用的默认编码: %s\n", mysql_character_set_name(mysql));// 设置编码为utf8mysql_set_character_set(mysql, "utf8");printf("mysql api使用的修改之后的编码: %s\n", mysql_character_set_name(mysql));printf("恭喜, 连接数据库服务器成功了...\n");// 3. 执行一个sql语句// 查询scott数据库下的dept部门表const char* sql = "select * from dept";// 执行这个sql语句int ret = mysql_query(mysql, sql);if(ret != 0){printf("mysql_query() a失败了, 原因: %s\n", mysql_error(mysql));return -1;}// 4. 取出结果集MYSQL_RES* res = mysql_store_result(mysql);if(res == NULL){printf("mysql_store_result() 失败了, 原因: %s\n", mysql_error(mysql));return -1;}// 5. 得到结果集中的列数int num = mysql_num_fields(res);// 6. 得到所有列的名字, 并且输出MYSQL_FIELD * fields = mysql_fetch_fields(res);for(int i=0; i<num; ++i){printf("%s\t\t", fields[i].name);}printf("\n");// 7. 遍历结果集中所有的行MYSQL_ROW row;while( (row = mysql_fetch_row(res)) != NULL){// 将当前行中的每一列信息读出for(int i=0; i<num; ++i){printf("%s\t\t", row[i]);}printf("\n");}// 8. 释放资源 - 结果集mysql_free_result(res);// 9. 写数据库// 以下三条是一个完整的操作, 对应的是一个事务// 设置事务为手动提交mysql_autocommit(mysql, 0); int ret1 = mysql_query(mysql, "insert into dept values(61, '海军', '圣地玛丽乔亚')");int ret2 = mysql_query(mysql, "insert into dept values(62, '七武海', '世界各地')");int ret3 = mysql_query(mysql, "insert into dept values(63, '四皇', '新世界')");printf("ret1 = %d, ret2 = %d, ret3 = %d\n", ret1, ret2, ret3);if(ret1==0 && ret2==0 && ret3==0){// 提交事务mysql_commit(mysql);}else{mysql_rollback(mysql);}// 释放数据库资源mysql_close(mysql);return 0;
}
1.4、C中的变量插入数据库中
以下是分别使用 动态 SQL
和 预处理语句
实现 MySQL 数据库的 增删查改(CRUD)
操作的完整示例程序。
1.4.1 动态SQL
#include <mysql/mysql.h>
#include <stdio.h>
#include <stdlib.h>// 连接到数据库
MYSQL* connect_to_db(const char *server, const char *user, const char *password, const char *database) {MYSQL *conn = mysql_init(NULL);if (!mysql_real_connect(conn, server, user, password, database, 0, NULL, 0)) {fprintf(stderr, "连接失败: %s\n", mysql_error(conn));exit(1);}return conn;
}// 插入数据
void insert_data(MYSQL *conn, const char *name, int age) {char query[100];snprintf(query, sizeof(query), "INSERT INTO users (name, age) VALUES ('%s', %d)", name, age);if (mysql_query(conn, query)) {fprintf(stderr, "插入失败: %s\n", mysql_error(conn));} else {printf("数据插入成功!\n");}
}// 删除数据
void delete_data(MYSQL *conn, int id) {char query[100];snprintf(query, sizeof(query), "DELETE FROM users WHERE id = %d", id);if (mysql_query(conn, query)) {fprintf(stderr, "删除失败: %s\n", mysql_error(conn));} else {printf("数据删除成功!\n");}
}// 查询数据
void query_data(MYSQL *conn) {if (mysql_query(conn, "SELECT * FROM users")) {fprintf(stderr, "查询失败: %s\n", mysql_error(conn));return;}MYSQL_RES *result = mysql_store_result(conn);if (result == NULL) {fprintf(stderr, "获取结果失败: %s\n", mysql_error(conn));return;}int num_fields = mysql_num_fields(result);MYSQL_ROW row;printf("查询结果:\n");while ((row = mysql_fetch_row(result))) {for (int i = 0; i < num_fields; i++) {printf("%s ", row[i] ? row[i] : "NULL");}printf("\n");}mysql_free_result(result);
}// 更新数据
void update_data(MYSQL *conn, int id, const char *new_name, int new_age) {char query[100];snprintf(query, sizeof(query), "UPDATE users SET name = '%s', age = %d WHERE id = %d", new_name, new_age, id);if (mysql_query(conn, query)) {fprintf(stderr, "更新失败: %s\n", mysql_error(conn));} else {printf("数据更新成功!\n");}
}int main() {const char *server = "localhost";const char *user = "root";const char *password = "password";const char *database = "my_db";MYSQL *conn = connect_to_db(server, user, password, database);// 插入数据insert_data(conn, "Alice", 25);// 查询数据query_data(conn);// 更新数据update_data(conn, 1, "Bob", 30);// 删除数据delete_data(conn, 1);// 查询数据query_data(conn);mysql_close(conn);return 0;
}
注意:这个方法主要是利用
snprintf
函数,将C语言中的变量传入字符数组中
,下面回顾一下**snprintf
** 函数
函数原型
int snprintf(char *str, size_t size, const char *format, ...);
参数说明
str
: 指向目标字符串的指针,格式化后的字符串将被写入到这个字符串中。size
: 指定要写入的最大字符数(包括字符串结尾的\0
),用来避免缓冲区溢出。format
: 格式化字符串,类似于printf
中的格式字符串。...
: 额外的参数,这是与格式化字符串中占位符相对应的值。返回值
- 成功时,返回写入目标字符串中的字符数(不包括
\0
,即目标字符串中最后的空字符)。- 如果输出的字符串被截断,则返回所需的字符数,如果返回值大于或等于
size
,则表示目标字符串不足以容纳完整的格式化输出。- 如果发生错误,则返回一个负值。
使用示例
下面是一个简单的例子来演示如何使用 snprintf
:
#include <stdio.h>int main() {char buffer[50]; // 定义一个字符数组作为缓冲区int value = 42;// 使用 snprintf 将格式化字符串写入缓冲区int n = snprintf(buffer, sizeof(buffer), "The answer is: %d", value);// 输出结果if (n < 0) {printf("Error occurred!\n");} else if (n >= sizeof(buffer)) {printf("Output was truncated. Needed size: %d\n", n);} else {printf("Formatted string: %s\n", buffer);}return 0;
}
说明
- 缓冲区: 在这个例子中,我们定义了一个字符数组
buffer
,它将用于存储格式化后的字符串。- snprintf: 我们调用
snprintf
来将格式化的字符串写入buffer
。我们指定了sizeof(buffer)
作为最大输出大小,这可以避免缓冲区溢出。- 返回值处理: 我们检查
snprintf
的返回值,以确定输出是否成功以及是否被截断。使用注意事项
- 总是应该指定最大缓冲区大小,以防止缓冲区溢出。
snprintf
是一个非常方便的函数,尤其是在构建动态字符串或者需要格式化输出的时候。通过这种方式,
snprintf
确保你可以安全地操作字符串,并有效地格式化输出。
1.4.2 预处理语句 (推荐使用)
在使用这种方法之前,需要先介绍两个方法: mysql_stmt_init
和 mysql_stmt_prepare
创建预处理语句。
mysql_stmt_init 函数
函数原型
MYSQL_STMT *mysql_stmt_init(MYSQL *mysql);
参数说明
参数 | 类型 | 说明 |
---|---|---|
mysql | MYSQL* | 已建立的MySQL连接句柄,由mysql_init() 和mysql_real_connect() 创建 |
返回值
- 成功:返回一个新的
MYSQL_STMT
预处理语句句柄 - 失败:返回NULL,可通过
mysql_error(mysql)
获取错误信息
功能
- 初始化一个新的预处理语句对象,为后续操作分配资源
mysql_stmt_prepare 函数
函数原型
int mysql_stmt_prepare(MYSQL_STMT *stmt, const char *query, unsigned long length);
参数说明
参数 | 类型 | 说明 |
---|---|---|
stmt | MYSQL_STMT* | 由mysql_stmt_init() 创建的预处理语句句柄 |
query | const char* | 要准备的SQL语句字符串 |
length | unsigned long | SQL语句的长度(字节数) |
返回值
- 成功:返回0
- 失败:返回非0值,可通过
mysql_stmt_error(stmt)
获取错误信息
功能
- 准备一个SQL语句进行预处理,解析并优化SQL语句
演示代码
#include <mysql/mysql.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 连接到数据库
MYSQL* connect_to_db(const char *server, const char *user, const char *password, const char *database) {MYSQL *conn = mysql_init(NULL);if (!mysql_real_connect(conn, server, user, password, database, 0, NULL, 0)) {fprintf(stderr, "连接失败: %s\n", mysql_error(conn));exit(1);}return conn;
}// 插入数据
void insert_data(MYSQL *conn, const char *name, int age) {//初始化预处理语句MYSQL_STMT *stmt = mysql_stmt_init(conn);const char *query = "INSERT INTO users (name, age) VALUES (?, ?)";// 准备一个SQL语句进行预处理,解析并优化SQL语句if (mysql_stmt_prepare(stmt, query, strlen(query))) {fprintf(stderr, "预处理语句失败: %s\n", mysql_stmt_error(stmt));return;}//使用 MYSQL_BIND 结构体绑定 C 语言变量到 SQL 参数。MYSQL_BIND bind[2];memset(bind, 0, sizeof(bind));// 绑定 namebind[0].buffer_type = MYSQL_TYPE_STRING;bind[0].buffer = (char *)name;bind[0].buffer_length = strlen(name);// 绑定 agebind[1].buffer_type = MYSQL_TYPE_LONG;bind[1].buffer = &age;bind[1].buffer_length = sizeof(age);if (mysql_stmt_bind_param(stmt, bind)) {fprintf(stderr, "绑定参数失败: %s\n", mysql_stmt_error(stmt));mysql_stmt_close(stmt);return;}if (mysql_stmt_execute(stmt)) {fprintf(stderr, "执行失败: %s\n", mysql_stmt_error(stmt));} else {printf("数据插入成功!\n");}mysql_stmt_close(stmt);
}// 删除数据
void delete_data(MYSQL *conn, int id) {//初始化预处理语句MYSQL_STMT *stmt = mysql_stmt_init(conn);const char *query = "DELETE FROM users WHERE id = ?";// 准备一个SQL语句进行预处理,解析并优化SQL语句if (mysql_stmt_prepare(stmt, query, strlen(query))) {fprintf(stderr, "预处理语句失败: %s\n", mysql_stmt_error(stmt));return;}//使用 MYSQL_BIND 结构体绑定 C 语言变量到 SQL 参数。MYSQL_BIND bind;memset(&bind, 0, sizeof(bind));// 绑定 idbind.buffer_type = MYSQL_TYPE_LONG;bind.buffer = &id;bind.buffer_length = sizeof(id);if (mysql_stmt_bind_param(stmt, &bind)) {fprintf(stderr, "绑定参数失败: %s\n", mysql_stmt_error(stmt));mysql_stmt_close(stmt);return;}if (mysql_stmt_execute(stmt)) {fprintf(stderr, "执行失败: %s\n", mysql_stmt_error(stmt));} else {printf("数据删除成功!\n");}mysql_stmt_close(stmt);
}// 查询数据
void query_data(MYSQL *conn) {if (mysql_query(conn, "SELECT * FROM users")) {fprintf(stderr, "查询失败: %s\n", mysql_error(conn));return;}MYSQL_RES *result = mysql_store_result(conn);if (result == NULL) {fprintf(stderr, "获取结果失败: %s\n", mysql_error(conn));return;}int num_fields = mysql_num_fields(result);MYSQL_ROW row;printf("查询结果:\n");while ((row = mysql_fetch_row(result))) {for (int i = 0; i < num_fields; i++) {printf("%s ", row[i] ? row[i] : "NULL");}printf("\n");}mysql_free_result(result);
}// 更新数据
void update_data(MYSQL *conn, int id, const char *new_name, int new_age) {//初始化预处理语句MYSQL_STMT *stmt = mysql_stmt_init(conn);const char *query = "UPDATE users SET name = ?, age = ? WHERE id = ?";// 准备一个SQL语句进行预处理,解析并优化SQL语句if (mysql_stmt_prepare(stmt, query, strlen(query))) {fprintf(stderr, "预处理语句失败: %s\n", mysql_stmt_error(stmt));return;}//使用 MYSQL_BIND 结构体绑定 C 语言变量到 SQL 参数。MYSQL_BIND bind[3];memset(bind, 0, sizeof(bind));// 绑定 new_namebind[0].buffer_type = MYSQL_TYPE_STRING;bind[0].buffer = (char *)new_name;bind[0].buffer_length = strlen(new_name);// 绑定 new_agebind[1].buffer_type = MYSQL_TYPE_LONG;bind[1].buffer = &new_age;bind[1].buffer_length = sizeof(new_age);// 绑定 idbind[2].buffer_type = MYSQL_TYPE_LONG;bind[2].buffer = &id;bind[2].buffer_length = sizeof(id);if (mysql_stmt_bind_param(stmt, bind)) {fprintf(stderr, "绑定参数失败: %s\n", mysql_stmt_error(stmt));mysql_stmt_close(stmt);return;}if (mysql_stmt_execute(stmt)) {fprintf(stderr, "执行失败: %s\n", mysql_stmt_error(stmt));} else {printf("数据更新成功!\n");}mysql_stmt_close(stmt);
}int main() {const char *server = "localhost";const char *user = "root";const char *password = "password";const char *database = "my_db";MYSQL *conn = connect_to_db(server, user, password, database);// 插入数据insert_data(conn, "Alice", 25);// 查询数据query_data(conn);// 更新数据update_data(conn, 1, "Bob", 30);// 删除数据delete_data(conn, 1);// 查询数据query_data(conn);mysql_close(conn);return 0;
}
关键点
- 使用
mysql_stmt_init
和mysql_stmt_prepare
创建预处理语句。- 使用
MYSQL_BIND
结构体绑定 C 语言变量到 SQL 参数。mysql_stmt_bind_param
方法进行绑定mysql_stmt_execute
方法进行执行- 优点:安全,防止 SQL 注入, 推荐在实际项目中使用。
数据库连接池
我们在进行数据库操作的时候为了提高数据库(关系型数据库)的访问瓶颈,除了在服务器端增加缓存服务器(例如redis)缓存常用的数据之外,还可以 增加连接池,来提高数据库服务器的访问效率。
一般来说,对于数据库操作都是在访问数据库的时候创建连接,访问完毕断开连接。但是如果 在高并发情况下,有些需要频繁处理的操作就会消耗很多的资源和时间
,比如:
- 建立通信连接的TCP三次握手
- 数据库服务器的连接认证
- 数据库服务器关闭连接时的资源回收
- 断开通信连接的TCP四次挥手
如果使用数据库连接池会减少这一部分的性能损耗。
接下来会基于MySql数据库(使用MySQL的API连接MySQL数据库)为大家讲解一下,如何使用C++11的相关新特性来实现一个数据库连接池。
连接池的设计
- 连接池只需要一个实例,所以连接池类应该是一个
单例模式的类
- 所有的数据库连接应该维护到一个安全的队列中
- 使用队列的目的是方便连接的添加和删除
- 所谓的安全指的是线程安全,也就是说需要使用互斥锁来保护队列数据的读写。
- 在需要的时候可以 从连接池中得到一个或多个可用的数据库连接
- 如果有可用连接,直接取出
- 如果没有可用连接,阻塞等待一定时长然后再重试
- 如果队列中
没有多余的可用连接,需要动态的创建新连接
- 如果队列中
空闲的连接太多,需要动态的销毁一部分
- 数据库操作完毕,需要将连接归还到连接池中
细节分析
1. 数据库连接的存储:可用使用STL中的队列queue
2. 连接池连接的动态创建:这部分工作需要交给一个单独的线程来处理
3. 连接池连接的动态销毁:这部分工作需要交给一个单独的线程来处理
4. 数据库连接的添加和归还:这是一个典型的生产者和消费者模型
- 消费者:需要访问数据库的线程,数据库连接被取出(消费)
- 生产者:专门负责创建数据库连接的线程
- 处理生产者和消费者模型需要使用条件变量阻塞线程
5. 连接池的默认连接数量:连接池中提供的可用连接的最小数量
- 如果不够就动态创建
- 如果太多就动态销毁
6. 连接池的最大连接数量:能够创建的最大有效数据库连接上限
7. 最大空闲时间:创建出的数据库连接在指定时间长度内一直未被使用,此时就需要销毁该连接。
8. 连接超时:消费者线程无法获取到可用连接是,阻塞等待的时间长度
综上所述,数据库连接池对应的单例模式的类的设计如下:
using namespace std;
/*
* 数据库连接池: 单例模式
* MySqlConn 是一个连接MySQL数据库的类
*/
class ConnectionPool
{
public:// 得到单例对象static ConnectionPool* getConnectPool();// 从连接池中取出一个连接shared_ptr<MySqlConn> getConnection();// 删除拷贝构造和拷贝赋值运算符重载函数ConnectionPool(const ConnectionPool& obj) = delete;ConnectionPool& operator=(const ConnectionPool& obj) = delete;private:// 构造函数私有化ConnectionPool();bool parseJsonFile();void produceConnection();void recycleConnection();void addConnection();string m_ip; // 数据库服务器ip地址string m_user; // 数据库服务器用户名string m_dbName; // 数据库服务器的数据库名string m_passwd; // 数据库服务器密码unsigned short m_port; // 数据库服务器绑定的端口int m_minSize; // 连接池维护的最小连接数int m_maxSize; // 连接池维护的最大连接数int m_maxIdleTime; // 连接池中连接的最大空闲时长int m_timeout; // 连接池获取连接的超时时长queue<MySqlConn*> m_connectionQ;mutex m_mutexQ;condition_variable m_cond;
};
项目代码如下:
1. 用C API 封装成C++类
-
类的定义
#pragma once #include <iostream> using namespace std; #include <chrono> using namespace chrono; #include <string> #include <mysql/mysql.h>class databaseConnect {public://初始化数据库连接databaseConnect();//释放数据库连接~databaseConnect();//连接数据库bool connect(string user, string passwd, string dbName, string ip, unsigned short port = 3306);//更新数据库连接bool update(string sql);//查询数据库连接bool query(string sql);//遍历查询得到的结果集bool next();//得到结果集中的字段值string value(int index);//事物操作bool transaction();//提交事物bool commit();//事物回滚bool rollback();//刷新起始的空闲时间点//在计算空闲时间点中,更新一下起始的时间void refreshAliveTime();//计算连接存活的总时长long long getAliveTime();private:void freeResult();//释放结果集private:MYSQL* m_conn = nullptr;//数据库对象MYSQL_RES* m_result = nullptr;//数据库结果集MYSQL_ROW m_row = nullptr;//结构为MYSQL_ROW的下一行结果(结果集中某一行的结果)steady_clock::time_point m_alivetime; };
-
类内实现
#include "databaseConnect.h"//构造函数初始化的结果:环境初始化+字符集设置utf-8 databaseConnect::databaseConnect() {//mysql 环境初始化m_conn = mysql_init(nullptr);//mysql 字符集设置 utf8mysql_set_character_set(m_conn, "utf8"); }//析构函数,用于关闭环境 databaseConnect::~databaseConnect() {//mysql 环境关闭if (m_conn != nullptr) {mysql_close(m_conn);}//类内私有成员函数,释放结结果集freeResult(); }//数据库连接操作 bool databaseConnect::connect(string user, string passwd, string dbName, string ip, unsigned short port) {//返回的是数据库对象MYSQL* ptr = mysql_real_connect(m_conn, ip.c_str(), user.c_str(), passwd.c_str(), dbName.c_str(), port, nullptr, 0);return ptr != nullptr; }//更新数据库连接包括,删、插入、修改,传入参数是MySql执行语句 bool databaseConnect::update(string sql) {if (mysql_query(m_conn, sql.c_str())) return false;return true; }//查询数据库连接,传入参数是MySql执行语句 bool databaseConnect::query(string sql) {freeResult();//保存结果时,清空上一次的结果if (mysql_query(m_conn, sql.c_str())) return false;// 从服务器检索完整的结果集并将其存储在客户端内存中// 返回值是MYSQL_RES* m_result ,数据库结果集m_result = mysql_store_result(m_conn);return true; }// 遍历查询得到的结果集 // 这里是调用一次next函数,就获取结果集中的下一行 bool databaseConnect::next() {if (m_result != nullptr) {m_row = mysql_fetch_row(m_result);if (m_row != nullptr) return true;}return false; }// 得到结果集中的字段值 //参数index表示是要得到第几列的字段值 string databaseConnect::value(int index) {// 获取结果集中列的个数int listCount = mysql_num_fields(m_result);//若是给出的索引值index越界,那么直接返回if (index >= listCount || index < 0) return string();//否则首先得到字段值,m_row是MYSQL_ROW类型,就是单行结果集的内容char* val = m_row[index];//返回值为char* 有'\0'//取出第index列的属性长度unsigned long length = mysql_fetch_lengths(m_result)[index];//去除'\0',截取数据部分return string(val, length); }//事物操作 bool databaseConnect::transaction() {// 设置false 表示需要自己手动提交return mysql_autocommit(m_conn, false); }//提交事物 bool databaseConnect::commit() {return mysql_commit(m_conn); }//事物回滚 bool databaseConnect::rollback() {return mysql_rollback(m_conn); }// 释放结果集函数 void databaseConnect::freeResult() {//先判断结果集成员MYSQL_RES* m_result是否为空if (m_result) {mysql_free_result(m_result);//释放后,为了保险起见记得置为NULLm_result = nullptr;} }//刷新起始的空闲时间点 //在计算空闲时间点中,更新一下起始的时间 void databaseConnect::refreshAliveTime() {m_alivetime = steady_clock::now(); }//计算连接存活的总时长 //调用类中成员方法refreshAliveTime后,再调用此方法就可以更新当前连接的存活时间 long long databaseConnect::getAliveTime() {nanoseconds res = steady_clock::now() - m_alivetime;milliseconds millisec = duration_cast<milliseconds>(res);return millisec.count(); }
2. 连接池设计
-
连接池类的头文件
#pragma once #include <queue> #include "databaseConnect.h" #include <mutex> #include <condition_variable>class connectPool {public:static connectPool* getConnectPool(); //获取连接池实例对象connectPool(const connectPool& obj) = delete; //单例模式,这里拷贝构造直接禁用connectPool& operator=(const connectPool& obj) = delete; //单例模式,这里的赋值重载也禁用shared_ptr<databaseConnect> getConnection(); //获取数据库连接~connectPool();private:connectPool();//懒汉模式——用到时创建,构造函数必须是私有的,或者直接禁用bool parseJsonFile(); //解析连接数据的一些信息void produceConnection(); //生产者,用于往数据库连接队列中增加数据连接void recycleConnection(); // 回收者(管理者),如果某些连接超时m_maxIdTime,那么就会从数据库连接中删除并断开连接void addConnection();private:string m_ip;string m_user;string m_passwd;string m_dbName;unsigned short m_port;//连接池中,可连接数范围int m_minSize;int m_maxSize;//超时时间int m_timeout; // 当消费者线程发现连接池是空连接的时候,int m_maxIdTime; // 队列中空连接最多空闲时间queue<databaseConnect*> m_connectQ;//队列存放数据库连接mutex m_mutexQ;//操作连接池锁住condition_variable m_cond;//生产者消费者,控制连接池连接个数 };
-
连接池类的源文件
#include "connectPool.h" #include <chrono> #include <jsoncpp/json/json.h> using namespace Json; #include <fstream> #include <thread>// 单例模式,实现一个静态成员方法,这里是得到一个连接池实例, // 并且由于是静态成员,所有的类对象共享一个实例静态成员,且返回的是对象的指针 // 由于是单例模式,所以还需要把连接池的有参构造函、拷贝构造函数都要删除 connectPool* connectPool::getConnectPool() {//这是一个静态局部变量,只能通过此方法中进行获取访问,但是声明周期和程序一样长static connectPool pool;return &pool; }// 解析json文件,这里存储的是连接数据库的以及一些私有成员变量的值 // 反序列化操作 bool connectPool::parseJsonFile() {ifstream ifs("dbconf.json");Reader rd;Value root;rd.parse(ifs, root);if (root.isObject()) {m_ip = root["ip"].asString();m_port = root["port"].asInt();m_user = root["userName"].asString();m_passwd = root["password"].asString();m_dbName = root["dbName"].asString();m_minSize = root["minSize"].asInt();m_maxSize = root["maxSize"].asInt();m_maxIdTime = root["maxIdTime"].asInt();m_timeout = root["timeout"].asInt();return true;}return false; }//生产者函数,往连接池队列中增加新的连接 void connectPool::produceConnection() {while (true) {unique_lock<mutex> locker(m_mutexQ);while (int(m_connectQ.size()) >= m_minSize) {m_cond.wait(locker);}addConnection();m_cond.notify_all(); //通知消费者线程,可以取出连接池队列中的连接} }//管理者函数(回收连接队列中的空闲超时连接) void connectPool::recycleConnection() {//管理连接池,如果连接时间超时,那么就删除连接while (true) {this_thread::sleep_for(chrono::milliseconds(500));//上锁,超出作用域自动析构解锁lock_guard<mutex> locker(m_mutexQ);//如是数据库连接队列中的连接个数超出最小个数,那就可以进一步判断是否存在超时连接while (int(m_connectQ.size()) > m_minSize) {//取出队列头部的连接,因为头部的连接一定是队列中空闲连接时间最长的databaseConnect* conn = m_connectQ.front();//若是这个连接超出m_maxIdTime,那么就从队列中删除//所有的连接在进入队列前都会更新一下计算空闲时长的起始时间点if (conn->getAliveTime() >= m_maxIdTime) {m_connectQ.pop();delete conn;}//若是没有超时,那么就跳出内部的循环,再次进入下一轮的检测else break;}} }//往连接池队列中增加连接 void connectPool::addConnection() {databaseConnect* conn = new databaseConnect;conn->connect(m_user, m_passwd, m_dbName, m_ip, m_port);//刷新一下计算空闲时长的起始时间点//所有的连接在进入队列前都会更新一下计算空闲时长的起始时间点conn->refreshAliveTime();//塞入队列m_connectQ.push(conn); }// 消费者线程,取出连接池队列中的头部连接 shared_ptr<databaseConnect> connectPool::getConnection() {// 上锁,超出作用域自动析构unique_lock<mutex> locker(m_mutexQ);//while循环判断,防止线程不同步,导致的冲突while (m_connectQ.empty()) {// 因为线程在阻塞期间是几乎不消耗资源的,因此当队列中没有连接时,可以先暂停并阻塞m_timeout时间// 此时,线程会释放锁的临界资源,允许其他线程进行修改临界资源// 期间若是有生产者生产连接,就可以解除阻塞,重新上锁if (cv_status::timeout == m_cond.wait_for(locker, chrono::milliseconds(m_timeout))) {//若是超出m_timeout时间队列中依旧没有连接,那么就跳出本次循环进入下一轮循环if (m_connectQ.empty()) continue;}}//自定义删除器,当智能指针计数为0的时候,自动调用这里的匿名函数//当连接不用了,那么我们不是真的要删除,还是要送回 连接池列队中shared_ptr<databaseConnect> connptr(m_connectQ.front(), [this](databaseConnect* conn) {lock_guard<mutex> locker(m_mutexQ);//刷新一下计算空闲时长的起始时间点//所有的连接在进入队列前都会更新一下计算空闲时长的起始时间点conn->refreshAliveTime();m_connectQ.push(conn);});m_connectQ.pop();m_cond.notify_all(); //已经消费一个连接池了,可以通知生产线程了return connptr; }connectPool::connectPool() {//加载配置文件if (!parseJsonFile()) return ;for (int i = 0; i < m_minSize; i++) {addConnection(); }// 添加两个线程,一个线程用于往连接池中生产连接,// 一个子线程用来管理线程池的数量(超出时间就销毁)thread prodecer(&connectPool::produceConnection, this);thread recycler(&connectPool::recycleConnection, this);prodecer.detach();recycler.detach(); }//删除连接池队列里的连接 connectPool::~connectPool() {while (!m_connectQ.empty()) {databaseConnect* conn = m_connectQ.front();m_connectQ.pop();;delete conn;} }
3. 主函数(测试)
#include "connectPool.h"
#include "databaseConnect.h"
/*
1.单线程:使用/不使用连接池不使用:非连接池,单线程,用时:42693912629纳秒,42693毫秒使用:连接池,单线程,用时:9150030844纳秒,9150毫秒
2.多线程:使用/不使用连接池不使用:非连接池,多线程,用时:60792322338纳秒,60792毫秒使用:连接池,多线程,用时:3522916191纳秒,3522毫秒
*/
void op1(int begin, int end) {for (int i = begin; i < end; i++) {databaseConnect conn;conn.connect("starry", "root", "dbConnectPool", "192.168.80.128");char sql[1024] = {0};sprintf(sql,"insert into person values(%d, 18, 'man', 'starry')", i);conn.update(sql);}
}void op2(connectPool* pool, int begin, int end) {for (int i = begin; i < end; i++) {shared_ptr<databaseConnect> conn = pool->getConnection();char sql[1024] = {0};sprintf(sql,"insert into person values(%d, 18, 'man', 'starry')", i);
// string sql = "insert into person values(" + to_string(i) + ", 18, 'man', 'yll')";
// conn->update(sql.c_str());conn->update(sql);}
}void test1() {
#if 0steady_clock::time_point begin = steady_clock::now();op1(0, 5000);steady_clock::time_point end = steady_clock::now();auto length = end - begin;cout << "非连接池,单线程,用时:" << length.count() << "纳秒," << length.count() / 1000000 << "毫秒" << endl;
#elseconnectPool* pool = connectPool::getConnectPool();steady_clock::time_point begin = steady_clock::now();op2(pool, 0, 5000);steady_clock::time_point end = steady_clock::now();auto length = end - begin;cout << "连接池,单线程,用时:" << length.count() << "纳秒," << length.count() / 1000000 << "毫秒" << endl;
#endif
}void test2() {
#if 0steady_clock::time_point begin = steady_clock::now();thread t1(op1, 0, 1000);thread t2(op1, 1000, 2000);thread t3(op1, 2000, 3000);thread t4(op1, 3000, 4000);thread t5(op1, 4000, 5000);t1.join();t2.join();t3.join();t4.join();t5.join();steady_clock::time_point end = steady_clock::now();auto length = end - begin;cout << "非连接池,多线程,用时:" << length.count() << "纳秒," << length.count() / 1000000 << "毫秒" << endl;
#elsesteady_clock::time_point begin = steady_clock::now();connectPool* pool = connectPool::getConnectPool();thread t1(op2, pool, 0, 1000);thread t2(op2, pool, 1000, 2000);thread t3(op2, pool, 2000, 3000);thread t4(op2, pool, 3000, 4000);thread t5(op2, pool, 4000, 5000);t1.join();t2.join();t3.join();t4.join();t5.join();steady_clock::time_point end = steady_clock::now();auto length = end - begin;cout << "连接池,多线程,用时:" << length.count() << "纳秒," << length.count() / 1000000 << "毫秒" << endl;
#endif
}int query() {databaseConnect conn;conn.connect("starry", "root", "dbConnectPool", "192.168.80.128");string sql = "insert into person values(6, 18, 'man', 'starry')";bool flag = conn.update(sql);cout << "flag value" << flag << endl;sql = "select * from person";conn.query(sql);while (conn.next()) {cout << conn.value(0) << ", "<< conn.value(1) << ", "<< conn.value(2) << ", "<< conn.value(3) << endl;}return 0;
}int main() {
// query();//连接测试test1();return 0;
}
4. 代码说明
1. 条件变量 wait_for
m_cond.wait_for(locker, chrono::milliseconds(m_timeout))
是 C++ 中条件变量(std::condition_variable
)的一个成员函数调用,用于线程同步。它的作用是在指定时间内等待条件满足,若超时或条件被触发则继续执行。以下是详细解释:
功能说明
-
等待条件
当前线程会阻塞(进入等待状态),直到以下两种情况之一发生:- 其他线程调用了
notify_one()
或notify_all()
,且关联的条件(用户定义的逻辑)变为真。 - 超时:等待时间超过指定的
m_timeout
毫秒。
- 其他线程调用了
-
自动释放锁
- 在等待期间,
locker
(通常是std::unique_lock<std::mutex>
)会被自动释放,允许其他线程获取锁并修改共享数据。 - 当条件被触发或超时后,线程会重新获取锁,继续执行后续代码。
- 在等待期间,
-
返回值
- 返回
std::cv_status::timeout
表示因超时结束等待。 - 返回
std::cv_status::no_timeout
表示条件被触发(需结合谓词检查实际条件)。
- 返回
参数解释
-
locker
必须是已锁定的std::unique_lock<std::mutex>
对象,用于保护共享数据的互斥锁。 -
chrono::milliseconds(m_timeout)
指定最大等待时长(单位为毫秒)。例如,100ms
表示最多等待100毫秒。
总结
wait_for
实现了限时等待条件的功能,结合互斥锁和谓词检查,能有效协调线程间的同步,避免无限阻塞。
2. shared_ptr 自定义删除器
//自定义删除器,当智能指针计数为0的时候,自动调用这里的匿名函数//当连接不用了,那么我们不是真的要删除,还是要送回 连接池列队中shared_ptr<databaseConnect> connptr(m_connectQ.front(), [this](databaseConnect* conn) {lock_guard<mutex> locker(m_mutexQ);//刷新一下计算空闲时长的起始时间点//所有的连接在进入队列前都会更新一下计算空闲时长的起始时间点conn->refreshAliveTime();m_connectQ.push(conn);});
这段代码定义了一个 shared_ptr
,用于管理数据库连接对象 databaseConnect
的生命周期。它的特点是自定义了删除器(deleter),当 shared_ptr
的引用计数归零时,不会直接删除连接对象,而是将连接放回连接池队列 m_connectQ
中,同时更新连接的空闲时间戳。以下是详细解析:
关键点总结
- 智能指针 + 自定义删除器:实现资源的自动管理。
- 线程安全:通过互斥锁保护共享资源(队列)。
- 连接复用:通过队列实现连接的缓存和重复利用。
3. 条件变量个数问题
上面的工程代码中,虽然有两个线程函数
// ...................
// ...................
// ...................//生产者函数,往连接池队列中增加新的连接
void connectPool::produceConnection() {while (true) {unique_lock<mutex> locker(m_mutexQ);while (int(m_connectQ.size()) >= m_minSize) {m_cond.wait(locker);}addConnection();m_cond.notify_all(); //通知消费者线程,可以取出连接池队列中的连接}
}// 消费者线程,取出连接池队列中的头部连接
shared_ptr<databaseConnect> connectPool::getConnection()
{// 上锁,超出作用域自动析构unique_lock<mutex> locker(m_mutexQ);//while循环判断,防止线程不同步,导致的冲突while (m_connectQ.empty()) {// 因为线程在阻塞期间是几乎不消耗资源的,因此当队列中没有连接时,可以先暂停并阻塞m_timeout时间// 此时,线程会释放锁的临界资源,允许其他线程进行修改临界资源// 期间若是有生产者生产连接,就可以解除阻塞,重新上锁if (cv_status::timeout == m_cond.wait_for(locker, chrono::milliseconds(m_timeout))) {//若是超出m_timeout时间队列中依旧没有连接,那么就跳出本次循环进入下一轮循环if (m_connectQ.empty()) continue;}}//自定义删除器,当智能指针计数为0的时候,自动调用这里的匿名函数//当连接不用了,那么我们不是真的要删除,还是要送回 连接池列队中shared_ptr<databaseConnect> connptr(m_connectQ.front(), [this](databaseConnect* conn) {lock_guard<mutex> locker(m_mutexQ);//刷新一下计算空闲时长的起始时间点//所有的连接在进入队列前都会更新一下计算空闲时长的起始时间点conn->refreshAliveTime();m_connectQ.push(conn);});m_connectQ.pop();m_cond.notify_all(); //已经消费一个连接池了,可以通知生产线程了return connptr;
}// ...................
// ...................
// ...................
这里只使用了一个条件变量,例如在getConnection
类方法中,如果调用了m_cond.notify_all();
,那么不仅会唤醒生产者线程中的produceConnection
类中的的阻塞线程:
while (int(m_connectQ.size()) >= m_minSize) {m_cond.wait(locker);}
同时也会唤醒,getConnection
类方法中阻塞部分
while (m_connectQ.empty()) {// 因为线程在阻塞期间是几乎不消耗资源的,因此当队列中没有连接时,可以先暂停并阻塞m_timeout时间// 此时,线程会释放锁的临界资源,允许其他线程进行修改临界资源// 期间若是有生产者生产连接,就可以解除阻塞,重新上锁if (cv_status::timeout == m_cond.wait_for(locker, chrono::milliseconds(m_timeout))) {//若是超出m_timeout时间队列中依旧没有连接,那么就跳出本次循环进入下一轮循环if (m_connectQ.empty()) continue;}}
但是这里是没有问题的,因为阻塞判断的过程中的都是while体循环,如果接触了阻塞,还是会进入下一轮循环去进行条件判断,直到满足条件。
4. 单例模式
在C++中,单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供全局访问点。根据实例化的时机,可以分为 懒汉模式(Lazy Initialization) 和 饿汉模式(Eager Initialization)。
1. 懒汉模式(Lazy Initialization)
特点:
- 延迟初始化:实例在第一次被访问时才创建。
- 线程安全:需要额外处理(如互斥锁或
std::call_once
)。 - 适用于资源敏感场景:避免不必要的初始化开销。
(1)基础懒汉模式(非线程安全)
class Singleton {
private:static Singleton* instance;Singleton() {} // 私有构造函数public:static Singleton* getInstance() {if (instance == nullptr) {instance = new Singleton();}return instance;}
};Singleton* Singleton::instance = nullptr; // 静态成员初始化
问题:
- 线程不安全:多个线程同时调用
getInstance()
可能导致多次实例化。
(2)线程安全的懒汉模式(加锁)
#include <mutex>class Singleton {
private:static Singleton* instance;static std::mutex mtx;Singleton() {}public:static Singleton* getInstance() {std::lock_guard<std::mutex> lock(mtx); // 加锁if (instance == nullptr) {instance = new Singleton();}return instance;}
};Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
优化:
- 双重检查锁定(Double-Checked Locking):减少锁的开销。
static Singleton* getInstance() {if (instance == nullptr) { // 第一次检查(不加锁)std::lock_guard<std::mutex> lock(mtx);if (instance == nullptr) { // 第二次检查(加锁)instance = new Singleton();}}return instance;
}
(3)C++11 线程安全懒汉模式(std::call_once
)
#include <mutex>class Singleton {
private:static Singleton* instance;static std::once_flag onceFlag;Singleton() {}public:static Singleton* getInstance() {std::call_once(onceFlag, []() {instance = new Singleton();});return instance;}
};Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::onceFlag;
优点:
- 更高效:
std::call_once
保证只初始化一次,比手动加锁更安全。
2. 饿汉模式(Eager Initialization)
特点:
- 提前初始化:程序启动时就创建实例。
- 线程安全:无需额外同步机制(C++保证静态变量的线程安全)。
- 适用于初始化开销小的场景。
(1)基础饿汉模式
class Singleton {
private:static Singleton* instance;Singleton() {}public:static Singleton* getInstance() {return instance;}
};Singleton* Singleton::instance = new Singleton(); // 程序启动时初始化
(2)C++11 改进版(局部静态变量)
class Singleton {
private:Singleton() {}public:static Singleton& getInstance() {static Singleton instance; // C++11 保证线程安全return instance;}
};
优点:
- 更简洁:无需手动管理指针。
- 自动析构:程序结束时自动调用析构函数。
3. 懒汉 vs 饿汉模式对比
特性 | 懒汉模式 | 饿汉模式 |
---|---|---|
初始化时机 | 第一次访问时 | 程序启动时 |
线程安全 | 需要额外处理 | 天然安全 |
资源占用 | 按需加载,节省资源 | 提前占用内存 |
适用场景 | 初始化开销大、资源敏感 | 初始化快、简单场景 |
4. 推荐方案
- C++11 及以上:使用 局部静态变量(饿汉模式改进版),既线程安全又简洁。
- 需要延迟初始化:使用
std::call_once
的懒汉模式。
// 推荐:C++11 饿汉模式(局部静态变量)
class Singleton {
public:static Singleton& getInstance() {static Singleton instance;return instance;}private:Singleton() = default;~Singleton() = default;Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;
};
关键点:
- 禁用拷贝构造和赋值(避免破坏单例)。
- 析构函数私有化(防止外部
delete
)。
这样既安全又高效,适用于大多数场景。 🚀
参考部分来源1:爱编程的大丙
参考部分来源2:Github 上的开源项目