ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入理解 oneTBB `concurrent_lru_cache::handle`:并发 LRU 缓存的值访问代理对象

深入理解 oneTBB `concurrent_lru_cache::handle`:并发 LRU 缓存的值访问代理对象 深入理解 oneTBBconcurrent_lru_cache::handle并发 LRU 缓存的值访问代理对象【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读concurrent_lru_cache::handle是 oneTBBoneAPI Threading Building Blocks并发 LRU 缓存容器对外暴露的唯一值访问入口它既是“引用计数器”又是“读写窗口”用户必须通过它读取或修改缓存中存储的值。本文以 handle_object_cls.rst 规范文档为主体结合本仓库内 oneTBB 的 concurrent_lru_cache.h 真实实现与 test_concurrent_lru_cache.cpp 测试用例完整讲解 handle 的生命周期语义、C11 前后的移动语义差异、全部成员函数契约以及它在容器驱逐策略中的关键作用帮助你在多线程场景下安全、正确地使用这个代理对象。注concurrent_lru_cache是 oneTBB 的预览preview特性位于本仓库 third-party/tbb 依赖目录中使用前需定义宏TBB_PREVIEW_CONCURRENT_LRU_CACHE。1. handle 是什么缓存的“代理值”而不是“值本身”concurrent_lru_cache容器将键映射到值并允许限制存储的未使用值的数量。它不会直接把值交给调用方而是返回一个代理对象——concurrent_lru_cache::handle。规范文档handle_object_cls.rst将其定义为Class that provides read and write access to values stored inconcurrent_lru_cache.即一个提供对concurrent_lru_cache中所存值的读写访问的类。为什么容器要“绕一道”而不是直接返回引用核心原因在于容器需要精确跟踪每个值的“在用”状态。从源码看concurrent_lru_cache.h 中operator[]的实现先通过聚合器aggregator串行化检索操作再将结果包装成 handle 返回handle operator[](key_type key) { retrieve_aggregator_operation op(key); my_aggregator.execute(op); if (op.is_new_value_needed()) { op.result().second.my_value my_value_function(key); op.result().second.my_is_ready.store(true, std::memory_order_release); } else { spin_wait_while_eq(op.result().second.my_is_ready, false); } return handle(*this, op.result()); }返回的 handle 内部持有两个指针指向缓存对象的my_lru_cache_ptr和指向存储记录map 节点的my_map_record_ptr见 handle_object 结构体。正是这两个指针让 handle 能够在析构/移动时向容器发出“使用结束”信号从而驱动 LRU 历史列表的维护。2. 生命周期语义引用计数与“在用”判定handle 最核心的语义是引用保持一个存活的handle对象会阻止容器删除它引用的值当 handle 被重新赋值或销毁时引用被释放当一个值的最后一个引用被释放后容器才被允许删除该值。换句话说concurrent_lru_cache的策略是存储所有当前在用的值加上有限数量的未用值多余的未用值按“最近最少使用”LRU策略淘汰。一个值是否“在用”完全由是否存在指向它的 handle 决定。这一机制在 signal_end_of_usage_serial 中体现得淋漓尽致当 handle 析构时调用signal_end_of_usage通过聚合器执行引用计数递减当my_ref_counter归零时该值进入 LRU 历史列表my_history_list若历史已满则从链表尾部驱逐最旧项并擦除 map 记录// if it was the last reference, put it to the LRU history if (! --(map_it-second.my_ref_counter)) { // if the LRU history is full, evict the oldest items to get space if (my_history_list.size() my_history_list_capacity) { ... my_storage_map.erase(map_it_to_evict); } my_history_list.push_front(map_it); map_it-second.my_history_list_iterator my_history_list.begin(); }而值的“领取”过程发生在 retrieve_serial命中时先从 LRU 历史中摘除该记录因为即将被使用再执行(map_it-second.my_ref_counter)。因此从源码结构可以推断每创建一个 handle引用计数加一每个 handle 析构引用计数减一容器的驱逐与保留完全以此为准。3. 不可复制为什么 handle 只能移动规范明确handle对象不能被复制。这保证了同一份“引用”在同一时刻只存在于一个 handle 中引用计数语义不会被复制操作破坏。针对不同 C 标准转移引用所有权的方式不同C 标准转移机制说明C11 及以后移动语义move semantics移动构造与移动赋值转移引用C11 之前handle_move_t辅助类型通过到/来自handle_move_t的隐式转换转移引用规范文档给出的完整类定义handle_object_cls.rst如下namespace oneapi { namespace tbb { template typename key_type, typename value_type, typename value_functor_type class concurrent_lru_cachekey_type, value_type, value_functor_type::handle { public: handle(); ~handle(); // Supported since C11 handle(handle src); handle operator(handle src); // Supported until C11 handle(handle_move_t m); handle operator(handle_move_t m); operator handle_move_t(); friend handle_move_t move(handle h); operator bool() const; value_type value(); private: void operator(handle); handle(handle); }; } // namespace tbb } // namespace oneapihandle_move_t是容器内部的一个辅助私有类型定义于concurrent_lru_cache的 private 区域用于在没有 C11 移动语义的环境下传递引用用户不得直接使用C11 引入移动语义后该类型已过时见 concurrent_lru_cache_cls.rst 成员表。4. 成员函数逐项解读4.1 默认构造与析构handle()构造一个不引用任何值的 handle空句柄。规范中该类成员的默认构造由实现提供在源码实现中handle_object 的默认构造将两个指针成员置空my_lru_cache_ptr(nullptr), my_map_record_ptr(nullptr)。~handle()释放如果存在对concurrent_lru_cache中值的引用。对应源码 析构函数若my_lru_cache_ptr非空则调用signal_end_of_usage(*my_map_record_ptr)通知容器该引用已结束。4.2 移动构造与移动赋值C11 起handle(handle src)把src持有的引用转移给新对象。完成后src不再引用任何值。源码实现handle_object 移动构造直接搬运两个指针并将src的指针置空移动前通过__TBB_ASSERT校验源对象状态合法两个指针要么同时非空、要么同时为空。handle operator(handle src)把src的引用转移给*this若*this此前已持有引用则先释放旧引用调用signal_end_of_usage再接管新引用最后将src置空实现见 handle_object 移动赋值。返回*this。需要特别注意移动赋值会先释放*this原有的引用因此不要用它做“交换”操作交换两个 handle 的引用请走std::move加临时对象或直接重建。4.3 预 C11 的转移机制在 C11 之前没有右值引用与移动语义规范设计了handle_move_t通道构造与赋值handle(handle_move_t m)与handle operator(handle_move_t m)允许从handle_move_t构造/赋值 handle。转换与转移operator handle_move_t()转换运算符以及自由友元函数friend handle_move_t move(handle h)将 handle 持有的引用转移到临时handle_move_t对象。规范特别提醒转换运算符不应被直接调用应使用move函数。完成后原 handle 不再引用任何值。两者配合即可在没有移动语义的环境下完成“引用转让”。在现代 C 项目中请直接使用移动语义无需关心这套旧通道当前实现也仅保留 C11 移动版本见 handle_object 中 delete的拷贝操作。4.4 状态查询与值访问operator bool() const检查 handle 是否持有引用。返回true当且仅当*this引用着缓存中的某个值。源码实现operator bool等价于return (my_lru_cache_ptr my_map_record_ptr);。这特别适合用来判断“移动后残留的源对象”是否为空句柄。value_type value()返回所引用value_type对象的引用。规范明确对一个不引用任何值的 handle 调用value()是未定义行为UB。源码通过__TBB_ASSERTvalue() 实现在调试构建中拦截“从已移动对象取值”等错误但发布构建下不保证安全调用方必须先用operator bool()确认。4.5 小结成员函数一览表成员作用关键语义handle()构造空句柄不引用任何值~handle()释放引用触发容器引用计数递减与 LRU 维护handle(handle)移动构造转移引用源对象变空C11 起operator(handle)移动赋值先释放旧引用再接管新引用C11 起handle(handle_move_t)等旧标准转移通道C11 之前使用已过时operator bool() const状态查询是否持有有效引用value_type value()值访问空句柄上调用为 UB5. 从测试用例看 handle 的正确用法仓库中的 test_concurrent_lru_cache.cpp 是验证 handle 语义最直接的证据其头文件 concurrent_lru_cache_common.h 定义了若干测试预置preset。几个关键场景1) 未用值的保留与上限连续写入不超过number_of_lru_history_items个键后值仍存活cache should store some unused objects一旦超过上限多出的未用值立即被驱逐cache should respect number of stored unused objects to number passed in constructor——这正是 handle 析构释放引用后、LRU 历史容量起作用的体现。2) LRU 驱逐顺序预热键 0 使其“最近使用”再写入新键触发驱逐验证被驱逐的是最久未用的键 1 而非键 0cache should evict items in lru order。3) 在用值不可驱逐先preset::handle_type h cache[0];保持键 0 在用再触发驱逐断言键 0 仍存活cache should not evict items in use——这是 handle 引用保持语义的直接验证。4) 容量为 0 的边界当number_of_lru_history_items 0时临时表达式cache[1].value()取出的对象在表达式结束后立即被销毁而一旦用具名 handleh持有引用对象便持续存活见 test 第 130-152 行。这提醒我们若希望值在取用后继续驻留必须把 handle 保存到具名变量中临时 handle 的析构会立刻释放引用。6. 实战建议与注意事项结合规范与源码使用handle时有几条值得牢记的规则优先auto承接返回值auto h cache[key];会把临时 handle 移动构造到具名对象中避免“取到即释放”的陷阱。测试中preset::handle_type h cache[0];正是标准写法。用operator bool()做安全检查对可能为空的句柄例如从容器或函数返回的 handle访问前先判断杜绝value()的 UB。理解移动即“释放”handle b std::move(a);后a变为空句柄不要再对a.value()解引用这由源码中移动操作把源指针置空保证。handle 生命周期决定缓存驻留想让值留在缓存中供后续命中就在作用域内保持一个 handle不再需要时让它析构即可容器会自动按 LRU 策略决定去留。性能考量value()返回的是value_type读写都直接作用于缓存内部对象不存在拷贝但缓存内部检索与引用计数更新均通过聚合器串行化高并发下对同一键的频繁operator[]会形成串行点。7. 参考文档与源码索引规范文档本文主体handle_object_cls.rst容器类规范concurrent_lru_cache_cls.rst参考手册API 速览concurrent_lru_cache_cls.rstreference 目录头文件实现concurrent_lru_cache.h测试用例test_concurrent_lru_cache.cpp 与 concurrent_lru_cache_common.h【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表