成为面试官 第一题 他们的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++网络库

image.png

为 防止网盘失效,请添加微信:watchpoints 直接 发送电子书 image.png

为了节省读者时间 一句话描述 shared_ptr 的引用计数本身是安全且无锁的, 但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化

本文则具体分析一下为什么“因为 shared_ptr 有两个数据成员,读写操作不能原子化”使得多线程读写同一个 shared_ptr 对象需要加锁

废话不多说直接开始,面试官不喜欢 怎么回答

前段时间我面试过几个校招生,每当我问到是否了解shared_ptr的时候,对方总能巴拉巴拉说出一大堆东西。 会讲到引用计数、 weak_ptr解决循环引用、 自定义删除器的用法等等等等。 感觉这些知识都是很八股的东西。

我会立马打断去问一句:引用计数具体是怎么实现的?

怎么做到多个shared_ptr之间的计数能共享,同步更新的呢

1. shared_ptr 的数据结构是什么

shared_ptr 是引用计数型(reference counting)智能指针, 几乎所有的实现都采用在堆(heap)上放个计数值(count)的办法

具体的数据结构如图 1 所示,其中 deleter 和 allocator 是可选的。

为了简化并突出重点,后文只画出 use_count 的值

代码如下:

https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/include/bits/shared_ptr.h https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/shared_ptr_base.h#L1795

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 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. 原子操作的误解 :多个原子操作组合是线程安全的吗?

基本概念

原子操作指的是不可中断的操作序列,即在多线程环境下,该操作要么完全执行完毕,要么根本不执行,不会出现中间状态被其他线程看到的情况。 这为解决并发编程中的数据竞争问题提供了基础。

应用场景

  • 计数器:如统计在线用户数量、请求次数等。
  • 标志位:用于线程间的简单信号传递,如停止标志。
  • 锁的替代:在某些场景下,原子操作可以作为轻量级锁的替代方案,减少锁带来的性能开销

前提是:只有一个操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
shared_ptr<Foo> y = x;

y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生

如果没有 mutex 保护,那么在多线程里就有 race condition

例如: 无论ptr 和cnt 哪个先执行 都不是原子的

例如 如果更新foo,还没有更新count=+1
   
     过程中,可能别其他线程 执行count-=1 ,结果被释放

core 发生来了。

3. 举例: 多线程无保护读写 shared_ptr 可能出现的 race condition

参考:https://www.cnblogs.com/Solstice/archive/2013/01/28/2879366.html

Best Practices 候选人:说不清楚,解释不明白如何破局

〉 采取第一原理 在生活中,项目中 看中 什么问题, 不是弱引用问题。而是程序 不core

  1. 挑战1:从来没用过,按照课本上提到名词,网上看过的名词,个人理解字面意思回答,别人问了,我回答了, 这个可不是写作文,拼凑字数就可以了,至少保底分数。

【 反馈:不自觉陷入敷衍交付,结果自己不满意,别人更不满意,工作大忌,最终交付都上线,真实环境考验,真实客户问题 都让敷衍交付现原形,最后给领导埋雷,没有人喜欢]

  1. 挑战2:看过部分代码,文章,然后说很难,很难, 然后说其中各种难点,试图说自己不理解一些事情

【反馈:不自觉陷入嘴里都是各种问题交付,说不清楚,解释不明白,怪别人太刁难,消极态度,更没有喜欢,不管什么原因,什么借口 在无法交付,不要完美 要完成,最简单方案是什么,最复杂方案又是什么, 打工人,解决不了至少说清楚问题是什么,做哪些定位。 最后什么输出都没有,很危险】

回到 面试现场。

  • 无论怎么说 面试官都无动于衷,装作听不见, 反问 仅仅这样回答蒙骗过关, 他们反问,仅仅靠这些回答进大厂?

后候选人一脸茫然,该说的,能说全部说了, 反复回答都是这几句话 ,还怎么回答还面试官等不到期望点,

我已经回答了,怎么反复反问?

  • 后悔记不住曾经 从哪里看过,听过。

  • 继续追问 让你产生自我怀疑 三,五年,十年 工作能力是学习方式搞错了,平时加班,忙,做项目 根本没时间总结, 需要掌握掌握更多底层远离,看更多书?

从现在这一刻开始,就是最好时刻, 你要明白一个事情,无论回去看多少书,掌握更多原理?花费更多时间,现在不明白 后面也不回明白。 不要支支吾吾,你对明白 下定义

  1. 无论怎么看 无法还原 c++发明者对它的理解, c++ 依赖库 开发前后整个背景过程
  2. 无论怎么看,没有大量真实用户场景挑战。 也无法真正 理解 实际用途。
  3. 即使有一天又真实场景,真是需求到来,也被其他事情干扰,很难 专注解决这个问题。

从现在这一刻开始,就是最好时刻,

你就负责这个事情,你做到比任何人更明白,更适合。

你让客户来告诉你这个怎么一个事情吗? 你让测试人员告诉 这个事情怎么做 吗? 你让经理项目设计一个操作步骤吗? 你让其他同事来帮助解决事情事情吗?

需要你 联系客户了解业务背景, 需要你 联系测试 了解问题场景 需要你 联系PM 抵御自己无法 处理事情 需要你 其他同事 吸取一切经验

不是证明 我掌握知识,我高高在上,不掌握我丢人 不是彻底马上解决 一切完成全部事情,10年 20年事情才是 交付

唯有你做到比任何人更明白心态才能前行。

在回到这个题目 至少课本上看过,动手写过demo,项目上用过,其他组件使用过 足够你可以继续看下去。 从这些 过往经历中. 你就负责这个事情,你做到比任何人更明白,更适合 做知识搬运工,从发现问题时刻,想法解决。问题 和过程答案更重要。

这是一个很难突破的简单事情

一、这个技术出现的背景、初衷和要达到什么样的目标或是要解决什么样的问题

二、这个技术的优势和劣势分别是什么

三、这个技术适用的场景。任何技术都有其适用的场景,离开了这个场景

四、技术的组成部分和关键点。

五、技术的底层原理和关键实现

六、已有的实现和它之间的对比

更多阅读

[1] Boost.SmartPtr: The Smart Pointer

[2] gcc13STL源码解析 std::shared_ptr

学习方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
一、这个技术出现的背景、初衷和要达到什么样的目标或是要解决什么样的问题

 二、这个技术的优势和劣势分别是什么 


 三、这个技术适用的场景。任何技术都有其适用的场景,离开了这个场景

 四、技术的组成部分和关键点。

 五、技术的底层原理和关键实现

 六、已有的实现和它之间的对比
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

// Reference transformations.

  

/// remove_reference

template<typename _Tp>

struct remove_reference

{ typedef _Tp type; };

  

template<typename _Tp>

struct remove_reference<_Tp&>

{ typedef _Tp type; };

  

template<typename _Tp>

struct remove_reference<_Tp&&>

{ typedef _Tp type; };

  

template<typename _Tp, bool = __is_referenceable<_Tp>::value>

struct __add_lvalue_reference_helper

{ typedef _Tp type; };

  

template<typename _Tp>

struct __add_lvalue_reference_helper<_Tp, true>

{ typedef _Tp& type; };

  

/// add_lvalue_reference

template<typename _Tp>

struct add_lvalue_reference

: public __add_lvalue_reference_helper<_Tp>

{ };

  

template<typename _Tp, bool = __is_referenceable<_Tp>::value>

struct __add_rvalue_reference_helper

{ typedef _Tp type; };

  

template<typename _Tp>

struct __add_rvalue_reference_helper<_Tp, true>

{ typedef _Tp&& type; };

  

/// add_rvalue_reference

template<typename _Tp>

struct add_rvalue_reference

: public __add_rvalue_reference_helper<_Tp>

{ };