面试题:C++中shared
成为面试官 第一题 他们的Best Practices是什么
面试题:C++中shared_ptr是线程安全的吗?
来源 :一名毕业三年的女程序媛面试头条经验 推荐阅读:
- 深入理解C++11:C++11新特性解析与应用
- Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14
- Linux多线程服务端编程:使用muduo C++网络库
本文则具体分析一下为什么“因为 shared_ptr 有两个数据成员,读写操作不能原子化”使得多线程读写同一个 shared_ptr 对象需要加锁
废话不多说直接开始,面试官不喜欢 怎么回答
前段时间我面试过几个校招生,每当我问到是否了解shared_ptr的时候,对方总能巴拉巴拉说出一大堆东西。会讲到引用计数、 weak_ptr解决循环引用、 自定义删除器的用法等等等等。感觉这些知识都是很八股的东西。
我会立马打断去问一句:引用计数具体是怎么实现的?
怎么做到多个shared_ptr之间的计数能共享,同步更新的呢
1. shared_ptr 的数据结构是什么
shared_ptr 是引用计数型(reference counting)智能指针, 几乎所有的实现都采用在堆(heap)上放个计数值(count)的办法
具体的数据结构如图 1 所示,其中 deleter 和 allocator 是可选的。
为了简化并突出重点,后文只画出 use_count 的值
代码如下:
++-v3/include/bits/shared_ptr.h /include/bits/shared_ptr_base.h#L1795
代码语言:javascript代码运行次数:0运行复制 element_type* _M_ptr; // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.
template<typename _Yp>
_Assignable<_Yp>
operator=(const __shared_ptr<_Yp, _Lp>& __r) noexcept
{
_M_ptr = __r._M_ptr; //
_M_refcount = __r._M_refcount; // __shared_count::op= doesn't throw
return *this;
}
也就是说对于引用计数这一变量的存储,是在堆上的,多个shared_ptr的对象都指向同一个堆地址。
在多线程环境下,管理同一个数据的shared_ptr在进行计数的增加或减少的时候是线程安全的吗?
答案是肯定的,这一操作是原子操作。
★To satisfy thread safety requirements, the reference counters are typically incremented using an equivalent of std::atomic::fetch_add with std::memory_order_relaxed (decrementing requires stronger ordering to safely destroy the control block)
2. 原子操作的误解 :多个原子操作组合是线程安全的吗?
基本概念
原子操作指的是不可中断的操作序列,即在多线程环境下,该操作要么完全执行完毕,要么根本不执行,不会出现中间状态被其他线程看到的情况。这为解决并发编程中的数据竞争问题提供了基础。
应用场景
- 计数器:如统计在线用户数量、请求次数等。
- 标志位:用于线程间的简单信号传递,如停止标志。
- 锁的替代:在某些场景下,原子操作可以作为轻量级锁的替代方案,减少锁带来的性能开销
前提是:只有一个操作
代码语言:javascript代码运行次数:0运行复制shared_ptr<Foo> y = x;
y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生
如果没有 mutex 保护,那么在多线程里就有 race condition
例如: 无论ptr 和cnt 哪个先执行 都不是原子的
例如 如果更新foo,还没有更新count=+1
过程中,可能别其他线程 执行count-=1 ,结果被释放
core 发生来了。
3. 举例:多线程无保护读写 shared_ptr 可能出现的 race condition
参考:.html
考虑一个简单的场景,有 3 个 shared_ptr<Foo> 对象 x、g、n:
- shared_ptr<Foo> g(new Foo); // 线程之间共享的 shared_ptr
- shared_ptr<Foo> x; // 线程 A 的局部变量
- shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量
一开始,各安其事。
线程 A 执行 x = g; (即 read g),以下完成了步骤 1,还没来及执行步骤 2。这时切换到了 B 线程。
同时编程 B 执行 g = n; (即 write G),两个步骤一起完成了。
先是步骤 1:
再是步骤 2:
这是 Foo1 对象已经销毁,x.ptr 成了空悬指针!
最后回到线程 A,完成步骤 2:
多线程无保护地读写 g,造成了“x 是空悬指针”的后果。这正是多线程读写同一个 shared_ptr 必须加锁的原因。
当然,race condition 远不止这一种,其他线程交织(interweaving)有可能会造成其他错误。
思考,假如 shared_ptr 的 operator= 实现是先复制 ref_count(步骤 2)再复制 ptr(步骤 1),会有哪些 race condition?
Best Practices 候选人:说不清楚,解释不明白如何破局
〉 采取第一原理 在生活中,项目中 看中 什么问题, 不是弱引用问题。而是程序 不core
- 挑战1:从来没用过,按照课本上提到名词,网上看过的名词,个人理解字面意思回答,别人问了,我回答了, 这个可不是写作文,拼凑字数就可以了,至少保底分数。
【 反馈:不自觉陷入敷衍交付,结果自己不满意,别人更不满意,工作大忌,最终交付都上线,真实环境考验,真实客户问题 都让敷衍交付现原形,最后给领导埋雷,没有人喜欢]
- 挑战2:看过部分代码,文章,然后说很难,很难, 然后说其中各种难点,试图说自己不理解一些事情
【反馈:不自觉陷入嘴里都是各种问题交付,说不清楚,解释不明白,怪别人太刁难,消极态度,更没有喜欢,不管什么原因,什么借口 在无法交付,不要完美 要完成,最简单方案是什么,最复杂方案又是什么, 打工人,解决不了至少说清楚问题是什么,做哪些定位。最后什么输出都没有,很危险】
回到 面试现场。
- 无论怎么说 面试官都无动于衷,装作听不见, 反问 仅仅这样回答蒙骗过关, 他们反问,仅仅靠这些回答进大厂?
后候选人一脸茫然,该说的,能说全部说了, 反复回答都是这几句话 ,还怎么回答还面试官等不到期望点,
我已经回答了,怎么反复反问?
- 后悔记不住曾经 从哪里看过,听过。
- 继续追问 让你产生自我怀疑 三,五年,十年 工作能力是学习方式搞错了,平时加班,忙,做项目 根本没时间总结, 需要掌握掌握更多底层远离,看更多书?
★从现在这一刻开始,就是最好时刻, 你要明白一个事情,无论回去看多少书,掌握更多原理?花费更多时间,现在不明白 后面也不回明白。不要支支吾吾,你对明白 下定义
- 无论怎么看 无法还原 c++发明者对它的理解, c++ 依赖库 开发前后整个背景过程
- 无论怎么看,没有大量真实用户场景挑战。也无法真正 理解 实际用途。
- 即使有一天又真实场景,真是需求到来,也被其他事情干扰,很难 专注解决这个问题。
从现在这一刻开始,就是最好时刻,
你就负责这个事情,你做到比任何人更明白,更适合。
你让客户来告诉你这个怎么一个事情吗?你让测试人员告诉 这个事情怎么做 吗?你让经理项目设计一个操作步骤吗?你让其他同事来帮助解决事情事情吗?
需要你 联系客户了解业务背景, 需要你 联系测试 了解问题场景 需要你 联系PM 抵御自己无法 处理事情 需要你 其他同事 吸取一切经验
不是证明 我掌握知识,我高高在上,不掌握我丢人 不是彻底马上解决 一切完成全部事情,10年 20年事情才是 交付
唯有你做到比任何人更明白心态才能前行。
★在回到这个题目 至少课本上看过,动手写过demo,项目上用过,其他组件使用过 足够你可以继续看下去。从这些 过往经历中. 你就负责这个事情,你做到比任何人更明白,更适合 做知识搬运工,从发现问题时刻,想法解决。问题 和过程答案更重要。
这是一个很难突破的简单事情
一、这个技术出现的背景、初衷和要达到什么样的目标或是要解决什么样的问题
二、这个技术的优势和劣势分别是什么
三、这个技术适用的场景。任何技术都有其适用的场景,离开了这个场景
四、技术的组成部分和关键点。
五、技术的底层原理和关键实现
六、已有的实现和它之间的对比
更多阅读
[1] Boost.SmartPtr: The Smart Pointer .html#shared_ptr [2] gcc13STL源码解析 std::shared_ptr .md /2024/03/22/gcc13_shared_ptr/
学习方法
代码语言:javascript代码运行次数:0运行复制一、这个技术出现的背景、初衷和要达到什么样的目标或是要解决什么样的问题
二、这个技术的优势和劣势分别是什么
三、这个技术适用的场景。任何技术都有其适用的场景,离开了这个场景
四、技术的组成部分和关键点。
五、技术的底层原理和关键实现
六、已有的实现和它之间的对比
代码语言:javascript代码运行次数:0运行复制
#include <iostream>
#include <iostream>
#include <memory>
//#include <mutex>
#include <thread>
using namespace std;
struct Base
{
Base() { std::cout << "Base::Base()\n"; }
// Note: non-virtual destructor is OK here
~Base() { std::cout << "Base::~Base()\n"; }
};
void thread_read(std::shared_ptr<Base> sp1)
{
std::shared_ptr<Base> lp = sp1; // 线程 A 的局部变量
std::cout << "read thead use count: " << sp1.use_count() << std::endl;
}
void thread_write(std::shared_ptr<Base> sp1)
{
std::shared_ptr<Base> sp3 = std::make_shared<Base>(); //// 线程 B 的局部变量
sp1=sp3; //reset
std::cout << "write thead use count: " <<sp1.use_count() << std::endl;
}
//g++ -g -Wall -std=c++11 demo_share_pointer.cpp
//g++ -std=c++11 -pthread -o test demo_share_pointer.cpp
int main()
{
std::shared_ptr<Base> sp1 = std::make_shared<Base>(); //线程之间共享的 shared_ptr
std::cout << "use count: " << sp1.use_count() << std::endl;
shared_ptr<Base> sp2 = sp1;
std::cout << "use count: " << sp1.use_count() << std::endl;
shared_ptr<Base> sp3 = sp2;
std::cout << "use count: " << sp1.use_count() << std::endl;
/**
Base::Base()
use count: 1
use count: 2
use count: 3
Base::~Base()
**/
std::thread t1{thread_read, sp1}, t2{thread_write, sp1};
t1.join();
t2.join();
std::cout << "All threads completed,\n";
return 0;
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2024-11-17,如有侵权请联系 cloudcommunity@tencent 删除线程线程安全c++shared多线程