C++并发与实战(1):实现一个高并发trie

写在前面的:

从这个p开始,我们开始项目实战cmu-15445 p0的编写,需要一些准备工作:

课程官网:https://15445.courses.cs.cmu.edu/fall2023/

github主页:https://github.com/cmu-db/bustub

本人的环境配置使用的是wsl2+clangd(编译)+cmake(构建)+vscode(IDE)+lldb(调试)(wsl1不支持lldb的部分功能,最好升级到2(虽然1的更轻量化),要提供虚拟化,模拟器什么的最好卸了(虽然我没卸,因为mrfz(doge)))还有格式插件clangformat,保持一个良好的码风是很重要的

其他一些无关的插件也最好卸了,以免出bug(环境这部分看个人喜好,喜欢什么用什么,笔者只是提供了个人用的比较舒服的一套)

具体环境搭建可以点这里:https://joytsing.cn/posts/40555/

1. trie.h学习笔记

1.1 MoveBlocked类

class MoveBlocked {
   public:
    explicit MoveBlocked(std::future<int> wait) : wait_(std::move(wait)) {}

    MoveBlocked(const MoveBlocked &) = delete;
    MoveBlocked(MoveBlocked &&that) noexcept {
        if (!that.waited_) {
            that.wait_.get();
        }
        that.waited_ = waited_ = true;
    }

    auto operator=(const MoveBlocked &) -> MoveBlocked & = delete;
    auto operator=(MoveBlocked &&that) noexcept -> MoveBlocked & {
        if (!that.waited_) {
            that.wait_.get();
        }
        that.waited_ = waited_ = true;
        return *this;
    }

    bool waited_{false};
    std::future<int> wait_;
};

在 C++ 中,按值传递的过程如下:

  1. 如果实参是右值,按值传递会调用移动构造函数
  2. 如果实参是左值,按值传递会调用拷贝构造函数

在MoveBlocked类中的应用

  • 当对象按值传递时,会触发移动构造函数(如果可用),std::future的移动操作会转移资源所有权
  • 通过删除拷贝构造函数(MoveBlocked(const MoveBlocked &) = delete),强制使用移动语义
  • 在代码中,std::future 禁用了拷贝构造,只支持移动构造,因此调用时必须传递一个右值(wait_(std::move(wait)))。

explicit关键字

explicit 关键字用于防止隐式类型转换,在没有 explicit 的情况

class MoveBlocked {
    public:
    // 没有 explicit
    MoveBlocked(std::future<int> wait) : wait_(std::move(wait)) {}
};

// 这时允许以下隐式转换:
std::future<int> fut = std::async([]() { return 42; });
MoveBlocked mb = fut;  // 编译通过:fut 被隐式转换为 MoveBlocked

所以我们使用强制显式构造可以让代码更清晰地表达意图,避免在不经意间创建 MoveBlocked 对象

1.2 Trie和TrieNode

// A TrieNode is a node in a Trie.
class TrieNode {
   public:
    // Create a TrieNode with no children.
    TrieNode() = default;

    // Create a TrieNode with some children.
    explicit TrieNode(std::map<char, std::shared_ptr<const TrieNode>> children) : children_(std::move(children)) {}

    virtual ~TrieNode() = default;

    // Clone returns a copy of this TrieNode. If the TrieNode has a value, the value is copied. The return
    // type of this function is a unique_ptr to a TrieNode.
    //
    // You cannot use the copy constructor to clone the node because it doesn't know whether a `TrieNode`
    // contains a value or not.
    //
    // Note: if you want to convert `unique_ptr` into `shared_ptr`, you can use `std::shared_ptr<T>(std::move(ptr))`.
    virtual auto Clone() const -> std::unique_ptr<TrieNode> { return std::make_unique<TrieNode>(children_); }

    // A map of children, where the key is the next character in the key, and the value is the next TrieNode.
    // You MUST store the children information in this structure. You are NOT allowed to remove the `const` from
    // the structure.
    std::map<char, std::shared_ptr<const TrieNode>> children_;

    // Indicates if the node is the terminal node.
    bool is_value_node_{false};

    // You can add additional fields and methods here except storing children. But in general, you don't need to add
    // extra fields to complete this project.
};

// A TrieNodeWithValue is a TrieNode that also has a value of type T associated with it.
template <class T>
class TrieNodeWithValue : public TrieNode {
   public:
    // Create a trie node with no children and a value.
    explicit TrieNodeWithValue(std::shared_ptr<T> value) : value_(std::move(value)) { this->is_value_node_ = true; }

    // Create a trie node with children and a value.
    TrieNodeWithValue(std::map<char, std::shared_ptr<const TrieNode>> children, std::shared_ptr<T> value)
        : TrieNode(std::move(children)), value_(std::move(value)) {
        this->is_value_node_ = true;
    }

    // Override the Clone method to also clone the value.
    //
    // Note: if you want to convert `unique_ptr` into `shared_ptr`, you can use `std::shared_ptr<T>(std::move(ptr))`.
    auto Clone() const -> std::unique_ptr<TrieNode> override {
        return std::make_unique<TrieNodeWithValue<T>>(children_, value_);
    }

    // The value associated with this trie node.
    std::shared_ptr<T> value_;
};

// A Trie is a data structure that maps strings to values of type T. All operations on a Trie should not
// modify the trie itself. It should reuse the existing nodes as much as possible, and create new nodes to
// represent the new trie.
//
// You are NOT allowed to remove any `const` in this project, or use `mutable` to bypass the const checks.
class Trie {
   private:
    // The root of the trie.
    std::shared_ptr<const TrieNode> root_{nullptr};

    // Create a new trie with the given root.
    explicit Trie(std::shared_ptr<const TrieNode> root) : root_(std::move(root)) {}

   public:
    // Create an empty trie.
    Trie() = default;

    // Get the value associated with the given key.
    // 1. If the key is not in the trie, return nullptr.
    // 2. If the key is in the trie but the type is mismatched, return nullptr.
    // 3. Otherwise, return the value.
    template <class T>
    auto Get(std::string_view key) const -> const T *;

    // Put a new key-value pair into the trie. If the key already exists, overwrite the value.
    // Returns the new trie.
    template <class T>
    auto Put(std::string_view key, T value) const -> Trie;

    // Remove the key from the trie. If the key does not exist, return the original trie.
    // Otherwise, returns the new trie.
    auto Remove(std::string_view key) const -> Trie;

    // Get the root of the trie, should only be used in test cases.
    auto GetRoot() const -> std::shared_ptr<const TrieNode> { return root_; }
};

我们注意到std::shared_ptr<const TrieNode> root_{nullptr};这句话

它使用了std::shared_ptr,它的作用类似指针却与引入了reference counting,让我们来介绍一下它

1. std::shared_ptr 的基本概念

  • 自动管理动态分配的对象,避免手动 delete
  • 引用计数 机制,每个 shared_ptr 维护一个引用计数(use count)。
  • 当最后一个 shared_ptr 被销毁时,自动释放资源
  • 支持 std::make_shared<T>(),可以避免额外的内存分配,提高效率。

2. std::shared_ptr 的基本用法

2.1 创建 shared_ptr

#include <iostream>
#include <memory>

struct Test {
    Test() { std::cout << "Test Constructor\n"; }
    ~Test() { std::cout << "Test Destructor\n"; }
};

int main() {
    std::shared_ptr<Test> ptr1 = std::make_shared<Test>();  // 推荐使用 make_shared
    {
        std::shared_ptr<Test> ptr2 = ptr1; // 引用计数 +1
        std::cout << "Use count: " << ptr1.use_count() << std::endl;
	    } // ptr2 作用域结束,引用计数 -1

    std::cout << "Use count: " << ptr1.use_count() << std::endl;
} // ptr1 作用域结束,Test 对象被销毁

输出

Test Constructor
Use count: 2
Use count: 1
Test Destructor

解释

  • ptr1 创建了 Test 对象,并维护引用计数 1
  • ptr2 共享 ptr1 所管理的对象,引用计数 2
  • ptr2 离开作用域,引用计数 1
  • ptr1 离开作用域,引用计数变为 0,对象被释放。

3. 什么时候使用 shared_ptr

适合场景

  • 需要多个对象共享同一个资源,并且需要自动管理生命周期。
  • 不能确定何时释放对象,而希望引用计数来自动管理。

不适合场景

  • 需要唯一所有权时,应该使用 std::unique_ptr
  • 循环引用风险的场景,应该使用 std::weak_ptr

4. 总结

功能shared_ptr
是否自动释放✅ 是
是否支持共享✅ 是
是否有引用计数✅ 是
是否线程安全✅ 线程安全(引用计数是原子操作)
适合场景资源需要共享,不能确定对象何时销毁

💡 最佳实践

  1. 尽量使用 std::make_shared<T>(),避免手动 new,减少内存分配开销。
  2. 函数参数尽量使用 const std::shared_ptr<T>&,避免不必要的引用计数增加。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值