一、JsonCpp 库
🔥 JSONCPP 是一个开源的 C++ 库,用于解析和生成 JSON(JavaScript Object Notation)数据。它提供了简单易用的接口,支持 JSON 的序列化和反序列化操作,适用于处理配置文件、网络通信数据等场景。
之前我在 【Linux网络#5】:应用层自定义协议 & 序列化 & 网络版计算器 也使用过 JsonCPP,要了解的可以看看那里内容
1. Json 数据格式
JSON 是一种轻量级的数据交换格式,采用完全独立于编程语言的文本格式来存储和表示数据。
比如:我们想表示一个 同学的信息
C 代码表示
char *name = "xx";
int age = 18;
float score[3] = {88.5, 99, 58};
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 数据,支持动态类型判断。
- 常用方法:
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)
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)
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 – 序列化
#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;
}
结果如下:
{"姓名" : "小明","年龄" : 18,"性别" : "男","成绩" : [88.0,77.5,66.0],"爱好" : {"书籍" : "三国演义","运动" : "rap"}
}
代码示例2 – 序列化封装
#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) 单元测试友好
-
可验证性 :将结果存储为字符串后,可以方便地与预期值进行对比,支持自动化测试。
std::string expected = R"({"姓名":"小明","年龄":18})"; ASSERT_EQ(body, expected);
-
至于这个单元测试,等下会在 谷歌 Test 单元测试中演示
代码示例3 – 反序列化
#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 ,如下:
# Ubuntu/Debian
sudo apt-get install libgtest-dev# macOS
brew install googletest
单元测试 serialize_test.cpp 代码如下:
// 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; // 显式设置为 nullstd::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
#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
#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(); // 将结果写入 stringstreamreturn true; // 转换为 std::string
}
注意:我们相比于之前的封装是禁止掉了缩进和换行,如果要打印之前的数据,就会显示如下:
{"姓名":"小明","年龄":18,"性别":"男","成绩":[88.0,77.5,66.0],"爱好":{"书籍":"三国演义","运动":"rap"}}
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 类基础介绍
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 管理。
- 构造时绑定
-
回调接口:
void setConnectionCallback(ConnectionCallback cb); // 连接建立/关闭回调 void setMessageCallback(MessageCallback cb); // 消息到达回调
② EventLoop 类基础介绍
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 模型运行。
-
关键成员:
std::unique_ptr<Poller> poller_; // 底层 IO 多路复用(epoll/poll) std::vector<Functor> pendingFunctors_; // 跨线程任务队列
-
核心方法:
void loop(); // 启动事件循环(必须在本线程调用) void quit(); // 停止循环 void runInLoop(Functor cb); // 跨线程安全的任务提交 TimerId runAfter(double delay, TimerCallback cb); // 定时器
③ TcpConnection 基础介绍
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++11void send(const void* message, int len);void send(const StringPiece& message);// void send(Buffer&& message); // C++11void send(Buffer* message); // this one will swap datavoid shutdown(); // NOT thread safe, no simultaneous callingvoid 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
。
- 继承
-
核心方法:
void send(const void* data, size_t len); // 线程安全的发送接口 void shutdown(); // 半关闭连接(写端) bool connected(); // 判断当前连接是否正常
-
状态迁移:
kDisconnected → kConnecting → kConnected → kDisconnecting → kDisconnected
④ TcpClient 类基础介绍
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 时唤醒 waitMutexLockGuard 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_);
};
-
职责:客户端入口,管理与服务端的单一连接。
-
异步连接:
void connect(); // 非阻塞连接,需通过 `connectionCallback_` 确认连接状态
-
同步控制:
- 使用
CountDownLatch
等待连接建立完成后再发送数据:
CountDownLatch latch(1); client.setConnectionCallback([&](const TcpConnectionPtr& conn) {if (conn->connected()) latch.countDown(); }); client.connect(); latch.wait(); // 等待连接成功
- 使用
⑤ Buffer 类基础介绍
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() constconst char* peek() const // 获取缓冲区中数据的起始地址const char* findEOL() const // 行的结束位置const char* findEOL(const char* start) constvoid 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() constvoid 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() constint32_t peekInt32() const // 尝试从缓冲区获取 4 字节数据, 进行网络字节序转换为整形, 但是数据并不从缓冲区删除 int16_t peekInt16() constint8_t peekInt8() constvoid 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 的读写缓冲。
-
内存布局:
[预留空间][可读数据][可写空间] |←kCheapPrepend→|←readableBytes→|←writableBytes→|
-
核心操作:
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 准备好
# 空格分隔
island 岛屿
life 生活
passion 激情
love 爱
sun 太阳
moon 月亮
如果我们要从上面的字典来获取哈希,并且遍历,测试代码如下:
#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; // 读取第一个单词作为 keystd::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
// 实现一个翻译服务器, 客户端发送过来一个英语单词, 返回一个汉语单词#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; // 读取第一个单词作为 keystd::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
#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 文件如下
# 生成编译文件前, 需要指定 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.cppg++ -o $@ $^ -std=c++17 $(CFLAG) $(LFLAG)
client: client.cppg++ -o $@ $^ -std=c++17 $(CFLAG) $(LFLAG).PHONY:clean
clean:rm -f server client
结果测试如下:
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 代码中写了这么一段注释 + 代码吧,如下:
// _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
指针 。这个指针指向调用该函数的对象实例。
例如:
void DictServer::onConnection(const muduo::net::TcpConnectionPtr &conn) {// 通过 this 访问当前对象的成员变量和方法
}
这意味着,非静态成员函数的签名实际上是这样的:
ReturnType FunctionName(ClassType* this, OtherParameters...);
因此,当你尝试将 onConnection
直接传递给 setConnectionCallback
时,编译器会报错,因为 onConnection
并不是一个普通的全局函数或静态函数,而是一个依赖于 this
的成员函数。
(2) 回调函数的要求
muduo::net::TcpServer::setConnectionCallback
的签名如下:
void setConnectionCallback(const ConnectionCallback& cb);
其中,ConnectionCallback
是一个函数指针或函数对象类型,通常定义为:
typedef std::function<void(const TcpConnectionPtr&)> ConnectionCallback;
std::function
要求传入的函数或可调用对象必须符合特定的签名:
void callback(const TcpConnectionPtr&);
由于 DictServer::onConnection
是一个非静态成员函数,它需要一个额外的 this
参数,因此无法直接满足上述签名要求。
6.2 使用 std::bind
进行函数适配
(1) std::bind
的作用
std::bind
是 C++ 标准库提供的工具,用于绑定函数及其参数,生成一个新的可调用对象(函数对象)。它可以将成员函数与其所属的对象绑定在一起,从而消除对 this
参数的显式依赖。
例如:
_server.setConnectionCallback(std::bind(&DictServer::onConnection, this, std::placeholders::_1));
这段代码的作用是:
- 将
DictServer::onConnection
绑定到当前对象(this
)。 std::placeholders::_1
表示占位符,表示将来调用时的第一个参数(即TcpConnectionPtr
)。
最终生成的函数对象的签名是:
void callback(const TcpConnectionPtr&);
这正好符合 setConnectionCallback
的要求。
(2) 为什么需要 std::placeholders
?
std::placeholders::_1
是占位符,表示将来调用时的实际参数。例如:
std::placeholders::_1
表示第一个参数。std::placeholders::_2
表示第二个参数,依此类推。
在你的代码中:
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 表达式的语法更简洁且更直观。例如:
_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
。例如:
static void onConnectionStatic(const muduo::net::TcpConnectionPtr &conn);_server.setConnectionCallback(DictServer::onConnectionStatic);
但这种方式的局限性在于,静态成员函数无法访问非静态成员变量或方法。
6.4 总结
(1) 为什么需要函数适配?
- 非静态成员函数需要
this
指针,而回调函数要求的是普通函数或函数对象。 std::bind
或 Lambda 表达式可以将成员函数与对象绑定,生成符合要求的函数对象。
(2) 函数适配的核心思想
std::bind
:将成员函数与对象绑定,并指定参数占位符。- Lambda 表达式 :更简洁的方式实现相同功能。
(3) 示例对比
// 使用 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
内部通过系统等条件自动选择策略。
#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 sstd::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
则是一个整体。
#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;// 方式2std::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)
task1(1, 2);
工作原理
- 直接在主线程中同步调用
std::packaged_task
的函数调用操作符 (operator()
)。 - 任务的执行和结果获取都在主线程中完成。
特点
- 同步执行 :任务在主线程中运行,不会创建新线程。
- 简单直观 :适合不需要并发的任务。
- 无线程开销 :避免了线程创建和管理的开销。
适用场景
- 当任务非常简单且不需要并发时(例如计算简单的加法)。
- 不需要异步执行或并行化。
优点
- 简单易懂,代码量少。
- 避免线程管理的复杂性。
缺点
- 无法利用多核 CPU 的性能优势。
- 如果任务耗时较长,会阻塞主线程。
方法 2:通过 std::thread
执行任务
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 表达式执行任务
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
就绪
手动传递结果
#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;
}
异常传递
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
:共享结果
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:异步并行计算
#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; // 输出 204return 0;
}
5. 注意事项
future
的析构阻塞:
std::future
析构时会等待异步任务完成。若需避免阻塞,可将future
存储到容器中。- 线程安全:
std::future
不可复制,跨线程传递需用std::shared_future
。 - 异常处理:
异步任务中的异常需通过promise::set_exception()
或future::get()
捕获。 - 性能权衡:
频繁创建线程(如std::async
)可能带来开销,建议结合线程池使用。