一、JsonCpp 库
🔥 JSONCPP 是一个开源的 C++ 库,用于解析和生成 JSON(JavaScript Object Notation)数据。它提供了简单易用的接口,支持 JSON 的序列化和反序列化操作,适用于处理配置文件、网络通信数据等场景。
之前我在 【Linux网络#5】:应用层自定义协议 & 序列化 & 网络版计算器 也使用过 JsonCPP,要了解的可以看看那里内容
1. Json 数据格式
JSON 是一种轻量级的数据交换格式,采用完全独立于编程语言的文本格式来存储和表示数据。
比如:我们想表示一个 同学的信息
C 代码表示
c
char *name = "xx";
int age = 18;
float score[3] = {88.5, 99, 58};
Json 表示
json
{
"姓名" : "xx",
"年龄" : 18,
"成绩" : [88.5, 99, 58],
"爱好" :{
"书籍" : "西游记",
"运动" : "打篮球"
}
}
包含以下基本类型:
- 对象(Object) :键值对集合,用
{}
包裹,如{"name": "Alice", "age": 25}
。 - 数组(Array) :有序值列表,用
[]
包裹,如[1, "text", true]
。 - 值(Value) :可以是字符串、数字、布尔值、
null
、对象或数组。
在 JSONCPP 中,所有 JSON 数据均通过 Json::Value
类表示。
2. JsonCpp 介绍
🔥 Jsoncpp
库主要是用于实现 Json 格式数据的序列化和反序列化,它实现了将多个数据对象组织成为 json 格式字符串,以及将 Json 格式字符串解析得到多个数据对象的功能。
先看一下 Json 数据对象类的表示
- 功能:存储任意 JSON 数据,支持动态类型判断。
- 常用方法:
c++
class Json::Value{
Value& operator=(const Value &other); //Value重载了[]和=,因此所有的赋值和获取数据都可以通过
Value& operator[](const std::string& key);//简单的⽅式完成 val["name"] = "xx";
Value& operator[](const char* key); // 访问或创建键值对
Value removeMember(const char* key);//移除元素
const Value& operator[](ArrayIndex index) const; //val["score"][0]
Value& append(const Value& value);//添加数组元素val["score"].append(88);
ArrayIndex size() const;//获取数组元素个数 val["score"].size();
std::string asString() const;//转string string name = val["name"].asString();
const char* asCString() const;//转char* char *name = val["name"].asCString();
// 获取值(需确保类型正确)
Int asInt() const;//转int int age = val["age"].asInt();
float asFloat() const;//转float float weight = val["weight"].asFloat();
bool asBool() const;//转 bool bool ok = val["ok"].asBool();
// 判断类型
bool isObject() const;
bool isArray() const;
bool isString() const;
};
生成器(序列化接口 -- Writer)
c++
class JSON_API StreamWriter {
virtual int write(Value const& root, std::ostream* sout) = 0;
}
class JSON_API StreamWriterBuilder : public StreamWriter::Factory {
virtual StreamWriter* newStreamWriter() const;
}
// 使用如下:
Json::StreamWriterBuilder builder;
builder.settings_["indentation"] = " "; // 缩进两空格
std::string jsonStr = Json::writeString(builder, root);
解析器(反序列化接口--Reader)
c++
class JSON_API CharReader {
virtual bool parse(char const* beginDoc, char const* endDoc,
Value* root, std::string* errs) = 0;
}
class JSON_API CharReaderBuilder : public CharReader::Factory {
virtual CharReader* newCharReader() const;
}
// 使用如下:
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
JSONCPP_STRING errs;
bool success = reader->parse(jsonStr, jsonStr + strlen(jsonStr), &root, &errs);
小结,主要用的 三个类 如下:
-
Json::Value
类:中间数据存储类- 就需要先存储到
Json::Value
对象中如果要将数据对象进行序列化,如果要将数据传进行反序列化,就是解析后,将数据对象放入到JJson::Value
对象中
- 就需要先存储到
-
Json::StreamWriter
类:用于进行数据序列化-
Json::StreamWriter::write()
序列化函数 -
Json::StreamWriterBuilder类: Json::StreamWriter
工厂类 -- 用于生产Json:.StreamWriter
对象
-
-
Json::CharReader
类:反序列化类Json::CharReader::parse()
反序列化函数Json::CharReaderBuilder
:Json::CharReader
工厂类-用于生产Json::.CharReader
对象
3. Json cpp 使用
代码示例1 -- 序列化
c++
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#include <memory>
// 实现数据的序列化
void serialize()
{
const char *name = "小明";
int age = 18;
const char *sex = "男"; // 要用 const, 否则会报错
float score[3] = {88, 77.5, 66};
Json::Value student;
student["姓名"] = name;
student["年龄"] = age;
student["性别"] = sex;
// 数组元素的赋值通过 append 来进行
student["成绩"].append(score[0]);
student["成绩"].append(score[1]);
student["成绩"].append(score[2]);
Json::Value fav;
fav["书籍"] = "三国演义";
fav["运动"] = "rap";
student["爱好"] = fav; // 嵌套对象
// 实例化一个工厂类对象
Json::StreamWriterBuilder swb;
// 设置输出格式:禁用 Unicode 转义
swb["emitUTF8"] = true; // 确保输出 UTF-8 编码的中文字符
// 通过工厂类对象来生产派生类对象 -- 两种方法
// Json::StreamWriter *sw = swb.newStreamWriter(); // 这种不建议
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter()); // 建议这种
// 原因:使用 std::unique_ptr 管理资源能够避免手动管理 Json::StreamWriter 的生命周期,能自动释放资源
sw->write(student, &std::cout);
std::cout << std::endl;
}
int main()
{
serialize();
return 0;
}
结果如下:
c++
{
"姓名" : "小明",
"年龄" : 18,
"性别" : "男",
"成绩" :
[
88.0,
77.5,
66.0
],
"爱好" :
{
"书籍" : "三国演义",
"运动" : "rap"
}
}
代码示例2 -- 序列化封装
c++
#include <iostream>
#include <string>
#include <sstream>
#include <jsoncpp/json/json.h>
#include <memory>
// 实现数据的序列化
bool serialize(const Json::Value &val, std::string &body)
{
std::stringstream ss;
// 实例化一个工厂类对象
Json::StreamWriterBuilder swb;
// 设置输出格式:禁用 Unicode 转义
swb["emitUTF8"] = true; // 确保输出 UTF-8 编码的中文字符
// 通过工厂类对象来生产派生类对象 -- 两种方法
// Json::StreamWriter *sw = swb.newStreamWriter(); // 这种不建议
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter()); // 建议这种
// 原因:使用 std::unique_ptr 管理资源能够避免手动管理 Json::StreamWriter 的生命周期,能自动释放资源
int ret = sw->write(val, &ss);
if(ret != 0){
std::cout << "json serialize failed\n";
return false;
}
body = ss.str();
return true;
}
int main()
{
const char *name = "小明";
int age = 18;
const char *sex = "男"; // 要用 const, 不然会报错
float score[3] = {88, 77.5, 66};
Json::Value student;
student["姓名"] = name;
student["年龄"] = age;
student["性别"] = sex;
// 数组元素的赋值通过 append 来进行
student["成绩"].append(score[0]);
student["成绩"].append(score[1]);
student["成绩"].append(score[2]);
Json::Value fav;
fav["书籍"] = "三国演义";
fav["运动"] = "rap";
student["爱好"] = fav;
std::string body;
serialize(student, body);
std::cout << body << std::endl;
return 0;
}
相比于 代码示例 1,其好处如下:
代码复用性
- 功能独立 :将序列化逻辑封装到
serialize
函数中,使其可以被其他模块复用,而不仅仅局限于main
函数。 - 解耦输入输出 :序列化操作不再直接绑定到
std::cout
,而是生成一个字符串(body
),允许调用者决定如何使用结果(例如写入文件、网络传输等)。
使用 std::stringstream
的好处
(1) 内存中的数据操作
- 中间存储 :将序列化结果暂存到
std::stringstream
中,而不是直接输出到控制台或文件,允许后续对数据进行二次处理(例如加密、压缩)。
(2) 避免副作用
- 无副作用设计 :不直接修改外部状态(如
std::cout
),而是通过返回值传递结果,符合函数式编程的最佳实践。
(3) 跨平台兼容性
- 统一编码 :通过
std::stringstream
确保生成的 JSON 字符串是内存中的 UTF-8 编码数据,避免因终端编码问题导致的乱码。
(4) 单元测试友好
-
可验证性 :将结果存储为字符串后,可以方便地与预期值进行对比,支持自动化测试。
c++std::string expected = R"({"姓名":"小明","年龄":18})"; ASSERT_EQ(body, expected);
-
至于这个单元测试,等下会在 谷歌 Test 单元测试中演示
代码示例3 -- 反序列化
cpp
#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>
#include <sstream>
#include <memory>
bool Unseriablize(const std::string &body, Json::Value &val)
{
Json::CharReaderBuilder crb;
std::string errs;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
bool ret = cr->parse(body.c_str(),body.c_str() + body.size(), &val, &errs);
if(!ret)
{
std::cout << "Json Unserialize : " << errs << "\n";
return false;
}
return true;
}
int main()
{
std::string str = R"({"姓名":"IsLand", "年龄": 19, "成绩":[32, 45, 56]})";
Json::Value stu;
bool ret = Unseriablize(str, stu);
if(!ret) return -1;
std::cout << "姓名: " << stu["姓名"].asString() << "\n";
std::cout << "年龄: " << stu["年龄"].asString() << "\n";
int sz = stu["成绩"].size();
for(int i = 0; i < sz; i++){
std::cout << "成绩: " << stu["成绩"][i].asFloat() << "\n";
}
return 0;
}
4. 谷歌 Test 单元测试
基于 Google Test 框架的单元测试示例,展示如何验证 serialize
函数的正确性。我们将通过对比生成的 JSON 字符串与预期值,确保序列化逻辑符合预期
安装 Google Test ,如下:
bash
# Ubuntu/Debian
sudo apt-get install libgtest-dev
# macOS
brew install googletest
单元测试 serialize_test.cpp 代码如下:
c++
// serialize_test.cpp(测试代码)
#include "serialize.h"
#include <gtest/gtest.h>
#include <jsoncpp/json/json.h>
TEST(SerializeTest, BasicObject) {
Json::Value obj;
obj["姓名"] = "小明";
obj["年龄"] = 18;
obj["性别"] = "男";
std::string body;
ASSERT_TRUE(serialize(obj, body));
// 预期字符串改为紧凑格式
std::string expected = R"({"姓名":"小明","年龄":18,"性别":"男"})";
EXPECT_EQ(body, expected);
}
// 测试嵌套 JSON 对象
TEST(SerializeTest, NestedObject) {
Json::Value student;
student["姓名"] = "小明";
Json::Value fav;
fav["书籍"] = "三国演义";
fav["运动"] = "rap";
student["爱好"] = fav;
std::string body;
ASSERT_TRUE(serialize(student, body));
std::string expected = R"({"姓名":"小明","爱好":{"书籍":"三国演义","运动":"rap"}})";
EXPECT_EQ(body, expected);
}
// 测试包含数组的 JSON 对象
TEST(SerializeTest, ArrayValue) {
Json::Value student;
student["成绩"].append(88.0);
student["成绩"].append(77.5);
student["成绩"].append(66.0);
std::string body;
ASSERT_TRUE(serialize(student, body));
std::string expected = R"({"成绩":[88.0,77.5,66.0]})";
EXPECT_EQ(body, expected);
}
// 测试特殊字符(如中文)是否正常
TEST(SerializeTest, UnicodeCharacters) {
Json::Value obj;
obj["描述"] = "这是一个包含中文的字段:你好,世界!";
std::string body;
ASSERT_TRUE(serialize(obj, body));
// 预期结果直接使用 UTF-8 编码的中文字符
std::string expected = R"({"描述":"这是一个包含中文的字段:你好,世界!"})";
EXPECT_EQ(body, expected);
}
// 测试空值处理
TEST(SerializeTest, EmptyValue) {
Json::Value obj;
obj["空值"] = Json::nullValue;
std::string body;
ASSERT_TRUE(serialize(obj, body));
std::string expected = R"({"空值":null})";
EXPECT_EQ(body, expected);
}
TEST(SerializeTest, ErrorHandling) {
Json::Value invalid; // 默认是空对象
invalid = Json::nullValue; // 显式设置为 null
std::string body;
EXPECT_FALSE(serialize(invalid, body)); // 现在应返回 false
}
// Google Test 的主函数
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
(1) 使用 R"()"
原始字符串字面量
- 直接通过
R"({"key":"value"})"
定义多行字符串,避免转义字符干扰。 - 确保预期字符串与实际生成的 JSON 格式完全一致。
(2) 断言宏
ASSERT_TRUE(condition)
:如果条件为假,立即终止当前测试。EXPECT_EQ(actual, expected)
:验证实际值与预期值是否相等,但不终止测试。
(3) 测试覆盖场景
- 基本对象 :验证键值对的正确性。
- 嵌套对象 :确保嵌套的
Json::Value
被正确序列化。 - 数组 :验证数组元素的顺序和值。
- 中文字符 :确保
emitUTF8
配置生效,中文字符不被转义。 - 空值 :处理
null
类型的 JSON 值。 - 错误处理 :验证函数在异常情况下的返回值。
对之前写的封装后序列化进行一点修改,如下:
serialize.h
c++
#ifndef SERIALIZE_H
#define SERIALIZE_H
#include <jsoncpp/json/json.h>
#include <string>
bool serialize(const Json::Value &val, std::string &body);
#endif
serialize.cpp
c++
#include "serialize.h"
#include <sstream>
#include <memory>
#include <iostream>
// 实现数据的序列化
bool serialize(const Json::Value &val, std::string &body)
{
if (val.isNull()) { // 显式检查 null 值
std::cout << "Input is null\n";
return false;
}
std::stringstream ss;
// 实例化一个工厂类对象
Json::StreamWriterBuilder swb;
// 设置输出格式:禁用 Unicode 转义
swb["emitUTF8"] = true; // 确保输出 UTF-8 编码的中文字符
swb["indentation"] = ""; // 禁用缩进和换行
// 通过工厂类对象来生产派生类对象 -- 两种方法
// Json::StreamWriter *sw = swb.newStreamWriter(); // 这种不建议
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter()); // 建议这种
// 原因:使用 std::unique_ptr 管理资源能够避免手动管理 Json::StreamWriter 的生命周期,能自动释放资源
int ret = sw->write(val, &ss);
if(ret != 0){
std::cout << "json serialize failed\n";
return false;
}
body = ss.str(); // 将结果写入 stringstream
return true; // 转换为 std::string
}
注意:我们相比于之前的封装是禁止掉了缩进和换行,如果要打印之前的数据,就会显示如下:
bash
{"姓名":"小明","年龄":18,"性别":"男","成绩":[88.0,77.5,66.0],"爱好":{"书籍":"三国演义","运动":"rap"}}
Makefile
makefile
test: serialize_test.cpp serialize.cpp
g++ -o $@ $^ -std=c++17 -ljsoncpp -lgtest -lgtest_main -pthread
.PHONY:clean
clean:
rm -f js test
结果如下:

小结:使用 Google Test 代码的好处
使用 Google Test 框架进行单元测试的主要好处包括:
- 确保代码正确性 :验证功能逻辑和边界条件。
- 提高代码质量 :减少回归问题,强制模块化设计。
- 加速开发流程 :快速反馈,支持持续集成。
- 支持团队协作 :文档化代码行为,降低沟通成本。
- 提升开发信心 :减少手动测试,鼓励重构。
比如上面
- 验证序列化逻辑(BasicObject)
- 确保
serialize
函数能够正确生成 JSON 字符串。 - 如果未来修改了
serialize
的实现,测试会立即捕获问题。
- 确保
- 边界条件测试(EmptyValue)
- 验证函数对非法输入的处理是否符合预期。
- 防止未来意外修改导致函数接受无效输入。
通过编写全面的单元测试,可以显著提高我们代码的可靠性、可维护性和开发效率。
二、Muduo 库
1. 基本概念
🐇 Muduo
由陈硕大佬开发,是一个基于非阻塞IO 和事件驱动 的C++高并发TCP网络编程库 。它是一款基于主从Reactor 模型的网络库,其使用的线程模型是 one loop per thread。
1.1 主从 Reactor 模型
- 主 Reactor (
MainReactor
,通常由EventLoop
实现):- 负责监听新连接(
accept
事件),通过Acceptor
类实现。 - 使用
epoll
/poll
等多路复用机制监控监听套接字。
- 负责监听新连接(
- 从 Reactor (
SubReactor
,多个EventLoop
线程):- 每个
EventLoop
管理一组已建立的 TCP 连接(TcpConnection
)。 - 处理连接的读写事件、定时任务和用户回调。
- 每个
1.2 One Loop Per Thread
- 线程绑定 :每个
EventLoop
对象严格绑定到一个线程(通过EventLoop::loop()
在所属线程运行)。 - 资源隔离 :TCP 连接的生命周期由所属
EventLoop
管理,避免跨线程竞争。 - 性能优化 :通过线程局部存储(
ThreadLocal
)实现高效的事件循环访问。

2. 常见接口
① TcpServer 类基础介绍
c++
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;
typedef std::function<void (const TcpConnectionPtr&, Buffer*, Timestamp)> MessageCallback;
class InetAddress : public muduo::copyable
{
public:
InetAddress(StringArg ip, uint16_t port, bool ipv6 = false);
};
class TcpServer : noncopyable
{
public:
enum Option
{
kNoReusePort,
kReusePort,
};
TcpServer(EventLoop* loop, const InetAddress& listenAddr, const string& nameArg, Option option = kNoReusePort);
void setThreadNum(int numThreads);
void start();
// 当⼀个新连接建⽴成功的时候被调用
void setConnectionCallback(const ConnectionCallback& cb)
{ connectionCallback_ = cb; }
// 消息的业务处理回调函数---这是收到新连接消息的时候被调用的函数
void setMessageCallback(const MessageCallback& cb)
{ messageCallback_ = cb; }
};
-
职责:服务端入口,管理监听套接字和连接池。
-
关键流程:
- 构造时绑定
EventLoop
(主 Reactor)。 start()
启动监听,注册Acceptor
到主 Reactor。- 新连接到达时,通过轮询算法分配从 Reactor 管理。
- 构造时绑定
-
回调接口:
c++void setConnectionCallback(ConnectionCallback cb); // 连接建立/关闭回调 void setMessageCallback(MessageCallback cb); // 消息到达回调
② EventLoop 类基础介绍
c++
class EventLoop : noncopyable
{
public:
/// Loops forever.
/// Must be called in the same thread as creation of the object.
void loop();
/// Quits loop.
/// This is not 100% thread safe, if you call through a raw pointer,
/// better to call through shared_ptr<EventLoop> for 100% safety.
void quit();
TimerId runAt(Timestamp time, TimerCallback cb);
/// Runs callback after @c delay seconds.
/// Safe to call from other threads.
TimerId runAfter(double delay, TimerCallback cb);
/// Runs callback every @c interval seconds.
/// Safe to call from other threads.
TimerId runEvery(double interval, TimerCallback cb);
/// Cancels the timer.
/// Safe to call from other threads.
void cancel(TimerId timerId);
private:
std::atomic<bool> quit_;
std::unique_ptr<Poller> poller_;
mutable MutexLock mutex_;
std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_);
}
-
职责:事件循环核心,驱动 Reactor 模型运行。
-
关键成员:
c++std::unique_ptr<Poller> poller_; // 底层 IO 多路复用(epoll/poll) std::vector<Functor> pendingFunctors_; // 跨线程任务队列
-
核心方法:
c++void loop(); // 启动事件循环(必须在本线程调用) void quit(); // 停止循环 void runInLoop(Functor cb); // 跨线程安全的任务提交 TimerId runAfter(double delay, TimerCallback cb); // 定时器
③ TcpConnection 基础介绍
c++
class TcpConnection : noncopyable, public std::enable_shared_from_this<TcpConnection>
{
public:
/// Constructs a TcpConnection with a connected sockfd
///
/// User should not create this object.
TcpConnection(EventLoop* loop, const string& name,int sockfd,const InetAddress& localAddr,const InetAddress& peerAddr);
bool connected() const { return state_ == kConnected; }
bool disconnected() const { return state_ == kDisconnected; }
void send(string&& message); // C++11
void send(const void* message, int len);
void send(const StringPiece& message);
// void send(Buffer&& message); // C++11
void send(Buffer* message); // this one will swap data
void shutdown(); // NOT thread safe, no simultaneous calling
void setContext(const boost::any& context)
{ context_ = context; }
const boost::any& getContext() const
{ return context_; }
boost::any* getMutableContext()
{ return &context_; }
void setConnectionCallback(const ConnectionCallback& cb)
{ connectionCallback_ = cb; }
void setMessageCallback(const MessageCallback& cb)
{ messageCallback_ = cb; }
private:
enum StateE { kDisconnected, kConnecting, kConnected, kDisconnecting };
EventLoop* loop_;
ConnectionCallback connectionCallback_;
MessageCallback messageCallback_;
WriteCompleteCallback writeCompleteCallback_;
boost::any context_;
};
-
职责:管理单个 TCP 连接的生命周期和 IO 操作。
-
关键特性:
- 继承
std::enable_shared_from_this
,依赖智能指针管理生命周期。 - 通过
Channel
类注册到所属EventLoop
的Poller
。
- 继承
-
核心方法:
c++void send(const void* data, size_t len); // 线程安全的发送接口 void shutdown(); // 半关闭连接(写端) bool connected(); // 判断当前连接是否正常
-
状态迁移:
c++kDisconnected → kConnecting → kConnected → kDisconnecting → kDisconnected
④ TcpClient 类基础介绍
c++
class TcpClient : noncopyable
{
public:
// TcpClient(EventLoop* loop);
// TcpClient(EventLoop* loop, const string& host, uint16_t port);
TcpClient(EventLoop* loop, const InetAddress& serverAddr,const string& nameArg);
~TcpClient(); // force out-line dtor, for std::unique_ptr members.
void connect(); // 连接服务器 -- 非阻塞接口
void disconnect(); // 关闭连接
void stop();
// 获取客户端对应的通信连接Connection对象的接口,发起connect后,有可能还没有连接建⽴成功
TcpConnectionPtr connection() const
{
MutexLockGuard lock(mutex_);
return connection_;
}
// 注意: Muduo 库的客户端也是通过 Eventloop 进行 IO 事件监控 IO 处理的
// 连接服务器成功时的回调函数
void setConnectionCallback(ConnectionCallback cb)
{ connectionCallback_ = std::move(cb); }
// 收到服务器发送的消息时的回调函数
void setMessageCallback(MessageCallback cb)
{ messageCallback_ = std::move(cb);
private:
EventLoop* loop_;
ConnectionCallback connectionCallback_;
MessageCallback messageCallback_;
WriteCompleteCallback writeCompleteCallback_;
TcpConnectionPtr connection_ GUARDED_BY(mutex_);
};
/*
需要注意的是,因为 muduo 库 不管是服务端还是客户端都是异步操作(TcpClient的connect 是非阻塞操作)
对于客户端来说: 可能会出现 在调用 connection 接口还没有完全建立成功的时候, send 发送数据,这是不被允许的。
因此我们可以使⽤内置的 CountDownLatch 类进⾏计数同步控制
yinw
*/
class CountDownLatch : noncopyable
{
public:
explicit CountDownLatch(int count);
void wait(){ // 计数 > 0 则阻塞
MutexLockGuard lock(mutex_);
while (count_ > 0)
{
condition_.wait();
}
}
void countDown(){ // 计数 --, 为 0 时唤醒 wait
MutexLockGuard lock(mutex_);
--count_;
if (count_ == 0)
{
condition_.notifyAll();
}
}
int getCount() const;
private:
mutable MutexLock mutex_;
Condition condition_ GUARDED_BY(mutex_);
int count_ GUARDED_BY(mutex_);
};
-
职责:客户端入口,管理与服务端的单一连接。
-
异步连接:
c++void connect(); // 非阻塞连接,需通过 `connectionCallback_` 确认连接状态
-
同步控制:
- 使用
CountDownLatch
等待连接建立完成后再发送数据:
c++CountDownLatch latch(1); client.setConnectionCallback([&](const TcpConnectionPtr& conn) { if (conn->connected()) latch.countDown(); }); client.connect(); latch.wait(); // 等待连接成功
- 使用
⑤ Buffer 类基础介绍
c++
class Buffer : public muduo::copyable
{
public:
static const size_t kCheapPrepend = 8;
static const size_t kInitialSize = 1024;
explicit Buffer(size_t initialSize = kInitialSize)
: buffer_(kCheapPrepend + initialSize),
readerIndex_(kCheapPrepend),
writerIndex_(kCheapPrepend);
void swap(Buffer& rhs)
size_t readableBytes() const // 获取缓冲区可读数据大小
size_t writableBytes() const
const char* peek() const // 获取缓冲区中数据的起始地址
const char* findEOL() const // 行的结束位置
const char* findEOL(const char* start) const
void retrieve(size_t len)
void retrieveInt64()
void retrieveInt32() // 数据读取位置向后偏移 4 字节, 本质上就是删除起始位置的 4 字节数据
void retrieveInt16()
void retrieveInt8()
string retrieveAllAsString() // 从缓冲区取出所有数据, 当作string 返回, 并删除缓冲区中数据
string retrieveAsString(size_t len) // 从缓冲区取出 len 长度数据, 当作string 返回, 并删除缓冲区中数据
void append(const StringPiece& str)
void append(const char* /*restrict*/ data, size_t len)
void append(const void* /*restrict*/ data, size_t len)
char* beginWrite()
const char* beginWrite() const
void hasWritten(size_t len)
void appendInt64(int64_t x)
void appendInt32(int32_t x)
void appendInt16(int16_t x)
void appendInt8(int8_t x)
int64_t readInt64()
int32_t readInt32() // 是 peekInt32() 和 retrieveInt32() 功能的合并
int16_t readInt16()
int8_t readInt8()
int64_t peekInt64() const
int32_t peekInt32() const // 尝试从缓冲区获取 4 字节数据, 进行网络字节序转换为整形, 但是数据并不从缓冲区删除
int16_t peekInt16() const
int8_t peekInt8() const
void prependInt64(int64_t x)
void prependInt32(int32_t x)
void prependInt16(int16_t x)
void prependInt8(int8_t x)
void prepend(const void* /*restrict*/ data, size_t len)
private:
std::vector<char> buffer_;
size_t readerIndex_;
size_t writerIndex_;
static const char kCRLF[];
};
-
设计目标:高效处理非阻塞 IO 的读写缓冲。
-
内存布局:
bash[预留空间][可读数据][可写空间] |←kCheapPrepend→|←readableBytes→|←writableBytes→|
-
核心操作:
c++void append(const char* data, size_t len); // 追加到可写空间 void retrieve(size_t len); // 消费已读数据 string retrieveAllAsString(); // 提取全部可读数据
-
优化点:
-
预留
kCheapPrepend
空间,避免协议解析时的内存拷贝。 -
自动扩容机制减少频繁内存分配。
-
3. 线程模型与性能优化
3.1 线程分工
线程类型 | 职责 | 对应类 |
---|---|---|
Main Thread | 监听新连接,处理定时任务 | TcpServer |
IO Threads | 处理连接的读写事件 | TcpConnection |
Compute Threads | 执行业务逻辑(用户自定义) | 用户代码 |
3.2 性能优化策略
- 零拷贝优化 :
Buffer
类通过swap
避免数据拷贝。 - 对象池 :频繁创建的
TcpConnection
使用对象池复用。 - 批量写操作:合并多个小数据包写入,减少系统调用次数。
4. 代码示例
这里我们使用 Muduo 网络库来实现一个简单英译汉服务器 和 客户端,快速上手 Muduo 库
先把我们需要用的字典 dictionary.txt 准备好
text
# 空格分隔
island 岛屿
life 生活
passion 激情
love 爱
sun 太阳
moon 月亮
如果我们要从上面的字典来获取哈希,并且遍历,测试代码如下:
c++
#include <iostream>
#include <string>
#include <unordered_map>
#include <fstream>
#include <sstream>
// 从字典中获取
std::unordered_map<std::string, std::string> loadDictionary(const std::string &filename) {
std::unordered_map<std::string, std::string> dict;
std::ifstream file(filename);
if (!file.is_open()) {
std::cerr << "无法加载字典文件: " << filename << std::endl;
return dict;
}
std::string line;
while (std::getline(file, line)) {
// 忽略空行和注释行(以 # 开头)
if (line.empty() || line[0] == '#') {
continue;
}
// 使用空格分隔键值对
std::istringstream iss(line);
std::string key, value;
iss >> key; // 读取第一个单词作为 key
std::getline(iss, value); // 读取剩余部分作为 value
// 去除 value 的前后空格
value.erase(0, value.find_first_not_of(" \t"));
value.erase(value.find_last_not_of(" \t") + 1);
if (!key.empty() && !value.empty()) {
dict[key] = value;
}
}
return dict;
}
int main() {
auto dict = loadDictionary("dictionary.txt");
for (const auto &[k, v] : dict) {
std::cout << k << " => " << v << std::endl;
}
return 0;
}
server.cpp
c++
// 实现一个翻译服务器, 客户端发送过来一个英语单词, 返回一个汉语单词
#include <iostream>
#include <string>
#include <unordered_map>
#include <fstream>
#include <sstream>
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/Buffer.h>
#include <muduo/net/TcpConnection.h>
// 0.0.0.0: 本机上任意ip地址, 通常用于网络监听地址, 用于监听本机上网卡的所有监听端口
const std::string IP = "0.0.0.0";
class DictServer
{
public:
DictServer(int port = 8888): _server(&_baseloop,
muduo::net::InetAddress(IP, port),
"DictServer", muduo::net::TcpServer::kReusePort)
{
// 设置回调函数
// _server.setConnectionCallback(onConnection); // 需要做函数适配 -- 绑定 因此不能直接这样
_server.setConnectionCallback(std::bind(&DictServer::onConnection, this, std::placeholders::_1));
_server.setMessageCallback(std::bind(&DictServer::onMessage, this,
std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
}
void Start() // 启动服务
{
// 注意两个顺序
_server.start(); // 先开始监听
_baseloop.loop(); // 再开始死循环事件监控
}
private:
void onConnection(const muduo::net::TcpConnectionPtr &conn)
{
if(conn->connected()){
std::cout << "连接建立" << std::endl;
}else{
std::cout << "连接断开" << std::endl;
}
}
// 从字典中获取
std::unordered_map<std::string, std::string> loadDictionary(const std::string &filename) {
std::unordered_map<std::string, std::string> dict;
std::ifstream file(filename);
if (!file.is_open()) {
std::cerr << "无法加载字典文件: " << filename << std::endl;
return dict;
}
std::string line;
while (std::getline(file, line)) {
// 忽略空行和注释行(以 # 开头)
if (line.empty() || line[0] == '#') {
continue;
}
// 使用空格分隔键值对
std::istringstream iss(line);
std::string key, value;
iss >> key; // 读取第一个单词作为 key
std::getline(iss, value); // 读取剩余部分作为 value
// 去除 value 的前后空格
value.erase(0, value.find_first_not_of(" \t"));
value.erase(value.find_last_not_of(" \t") + 1);
if (!key.empty() && !value.empty()) {
dict[key] = value;
}
}
return dict;
}
void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
{
// static std::unordered_map<std::string, std::string> dict_map = {
// {"island", "岛屿"},
// {"life", "生活"},
// {"passion", "激情"},
// {"love", "爱"}
// };
dict_map = loadDictionary("dictionary.txt") ;
std::string msg = buf->retrieveAllAsString(); // 从缓冲区取出字符串
if (msg.empty()) {
std::cerr << "接收到空消息" << std::endl;
conn->shutdown(); // 关闭连接
return;
}
// 取出英文对应的中文
std::string res;
auto it = dict_map.find(msg);
if(it != dict_map.end()){res = it->second;}
else {res = "该单词未知! ";}
conn->send(res); // 发送数据
}
private:
muduo::net::EventLoop _baseloop; // baseloop 要放在 server上, 因为是通过其来构造 server 的
muduo::net::TcpServer _server;
std::unordered_map<std::string, std::string> dict_map;
};
int main()
{
DictServer server;
server.Start();
return 0;
}
client.cpp
c++
#include <iostream>
#include <string>
#include <unordered_map>
#include <muduo/net/TcpClient.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/Buffer.h>
#include <muduo/net/TcpConnection.h>
#include <muduo/base/CountDownLatch.h>
#include <muduo/net/EventLoopThread.h>
class DictClient
{
public:
DictClient(const std::string &sip, int sport):
_baseloop(_loopthread.startLoop()),
_downlatch(1),
_client(_baseloop, muduo::net::InetAddress(sip, sport), "DictClient")
{
_client.setConnectionCallback(std::bind(&DictClient::onConnection, this, std::placeholders::_1));
_client.setMessageCallback(std::bind(&DictClient::onMessage, this,
std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
// 连接服务器
_client.connect();
// 还需要保证发信息前, 连接建立完成
_downlatch.wait();
}
bool Send(const std::string &msg)
{
if(_conn->connected() == false){
std::cout << "连接断开, 发送失败\n";
return false;
}
_conn->send(msg);
return true;
}
private:
void onConnection(const muduo::net::TcpConnectionPtr &conn)
{
if(conn->connected()){
std::cout << "连接建立" << std::endl;
_downlatch.countDown(); // 计数 -- 为 0 时候唤醒
_conn = conn;
}else{
std::cout << "连接断开" << std::endl;
_conn.reset(); // 重置清空
}
}
// 接收响应处理数据
void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
{
std::string res = buf->retrieveAllAsString();
std::cout << res << std::endl;
}
private:
muduo::net::TcpConnectionPtr _conn;
muduo::CountDownLatch _downlatch;
muduo::net::EventLoopThread _loopthread;
muduo::net::EventLoop *_baseloop;
muduo::net::TcpClient _client;
};
int main()
{
DictClient client("127.0.0.1", 8888);
while(true)
{
std::string msg;
std::cin >> msg;
client.Send(msg);
}
return 0;
}
Makefile 文件如下
makefile
# 生成编译文件前, 需要指定 muduo 库路径(根据当前Makefile 的相对路径) -I 指定头文件路径
CFLAG = -I ../../build/release-install-cpp11/include/
# 链接库
LFLAG = -L ../../build/release-install-cpp11/lib -lmuduo_net -lmuduo_base -pthread # muduo_net 要放在 muduo_base 前面
all: server client
server: server.cpp
g++ -o $@ $^ -std=c++17 $(CFLAG) $(LFLAG)
client: client.cpp
g++ -o $@ $^ -std=c++17 $(CFLAG) $(LFLAG)
.PHONY:clean
clean:
rm -f server client
结果测试如下:
bash
lighthouse@VM-8-10-ubuntu:~/code/project/JSON-RPC/demo/muduo$ ./server
20250314 14:53:49.147999Z 844500 INFO TcpServer::newConnection [DictServer] - new connection [DictServer-0.0.0.0:8888#1] from 127.0.0.1:53212 - TcpServer.cc:80
连接建立
lighthouse@VM-8-10-ubuntu:~/code/project/JSON-RPC/demo/muduo$ ./client
20250314 15:00:04.139580Z 846793 INFO TcpClient::TcpClient[DictClient] - connector 0x55A318EC9090 - TcpClient.cc:69
20250314 15:00:04.139598Z 846793 INFO TcpClient::connect[DictClient] - connecting to 127.0.0.1:8888 - TcpClient.cc:107
连接建立
sun
太阳
island
岛屿
i
该单词未知!
5. 注意事项
- 线程安全 :
- 除
TcpConnection::send()
外,多数操作需在所属EventLoop
线程执行。 - 使用
runInLoop()
实现跨线程调用。
- 除
- 生命周期管理 :
TcpConnection
通过shared_ptr
管理,避免回调中对象提前析构。
- 资源限制 :
- 需配置最大连接数防止 DDOS 攻击(通过
TcpServer::setThreadNum()
控制线程数)。
- 需配置最大连接数防止 DDOS 攻击(通过
6. 补充 -- 函数适配
还记得我们在 server.cpp 代码中写了这么一段注释 + 代码吧,如下:
c++
// _server.setConnectionCallback(onConnection); // 需要做函数适配 -- 绑定 因此不能直接这样
_server.setConnectionCallback(std::bind(&DictServer::onConnection, this, std::placeholders::_1));
_server.setMessageCallback(std::bind(&DictServer::onMessage, this,
std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
6.1 为什么不能直接使用 onConnection
?
(1) 非静态成员函数的特殊性
在 C++ 中,非静态成员函数(如 DictServer::onConnection
)有一个隐式的参数:this
指针 。这个指针指向调用该函数的对象实例。
例如:
cpp
void DictServer::onConnection(const muduo::net::TcpConnectionPtr &conn) {
// 通过 this 访问当前对象的成员变量和方法
}
这意味着,非静态成员函数的签名实际上是这样的:
cpp
ReturnType FunctionName(ClassType* this, OtherParameters...);
因此,当你尝试将 onConnection
直接传递给 setConnectionCallback
时,编译器会报错,因为 onConnection
并不是一个普通的全局函数或静态函数,而是一个依赖于 this
的成员函数。
(2) 回调函数的要求
muduo::net::TcpServer::setConnectionCallback
的签名如下:
cpp
void setConnectionCallback(const ConnectionCallback& cb);
其中,ConnectionCallback
是一个函数指针或函数对象类型,通常定义为:
cpp
typedef std::function<void(const TcpConnectionPtr&)> ConnectionCallback;
std::function
要求传入的函数或可调用对象必须符合特定的签名:
cpp
void callback(const TcpConnectionPtr&);
由于 DictServer::onConnection
是一个非静态成员函数,它需要一个额外的 this
参数,因此无法直接满足上述签名要求。
6.2 使用 std::bind
进行函数适配
(1) std::bind
的作用
std::bind
是 C++ 标准库提供的工具,用于绑定函数及其参数,生成一个新的可调用对象(函数对象)。它可以将成员函数与其所属的对象绑定在一起,从而消除对 this
参数的显式依赖。
例如:
c++
_server.setConnectionCallback(std::bind(&DictServer::onConnection, this, std::placeholders::_1));
这段代码的作用是:
- 将
DictServer::onConnection
绑定到当前对象(this
)。 std::placeholders::_1
表示占位符,表示将来调用时的第一个参数(即TcpConnectionPtr
)。
最终生成的函数对象的签名是:
c++
void callback(const TcpConnectionPtr&);
这正好符合 setConnectionCallback
的要求。
(2) 为什么需要 std::placeholders
?
std::placeholders::_1
是占位符,表示将来调用时的实际参数。例如:
std::placeholders::_1
表示第一个参数。std::placeholders::_2
表示第二个参数,依此类推。
在你的代码中:
c++
std::bind(&DictServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
表示:
- 第一个参数是
TcpConnectionPtr
。 - 第二个参数是
Buffer*
。 - 第三个参数是
Timestamp
。
这些参数会在回调触发时由 muduo
框架自动提供。
6.3 如果不使用 std::bind
,还有什么选择?
(1) 使用 Lambda 表达式
从 C++11 开始,可以使用 Lambda 表达式代替 std::bind
。Lambda 表达式的语法更简洁且更直观。例如:
c++
_server.setConnectionCallback([this](const muduo::net::TcpConnectionPtr &conn) {
this->onConnection(conn);
});
_server.setMessageCallback([this](const muduo::net::TcpConnectionPtr &conn,
muduo::net::Buffer *buf,
muduo::Timestamp timestamp) {
this->onMessage(conn, buf, timestamp);
});
Lambda 表达式的优点:
- 更易读,逻辑清晰。
- 不需要显式使用
std::placeholders
。
(2) 使用静态成员函数
如果你不需要访问非静态成员变量,可以将回调函数声明为静态成员函数。静态成员函数没有隐式的 this
参数,因此可以直接传递给 setConnectionCallback
。例如:
c++
static void onConnectionStatic(const muduo::net::TcpConnectionPtr &conn);
_server.setConnectionCallback(DictServer::onConnectionStatic);
但这种方式的局限性在于,静态成员函数无法访问非静态成员变量或方法。
6.4 总结
(1) 为什么需要函数适配?
- 非静态成员函数需要
this
指针,而回调函数要求的是普通函数或函数对象。 std::bind
或 Lambda 表达式可以将成员函数与对象绑定,生成符合要求的函数对象。
(2) 函数适配的核心思想
std::bind
:将成员函数与对象绑定,并指定参数占位符。- Lambda 表达式 :更简洁的方式实现相同功能。
(3) 示例对比
c++
// 使用 std::bind
_server.setConnectionCallback(std::bind(&DictServer::onConnection, this, std::placeholders::_1));
// 使用 Lambda 表达式
_server.setConnectionCallback([this](const muduo::net::TcpConnectionPtr &conn) {
this->onConnection(conn);
});
三、C++ 11 异步操作
1. std::future 介绍
🈂️std::future
是C++11标准库中的一个模板类,它表示一个异步操作的结果。当我们在多线程编程中使用异步任务时,std:future可以帮助我们在需要的时候获取任务的执行结果。std::future的一个重要特性是能够阻塞当前线程,直到异步操作完成,从而确保我们在获取结果时不会遇到未完成的操作。
注意:std::future 本质上不是一个异步任务,而是一个辅助我们获取异步任务结果的东西
2. 核心组件概述
组件 | 作用 |
---|---|
std::async |
启动异步任务,返回 std::future 对象以获取结果。 |
std::future |
提供异步操作的最终结果(值或异常),只能移动(不可复制)。 |
std::promise |
存储异步操作的中间结果,通过 std::future 获取。 |
std::packaged_task |
将可调用对象(函数、Lambda)包装为异步任务,与 std::future 结合使用。 |
std::shared_future |
可复制的 future ,允许多次获取结果。 |
std::future
并不能单独使用,而是需要搭配一些能够执行异步任务的模板类或者函数一起使用,异步任务搭配使用:
std::asymc
函数模板:异步执行一个函数,返回一个future
对象用于获取函数结果std::packaged_task
类模板:为一个函数生成一个异步任务对象(可调用对象),用于在其他线程中执行std::promise
类模板:实例化的对象可以返回一个future
, 在其他线程中向promise
对象设置数据,其他线程的关联future
就可以获取数据
3. 应用场景
- 异步任务 :当我们需要在后台执行一些耗时操作时,如网络请求或计算密集型任务等,
std::future
可以用来表示这些异步任务的结果。通过将任务与主线程分离,我们可以实现任务的并行处理,从而提高程序的执行效率 - 并发控制 :在多线程编程中,我们可能需要等待某些任务完成后才能继续执行其他操作。通过使用
std:future
,我们可以实现线程之间的同步,确保任务完成后再获取结果并继续执行后续操作 - 结果获取 :
std:future
提供了一种安全的方式来获取异步任务的结果。我们可以使用std::future:get()
函数来获取任务的结果,此函数会阻塞当前线程,直到异步操作完成。这样,在调用get()函数时,我们可以确保已经获取到了所需的结果
场景 | 适用组件 | 示例 |
---|---|---|
简单异步任务 | std::async + std::future |
后台计算、文件读写 |
手动控制结果传递 | std::promise + std::future |
线程间传递复杂数据 |
多次执行同一任务 | std::packaged_task |
线程池中的任务调度 |
多消费者共享结果 | std::shared_future |
多个线程等待同一计算结果 |
4. 用法示例
4.1 async
使用 std::async
关联异步任务
std::async
是一种将任务与 std::future
关联的简单方法。它创建并运行一个异步任务,并返回一个与该任务结果关联的std::future对象。默认情况下,std:.async是否启动一个新线程,或者在等待future时,任务是否同步运行都取决于你给的 参数。
这个参数为 std::launch
类型:
std::launch:deferred
表明该函数会被延迟调用,直到在future
上调用get()
或者wait()
才会开始。- 执行任务
std::launch::async
表明函数会在自己创建的线程上运行 std::launch::deferred
、std::launch::async
内部通过系统等条件自动选择策略。
c++
#include <iostream>
#include <future>
#include <chrono>
#include <thread>
int Add(int num1, int num2)
{
std::cout << "into add\n";
return num1 + num2;
}
int main()
{
// 进行异步阻塞调用
std::future<int> fut = std::async(std::launch::async, Add, 11, 22);
// 休眠 1 s
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "----------------------------------\n" ;
// 获取异步执行的结果, 如果还没有结果就会阻塞
std::cout << fut.get() << "\n";
return 0;
}
// 输出
into add
----------------------------------
33
// 如果换成 launch::deferred
----------------------------------
into add
33
4.2 packaged_task
使用 std::packaged_task
和 std::future
配合
🔥 std::packaged_task
就是将任务和 std::feature
绑定在一起的模板(模板类 ),是一种对任务的封装(二次封装 封装成一个可调用对象作为任务放到其他线程执行)。我们可以通过 std:packaged_task
对象获取任务相关联的 std::feature
对象,通过调用 get_future()
方法获得。
std::packaged task
的模板参数是 函数签名。
可以把 std::future
和 std::async
看成是分开的,而 std::packaged_task
则是一个整体。
c++
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
#include <memory>
int Add(int num1, int num2)
{
return num1 + num2;
}
int main()
{
// 1. 封装任务
std::packaged_task<int(int, int)> task1(Add);
// 2. 执行任务包关联的 future 对象
std::future<int> fut1 = task1.get_future();
// 3. 执行任务
task1(1, 2); // 方法 1
// 4. 获取结果
std::cout << fut1.get() << std::endl;
// 方式2
std::packaged_task<int(int, int)> task2(Add); // 1. 封装任务
std::future<int> fut2 = task2.get_future();
std::thread t(std::move(task2), 11, 22); // 3. 执行任务
t.join(); // 还需要等待线程, 否则会抛异常
std::cout << fut2.get() << std::endl; // 4. 获取结果
// 方式 3 -- 异步执行任务(封装任务)
// std::packaged_task<int(int, int)> task(add);
// 此处可执⾏其他操作, 无需等待
// std::cout << "hello IsLand!" << std::endl;
// std::future<int> result_future = task.get_future();
//需要注意的是,task虽然重载了()运算符,但task并不是⼀个函数,
//std::async(std::launch::async, task, 1, 2); --错误用法
//所以导致它作为线程的⼊⼝函数时,语法上看没有问题,但是实际编译的时候会报错
// std::thread(task, 1, 2); ---错误用法
// ⽽packaged_task禁⽌了拷⻉构造,
// 且因为每个packaged_task所封装的函数签名都有可能不同,因此也⽆法当作参数⼀样传递
// 传引⽤不可取,毕竟任务在多线程下执⾏存在局部变量声明周期的问题,因此不能传引⽤
// 因此想要将⼀个packaged_task进⾏异步调⽤,
// 简单⽅法就只能是new packaged_task,封装函数传地址进⾏解引用调用了
// ⽽类型不同的问题,在使⽤的时候可以使⽤类型推导来解决
auto task3 = std::make_shared<std::packaged_task<int(int, int)>>(Add); // 1. 封装任务
std::future<int> fut3 = task3->get_future(); // 2. 执行任务包关联的 future 对象
std::thread thr([task3](){
(*task3)(111, 222);
}); // 3. 执行任务
thr.join(); // 还需要等待线程退出, 否则会抛异常
std::cout << fut3.get() << std::endl; // 4. 获取结果
return 0;
}
上面代码中演示了 3 种执行任务的方法,那么哪种方法更好呢??
方法 1:直接调用 task(1, 2)
c++
task1(1, 2);
工作原理
- 直接在主线程中同步调用
std::packaged_task
的函数调用操作符 (operator()
)。 - 任务的执行和结果获取都在主线程中完成。
特点
- 同步执行 :任务在主线程中运行,不会创建新线程。
- 简单直观 :适合不需要并发的任务。
- 无线程开销 :避免了线程创建和管理的开销。
适用场景
- 当任务非常简单且不需要并发时(例如计算简单的加法)。
- 不需要异步执行或并行化。
优点
- 简单易懂,代码量少。
- 避免线程管理的复杂性。
缺点
- 无法利用多核 CPU 的性能优势。
- 如果任务耗时较长,会阻塞主线程。
方法 2:通过 std::thread
执行任务
c++
std::thread t(std::move(task2), 11, 22);
t.join();
工作原理
- 将
std::packaged_task
移动到一个新线程中执行。 - 使用
std::move
将任务的所有权转移给线程。 - 调用
join()
等待线程完成。
特点
- 异步执行 :任务在单独的线程中运行,主线程可以继续执行其他操作。
- 线程管理 :需要手动管理线程的生命周期(如
join
或detach
)。
适用场景
- 当任务较耗时且需要并发执行时。
- 适合需要异步处理的场景(例如网络请求、文件 I/O 等)。
优点
- 可以充分利用多核 CPU 的性能。
- 主线程不会被阻塞,能够并发执行其他任务。
缺点
- 需要显式管理线程的生命周期(如
join
或detach
),否则会导致未定义行为。 - 创建线程有一定的开销,不适合频繁创建大量线程。
方法 3:通过 std::shared_ptr
和 Lambda 表达式执行任务
c++
auto task3 = std::make_shared<std::packaged_task<int(int, int)>>(Add);
std::future<int> fut3 = task3->get_future();
std::thread thr([task3](){
(*task3)(111, 222);
});
thr.join();
工作原理
- 使用
std::shared_ptr
管理std::packaged_task
的生命周期。 - 在线程中通过 Lambda 表达式调用任务。
- 调用
join()
等待线程完成。
特点
- 共享所有权 :通过
std::shared_ptr
共享任务的所有权,确保任务在线程完成后仍然有效。 - 异步执行 :任务在单独的线程中运行,主线程可以继续执行其他操作。
适用场景
- 当任务需要在线程之间共享时。
- 需要确保任务对象的生命周期安全(即使线程先于主线程结束)。
优点
- 更安全:通过
std::shared_ptr
管理任务对象的生命周期,避免提前销毁问题。 - 更灵活:可以通过 Lambda 表达式自定义线程的行为。
缺点
- 增加了代码复杂性(需要管理
std::shared_ptr
和 Lambda 表达式)。 - 相比方法 2,性能开销略高(因为引入了智能指针)。
4. 对比与选择
特性 | 方法 1 (直接调用) | 方法 2 (std::thread ) |
方法 3 (std::shared_ptr + Lambda) |
---|---|---|---|
执行方式 | 同步 | 异步 | 异步 |
线程管理 | 无需管理 | 需要手动管理 (join/detach ) |
需要手动管理 (join/detach ) |
任务生命周期 | 主线程负责 | 主线程负责 | 智能指针自动管理 |
代码复杂度 | 简单 | 中等 | 较复杂 |
适用场景 | 简单任务 | 耗时任务 | 需要共享任务对象的场景 |
5. 哪个更好?
(1) 如果任务简单且不需要并发
- 推荐方法 1 :直接调用
task(1, 2)
。 - 原因 :
- 代码简单,易于维护。
- 无需创建线程,避免额外开销。
(2) 如果任务耗时且需要并发
- 推荐方法 2 :通过
std::thread
执行任务。 - 原因 :
- 异步执行,充分利用多核 CPU。
- 代码相对简单,适合大多数异步任务。
(3) 如果需要共享任务对象或更复杂的线程逻辑
- 推荐方法 3 :通过
std::shared_ptr
和 Lambda 表达式执行任务。 - 原因 :
- 更安全,避免任务对象提前销毁。
- 更灵活,适合复杂的线程管理场景。
(4)结论
- 简单任务 :优先选择方法 1。
- 耗时任务 :优先选择方法 2。
- 复杂任务 :优先选择方法 3。
4.3 promise
std::promise
是一个模板类,是对于结果的封装
std:promise
提供了一种设置值的方式,它可以在设置之后通过相关联的std::future
对象进行读取。- 换种说法就是之前说过:
std::future
可以读取一个异步函数的返回值了,但是要等待就绪,而std::promise
就提供一种 方式手动让std::future
就绪
手动传递结果
c++
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
#include <memory>
int Add(int num1, int num2)
{
return num1 + num2;
}
int main()
{
// 1. 在使用的时候, 先实例化一个指定结果的 promise 对象
std::promise<int> pro;
// 2. 通过promise对象,获取相关联的 future 对象
std::future<int> fut = pro.get_future();
// 3. 在任意位置给 promise 设置数据,就可以通过 关联future 获取到这个设置的数据了
std::thread thr([&pro](){
int sum = Add(11, 22);
pro.set_value(sum);
});
std::cout << fut.get() << std::endl; // --> 3
// 如果不写 join 就会出现如下:
// terminate called without an active exception
// Aborted (core dumped)
thr.join();
return 0;
}
异常传递
c++
void task_with_exception(std::promise<void> prom) {
try {
throw std::runtime_error("Oops!");
} catch (...) {
prom.set_exception(std::current_exception());
}
}
int main() {
std::promise<void> prom;
std::future<void> fut = prom.get_future();
std::thread t(task_with_exception, std::move(prom));
t.join();
try {
fut.get();
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl; // 输出 "Oops!"
}
return 0;
}
4.4 shared_future
std::shared_future
:共享结果
c++
void print_result(std::shared_future<int> fut) {
std::cout << "Result: " << fut.get() << std::endl;
}
int main() {
std::promise<int> prom;
std::shared_future<int> sfut = prom.get_future().share();
std::thread t1(print_result, sfut);
std::thread t2(print_result, sfut);
prom.set_value(100);
t1.join();
t2.join();
return 0;
}
4.5 完整实例
示例1:异步并行计算
c++
#include <future>
#include <vector>
#include <numeric>
#include <iostream>
// 并行计算向量元素的平方和
int parallel_sum(const std::vector<int>& vec) {
auto mid = vec.begin() + vec.size() / 2;
// 分两部分异步计算
auto fut1 = std::async(std::launch::async, [&] {
return std::accumulate(vec.begin(), mid, 0, [](int a, int b) { return a + b * b; });
});
auto fut2 = std::async(std::launch::async, [&] {
return std::accumulate(mid, vec.end(), 0, [](int a, int b) { return a + b * b; });
});
return fut1.get() + fut2.get();
}
int main() {
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8};
std::cout << "Sum of squares: " << parallel_sum(data) << std::endl; // 输出 204
return 0;
}
5. 注意事项
future
的析构阻塞 :
std::future
析构时会等待异步任务完成。若需避免阻塞,可将future
存储到容器中。- 线程安全 :
std::future
不可复制,跨线程传递需用std::shared_future
。 - 异常处理 :
异步任务中的异常需通过promise::set_exception()
或future::get()
捕获。 - 性能权衡 :
频繁创建线程(如std::async
)可能带来开销,建议结合线程池使用。