前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【#2】介绍第三方库

【#2】介绍第三方库

作者头像
IsLand1314
发布2025-03-26 14:07:44
发布2025-03-26 14:07:44
6100
代码可运行
举报
文章被收录于专栏:学习之路学习之路
运行总次数:0
代码可运行

一、JsonCpp 库

🔥 JSONCPP 是一个开源的 C++ 库,用于解析和生成 JSON(JavaScript Object Notation)数据。它提供了简单易用的接口,支持 JSON 的序列化和反序列化操作,适用于处理配置文件、网络通信数据等场景。

之前我在 【Linux网络#5】:应用层自定义协议 & 序列化 & 网络版计算器 也使用过 JsonCPP,要了解的可以看看那里内容


1. Json 数据格式

JSON 是一种轻量级的数据交换格式,采用完全独立于编程语言的文本格式来存储和表示数据。

比如:我们想表示一个 同学的信息

C 代码表示

代码语言:javascript
代码运行次数:0
运行
复制
char *name = "xx";
int age = 18;
float score[3] = {88.5, 99, 58};

Json 表示

代码语言:javascript
代码运行次数:0
运行
复制
{
    "姓名" : "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 数据,支持动态类型判断。
  • 常用方法
代码语言:javascript
代码运行次数:0
运行
复制
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)

代码语言:javascript
代码运行次数:0
运行
复制
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)

代码语言:javascript
代码运行次数:0
运行
复制
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 – 序列化

代码语言:javascript
代码运行次数:0
运行
复制
#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;
}

结果如下:

代码语言:javascript
代码运行次数:0
运行
复制
{
        "姓名" : "小明",
        "年龄" : 18,
        "性别" : "男",
        "成绩" : 
        [
                88.0,
                77.5,
                66.0
        ],
        "爱好" : 
        {
                "书籍" : "三国演义",
                "运动" : "rap"
        }
}

代码示例2 – 序列化封装

代码语言:javascript
代码运行次数:0
运行
复制
#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) 单元测试友好

可验证性 :将结果存储为字符串后,可以方便地与预期值进行对比,支持自动化测试。

代码语言:javascript
代码运行次数:0
运行
复制
std::string expected = R"({"姓名":"小明","年龄":18})";
ASSERT_EQ(body, expected);

至于这个单元测试,等下会在 谷歌 Test 单元测试中演示

代码示例3 – 反序列化

代码语言:javascript
代码运行次数:0
运行
复制
#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 ,如下:

代码语言:javascript
代码运行次数:0
运行
复制
# Ubuntu/Debian
sudo apt-get install libgtest-dev

# macOS
brew install googletest

单元测试 serialize_test.cpp 代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
// 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

代码语言:javascript
代码运行次数:0
运行
复制
#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

代码语言:javascript
代码运行次数:0
运行
复制
#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
}

注意:我们相比于之前的封装是禁止掉了缩进和换行,如果要打印之前的数据,就会显示如下:

代码语言:javascript
代码运行次数:0
运行
复制
{"姓名":"小明","年龄":18,"性别":"男","成绩":[88.0,77.5,66.0],"爱好":{"书籍":"三国演义","运动":"rap"}}

Makefile

代码语言:javascript
代码运行次数:0
运行
复制
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
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
image-20250314134028668
2. 常见接口
① TcpServer 类基础介绍
代码语言:javascript
代码运行次数:0
运行
复制
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 管理。

回调接口

代码语言:javascript
代码运行次数:0
运行
复制
void setConnectionCallback(ConnectionCallback cb); // 连接建立/关闭回调
void setMessageCallback(MessageCallback cb);       // 消息到达回调

② EventLoop 类基础介绍
代码语言:javascript
代码运行次数:0
运行
复制
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 模型运行。

关键成员

代码语言:javascript
代码运行次数:0
运行
复制
std::unique_ptr<Poller> poller_;   // 底层 IO 多路复用(epoll/poll)
std::vector<Functor> pendingFunctors_; // 跨线程任务队列

核心方法

代码语言:javascript
代码运行次数:0
运行
复制
void loop();          // 启动事件循环(必须在本线程调用)
void quit();		 // 停止循环
void runInLoop(Functor cb); // 跨线程安全的任务提交
TimerId runAfter(double delay, TimerCallback cb); // 定时器

③ TcpConnection 基础介绍
代码语言:javascript
代码运行次数:0
运行
复制
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 类注册到所属 EventLoopPoller

核心方法

代码语言:javascript
代码运行次数:0
运行
复制
void send(const void* data, size_t len);  // 线程安全的发送接口
void shutdown();                          // 半关闭连接(写端)
bool connected();						  // 判断当前连接是否正常

状态迁移

代码语言:javascript
代码运行次数:0
运行
复制
kDisconnected → kConnecting → kConnected → kDisconnecting → kDisconnected

④ TcpClient 类基础介绍
代码语言:javascript
代码运行次数:0
运行
复制
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_);
};

职责:客户端入口,管理与服务端的单一连接。

异步连接

代码语言:javascript
代码运行次数:0
运行
复制
void connect();  // 非阻塞连接,需通过 `connectionCallback_` 确认连接状态

同步控制

  • 使用 CountDownLatch 等待连接建立完成后再发送数据:
代码语言:javascript
代码运行次数:0
运行
复制
CountDownLatch latch(1);
client.setConnectionCallback([&](const TcpConnectionPtr& conn) {
    if (conn->connected()) latch.countDown();
});
client.connect();
latch.wait();  // 等待连接成功

⑤ Buffer 类基础介绍
代码语言:javascript
代码运行次数:0
运行
复制
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 的读写缓冲。

内存布局

代码语言:javascript
代码运行次数:0
运行
复制
[预留空间][可读数据][可写空间]
|←kCheapPrepend→|←readableBytes→|←writableBytes→|

核心操作

代码语言:javascript
代码运行次数:0
运行
复制
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 准备好

代码语言:javascript
代码运行次数:0
运行
复制
# 空格分隔
island 岛屿
life 生活
passion 激情
love 爱
sun 太阳
moon 月亮

如果我们要从上面的字典来获取哈希,并且遍历,测试代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
#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

代码语言:javascript
代码运行次数:0
运行
复制
// 实现一个翻译服务器, 客户端发送过来一个英语单词, 返回一个汉语单词

#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

代码语言:javascript
代码运行次数:0
运行
复制
#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 文件如下

代码语言:javascript
代码运行次数:0
运行
复制
# 生成编译文件前, 需要指定 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

结果测试如下:

代码语言:javascript
代码运行次数:0
运行
复制
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 代码中写了这么一段注释 + 代码吧,如下:

代码语言:javascript
代码运行次数:0
运行
复制
// _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 指针 。这个指针指向调用该函数的对象实例。

例如:

代码语言:javascript
代码运行次数:0
运行
复制
void DictServer::onConnection(const muduo::net::TcpConnectionPtr &conn) {
    // 通过 this 访问当前对象的成员变量和方法
}

这意味着,非静态成员函数的签名实际上是这样的:

代码语言:javascript
代码运行次数:0
运行
复制
ReturnType FunctionName(ClassType* this, OtherParameters...);

因此,当你尝试将 onConnection 直接传递给 setConnectionCallback 时,编译器会报错,因为 onConnection 并不是一个普通的全局函数或静态函数,而是一个依赖于 this 的成员函数。

(2) 回调函数的要求

muduo::net::TcpServer::setConnectionCallback 的签名如下:

代码语言:javascript
代码运行次数:0
运行
复制
void setConnectionCallback(const ConnectionCallback& cb);

其中,ConnectionCallback 是一个函数指针或函数对象类型,通常定义为:

代码语言:javascript
代码运行次数:0
运行
复制
typedef std::function<void(const TcpConnectionPtr&)> ConnectionCallback;

std::function 要求传入的函数或可调用对象必须符合特定的签名:

代码语言:javascript
代码运行次数:0
运行
复制
void callback(const TcpConnectionPtr&);

由于 DictServer::onConnection 是一个非静态成员函数,它需要一个额外的 this 参数,因此无法直接满足上述签名要求。


6.2 使用 std::bind 进行函数适配

(1) std::bind 的作用

std::bind 是 C++ 标准库提供的工具,用于绑定函数及其参数,生成一个新的可调用对象(函数对象)。它可以将成员函数与其所属的对象绑定在一起,从而消除对 this 参数的显式依赖。

例如:

代码语言:javascript
代码运行次数:0
运行
复制
_server.setConnectionCallback(std::bind(&DictServer::onConnection, this, std::placeholders::_1));

这段代码的作用是:

  • DictServer::onConnection 绑定到当前对象(this)。
  • std::placeholders::_1 表示占位符,表示将来调用时的第一个参数(即 TcpConnectionPtr)。

最终生成的函数对象的签名是:

代码语言:javascript
代码运行次数:0
运行
复制
void callback(const TcpConnectionPtr&);

这正好符合 setConnectionCallback 的要求。

(2) 为什么需要 std::placeholders

std::placeholders::_1 是占位符,表示将来调用时的实际参数。例如:

  • std::placeholders::_1 表示第一个参数。
  • std::placeholders::_2 表示第二个参数,依此类推。

在你的代码中:

代码语言:javascript
代码运行次数:0
运行
复制
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 表达式的语法更简洁且更直观。例如:

代码语言:javascript
代码运行次数:0
运行
复制
_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。例如:

代码语言:javascript
代码运行次数:0
运行
复制
static void onConnectionStatic(const muduo::net::TcpConnectionPtr &conn);

_server.setConnectionCallback(DictServer::onConnectionStatic);

但这种方式的局限性在于,静态成员函数无法访问非静态成员变量或方法。


6.4 总结

(1) 为什么需要函数适配?

  • 非静态成员函数需要 this 指针,而回调函数要求的是普通函数或函数对象。
  • std::bind 或 Lambda 表达式可以将成员函数与对象绑定,生成符合要求的函数对象。

(2) 函数适配的核心思想

  • std::bind :将成员函数与对象绑定,并指定参数占位符。
  • Lambda 表达式 :更简洁的方式实现相同功能。

(3) 示例对比

代码语言:javascript
代码运行次数:0
运行
复制
// 使用 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 内部通过系统等条件自动选择策略。
代码语言:javascript
代码运行次数:0
运行
复制
#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_taskstd::future 配合

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

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

可以把 std::futurestd::async 看成是分开的,而 std::packaged_task则是一个整体。

代码语言:javascript
代码运行次数:0
运行
复制
#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)
代码语言:javascript
代码运行次数:0
运行
复制
task1(1, 2);

工作原理

  • 直接在主线程中同步调用 std::packaged_task 的函数调用操作符 (operator())。
  • 任务的执行和结果获取都在主线程中完成。

特点

  • 同步执行 :任务在主线程中运行,不会创建新线程。
  • 简单直观 :适合不需要并发的任务。
  • 无线程开销 :避免了线程创建和管理的开销。

适用场景

  • 当任务非常简单且不需要并发时(例如计算简单的加法)。
  • 不需要异步执行或并行化。

优点

  • 简单易懂,代码量少。
  • 避免线程管理的复杂性。

缺点

  • 无法利用多核 CPU 的性能优势。
  • 如果任务耗时较长,会阻塞主线程。

方法 2:通过 std::thread 执行任务
代码语言:javascript
代码运行次数:0
运行
复制
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 表达式执行任务
代码语言:javascript
代码运行次数:0
运行
复制
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 就绪

手动传递结果

代码语言:javascript
代码运行次数:0
运行
复制
#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;
}

异常传递

代码语言:javascript
代码运行次数: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共享结果

代码语言:javascript
代码运行次数:0
运行
复制
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:异步并行计算

代码语言:javascript
代码运行次数:0
运行
复制
#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. 注意事项
  1. future 的析构阻塞std::future 析构时会等待异步任务完成。若需避免阻塞,可将 future 存储到容器中。
  2. 线程安全std::future 不可复制,跨线程传递需用 std::shared_future
  3. 异常处理: 异步任务中的异常需通过 promise::set_exception()future::get() 捕获。
  4. 性能权衡: 频繁创建线程(如 std::async)可能带来开销,建议结合线程池使用。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、JsonCpp 库
    • 1. Json 数据格式
    • 2. JsonCpp 介绍
    • 3. Json cpp 使用
    • 4. 谷歌 Test 单元测试
  • 二、Muduo 库
    • 1. 基本概念
      • 1.1 主从 Reactor 模型
      • 1.2 One Loop Per Thread
    • 2. 常见接口
      • ① TcpServer 类基础介绍
      • ② EventLoop 类基础介绍
      • ③ TcpConnection 基础介绍
      • ④ TcpClient 类基础介绍
      • ⑤ Buffer 类基础介绍
    • 3. 线程模型与性能优化
      • 3.1 线程分工
      • 3.2 性能优化策略
    • 4. 代码示例
    • 5. 注意事项
    • 6. 补充 – 函数适配
      • 6.1 为什么不能直接使用 onConnection?
      • 6.2 使用 std::bind 进行函数适配
      • 6.3 如果不使用 std::bind,还有什么选择?
      • 6.4 总结
  • 三、C++ 11 异步操作
    • 1. std::future 介绍
    • 2. 核心组件概述
    • 3. 应用场景
    • 4. 用法示例
      • 4.1 async
      • 4.2 packaged_task
      • 4.3 promise
      • 4.4 shared_future
      • 4.5 完整实例
    • 5. 注意事项
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档