【#2】介绍第三方库

一、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);

小结,主要用的 三个类 如下:

  1. Json::Value类:中间数据存储类

    • 就需要先存储到 Json::Value 对象中如果要将数据对象进行序列化,如果要将数据传进行反序列化,就是解析后,将数据对象放入到J Json::Value 对象中
  2. Json::StreamWriter类:用于进行数据序列化

    • Json::StreamWriter::write() 序列化函数

    • Json::StreamWriterBuilder类: Json::StreamWriter 工厂类 – 用于生产 Json:.StreamWriter 对象

  3. Json::CharReader类:反序列化类

    • Json::CharReader::parse() 反序列化函数
    • Json::CharReaderBuilderJson::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

结果如下:

image-20250314092524088

小结:使用 Google Test 代码的好处

使用 Google Test 框架进行单元测试的主要好处包括:

  1. 确保代码正确性 :验证功能逻辑和边界条件。
  2. 提高代码质量 :减少回归问题,强制模块化设计。
  3. 加速开发流程 :快速反馈,支持持续集成。
  4. 支持团队协作 :文档化代码行为,降低沟通成本。
  5. 提升开发信心 :减少手动测试,鼓励重构。

比如上面

  1. 验证序列化逻辑(BasicObject)
    • 确保 serialize 函数能够正确生成 JSON 字符串。
    • 如果未来修改了 serialize 的实现,测试会立即捕获问题。
  2. 边界条件测试(EmptyValue)
    • 验证函数对非法输入的处理是否符合预期。
    • 防止未来意外修改导致函数接受无效输入。

通过编写全面的单元测试,可以显著提高我们代码的可靠性、可维护性和开发效率。

二、Muduo 库

1. 基本概念

🐇 Muduo 由陈硕大佬开发,是一个基于非阻塞IO事件驱动的C++高并发TCP网络编程库。它是一款基于主从Reactor模型的网络库,其使用的线程模型是 one loop per thread

1.1 主从 Reactor 模型
  • 主 ReactorMainReactor,通常由 EventLoop 实现):
    • 负责监听新连接(accept 事件),通过 Acceptor 类实现。
    • 使用 epoll/poll 等多路复用机制监控监听套接字。
  • 从 ReactorSubReactor,多个 EventLoop 线程):
    • 每个 EventLoop 管理一组已建立的 TCP 连接(TcpConnection)。
    • 处理连接的读写事件、定时任务和用户回调。
1.2 One Loop Per Thread
  • 线程绑定:每个 EventLoop 对象严格绑定到一个线程(通过 EventLoop::loop() 在所属线程运行)。
  • 资源隔离:TCP 连接的生命周期由所属 EventLoop 管理,避免跨线程竞争。
  • 性能优化:通过线程局部存储(ThreadLocal)实现高效的事件循环访问。

image-20250314134028668

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; }
};
  • 职责:服务端入口,管理监听套接字和连接池。

  • 关键流程

    1. 构造时绑定 EventLoop(主 Reactor)。
    2. start() 启动监听,注册 Acceptor 到主 Reactor。
    3. 新连接到达时,通过轮询算法分配从 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 类注册到所属 EventLoopPoller
  • 核心方法

    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 性能优化策略
  1. 零拷贝优化Buffer 类通过 swap 避免数据拷贝。
  2. 对象池:频繁创建的 TcpConnection 使用对象池复用。
  3. 批量写操作:合并多个小数据包写入,减少系统调用次数。

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. 注意事项

  1. 线程安全
    • TcpConnection::send() 外,多数操作需在所属 EventLoop 线程执行。
    • 使用 runInLoop() 实现跨线程调用。
  2. 生命周期管理
    • TcpConnection 通过 shared_ptr 管理,避免回调中对象提前析构。
  3. 资源限制
    • 需配置最大连接数防止 DDOS 攻击(通过 TcpServer::setThreadNum() 控制线程数)。

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::deferredstd::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_taskstd::future 配合

🔥 std::packaged_task 就是将任务和 std::feature 绑定在一起的模板(模板类),是一种对任务的封装(二次封装 封装成一个可调用对象作为任务放到其他线程执行)。我们可以通过 std:packaged_task对象获取任务相关联的 std::feature 对象,通过调用 get_future()方法获得。

  • std::packaged task 的模板参数是 函数签名

可以把 std::futurestd::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() 等待线程完成。

特点

  • 异步执行 :任务在单独的线程中运行,主线程可以继续执行其他操作。
  • 线程管理 :需要手动管理线程的生命周期(如 joindetach)。

适用场景

  • 当任务较耗时且需要并发执行时。
  • 适合需要异步处理的场景(例如网络请求、文件 I/O 等)。

优点

  • 可以充分利用多核 CPU 的性能。
  • 主线程不会被阻塞,能够并发执行其他任务。

缺点

  • 需要显式管理线程的生命周期(如 joindetach),否则会导致未定义行为。
  • 创建线程有一定的开销,不适合频繁创建大量线程。

方法 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. 注意事项

  1. future 的析构阻塞
    std::future 析构时会等待异步任务完成。若需避免阻塞,可将 future 存储到容器中。
  2. 线程安全
    std::future 不可复制,跨线程传递需用 std::shared_future
  3. 异常处理
    异步任务中的异常需通过 promise::set_exception()future::get() 捕获。
  4. 性能权衡
    频繁创建线程(如 std::async)可能带来开销,建议结合线程池使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/40134.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Qt开发:QInputDialog的使用

文章目录 一、QInputDialog的介绍二、 QInputDialog的基本用法三、使用 QInputDialog的实例四、QInputDialog的信号与槽 一、QInputDialog的介绍 QInputDialog 是 Qt 提供的一个对话框类&#xff0c;用于获取用户输入的文本、整数或浮点数。它提供了简单易用的静态方法和可定制…

SCI一区 | Matlab实现DBO-TCN-LSTM-Attention多变量时间序列预测

SCI一区 | Matlab实现DBO-TCN-LSTM-Attention多变量时间序列预测 目录 SCI一区 | Matlab实现DBO-TCN-LSTM-Attention多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.【SCI一区级】Matlab实现DBO-TCN-LSTM-Attention多变量时间序列预测&#xff08;程…

Vulnhub-Thales通关攻略

第0步&#xff1a;网卡配置 靶机端&#xff1a;将下载好的靶机环境&#xff0c;导入 VritualBox&#xff0c;设置为 Host-Only 模式 Kali端&#xff1a;将 VMware 中桥接模式网卡设置为 VritualBox 的 Host-only 第一步&#xff1a;确定靶机IP #靶机IP 192.168.56.101#KaliIP 1…

JVM 02

今天是2025/03/23 19:07 day 10 总路线请移步主页Java大纲相关文章 今天进行JVM 3,4 个模块的归纳 首先是JVM的相关内容概括的思维导图 3. 类加载机制 加载过程 加载&#xff08;Loading&#xff09; 通过类全限定名获取类的二进制字节流&#xff08;如从JAR包、网络、动态…

Python学习笔记(7)关于列表创建,增加,删除

列表 **Python中一切都是对象 存放多个值的连续内存空间 大小可变 增加元素 a a[50]#➕运算符操作&#xff0c;产生了新对象 list.append(x) #将元素x增加至list尾部 list.extend(alist) #将列表alist增加至list尾部 list.insert(index.x) #将元素x插入list指定index位置 …

【图片识别Excel表格】批量将图片上的区域文字识别后保存为表格,基于WPF和阿里云的项目实战总结

一、项目背景 在信息处理和文档管理中,经常会遇到需要从大量图片中提取文字并进行整理的场景。例如,财务部门需要从大量报销票据中提取金额、日期等信息;法务部门需要从合同文档中提取关键条款;教育行业需要从试卷中提取学生的答题内容等。传统的手工处理方式不仅耗时长、…

【C语言】文件操作(详解)

个人主页 今天我们来讲一下有关文件的相关操作&#xff0c;希望看完这篇文章对你有所帮助&#xff0c;大力感谢你对博主的支持&#xff01; 文章目录 ⭐一、为什么使用文件&#x1f389;二、什么是文件2.1 程序文件2.2 数据文件2.3 文件名 &#x1f3a1;三、二进制文件和文本…

数据库中不存在该字段

mybatisplus 定义的类中某些字段是数据库里面没有的&#xff0c;我们可用tablefield(existfalse)来注解&#xff0c;演示如下&#xff1a;

计算机组成原理———I\O系统精讲<1>

本篇文章主要介绍输入输出系统的发展概况 一.输入输出系统的发展概况 1.早期阶段 该阶段的特点是I/O设备与主存交换信息都必须通过CPU 当时的I/O设备有如下几个特点&#xff1a; &#xff08;1&#xff09;每个I\O设备都必须配有一套独立的逻辑电路与CPU相连&#xff0c;用来…

Linux操作系统7- 线程同步与互斥7(RingQueue环形队列生产者消费者模型改进)

上篇文章&#xff1a;Linux操作系统7- 线程同步与互斥6&#xff08;POSIX信号量与环形队列生产者消费者模型&#xff09;-CSDN博客 本篇代码仓库&#xff1a;myLerningCode/l36 橘子真甜/Linux操作系统与网络编程学习 - 码云 - 开源中国 (gitee.com) 目录 一. 单生产单消费单保…

全面讲解python的uiautomation包

在常规的模拟鼠标和键盘操作&#xff0c;我们一般使用pyautogui&#xff0c;uiautomation模块不仅能直接支持这些操作&#xff0c;还能通过控件定位方式直接定位到目标控件的位置&#xff0c;而不需要自己去获取对应坐标位置。uiautomation模块不仅支持任意坐标位置截图&#x…

图解CNN、RNN、LSTM

一、CNN 二、RNN 三、LSTM 以上笔记参考自b站up主 自然卷小蛮&#xff08;自然卷小蛮的个人空间-自然卷小蛮个人主页-哔哩哔哩视频&#xff09;&#xff0c;感兴趣的可以去深入了解。

3.25学习总结 抽象类和抽象方法+接口+内部类+API

抽象类和抽象方法&#xff1a; 有抽象方法&#xff0c;那么类肯定是抽象类。父类不一定是抽象的&#xff0c;但如果父类中有抽象方法那一定是抽象类。 如果子类中都存在吃这个行为&#xff0c;但吃的具体东西不同&#xff0c;那么吃这个行为定义在父类里面就是抽象方法&#x…

Ubuntu22.04 UEFI系统配置Apache Tomcat/8.5.87为开机自动启动

前置条件&#xff0c;Java与Tomcat目录均为/usr/local路径下。 java安装目录为&#xff1a;/usr/local/java tomcat安装目录为&#xff1a;/usr/local/tomcat 1. 创建 Tomcat 专用用户和组&#xff08;可选但推荐&#xff09; # 创建 tomcat 用户组 sudo groupadd tomcat#…

MySQL复习

1基本操作复习 1.1数据库创建 创建数据库create database 数据库名;判断再创建数据库create database if not exists 数据库名;创建数据库指定字符集create database 数据库名 character set 字符集;创建数据库指定排序方式create database 数据库名 collate 排序方式;创建数据…

数据结构—树(java实现)

目录 一、树的基本概念1.树的术语2.常见的树结构 二、节点的定义三、有关树结构的操作1.按照数组构造平衡 二叉搜索树2.层序遍历树3.前、中、后序遍历树(1).前序遍历树(2).中序遍历树(3).后序遍历树(4).各种遍历的情况的效果对比 4.元素添加5.元素删除1.删除叶子节点2.删除单一…

SPI 机制与 Spring Boot AutoConfiguration 对比解析

一、架构效率革命性提升 1.1 类加载效率跃升 Spring Boot 2.7引入的AutoConfiguration.imports采用清单式配置加载&#xff0c;对比传统SPI机制&#xff1a; 传统SPI扫描路径&#xff1a;META-INF/services/** Spring Boot新方案&#xff1a;META-INF/spring/org.springfram…

node-red dashboard

安装&#xff1a; npm install node-red-dashboard 访问&#xff1a; http://127.0.0.1:1880/ui 1. 创建一个新的 Dashboard 页面: 在 Node-RED 编辑器中&#xff0c;拖动一个 ui_dashboard 节点到工作区&#xff0c;并将其连接到你的数据流。 2. 配置 Dashboard 节点: 双击…

深入理解现代C++在IT行业中的核心地位与应用实践

深入理解现代C在IT行业中的核心地位与应用实践 一、C在IT行业中的不可替代性 现代IT行业中&#xff0c;C凭借其零成本抽象和系统级控制能力&#xff0c;在以下关键领域保持不可替代地位&#xff1a; 应用领域C优势体现典型应用案例高性能计算直接内存管理&#xff0c;SIMD指令…

医院挂号预约小程序|基于微信小程序的医院挂号预约系统设计与实现(源码+数据库+文档)

医院挂号预约小程序 目录 基于微信小程序的医院挂号预约系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、小程序用户端 2、系统服务端 &#xff08;1&#xff09; 用户管理 &#xff08;2&#xff09;医院管理 &#xff08;3&#xff09;医生管理 &#xf…