@@ -421,7 +421,7 @@ <h2 id="index-_1">前言</h2>
421
421
<blockquote>
422
422
<p><img src="../img/bulb.png" height="30px" width="auto" style="margin: 0; border: none"/> 本书还在持续更新中……要追番的话,可以在 <a href="https://github.com/parallel101/cppguidebook">GitHub</a> 点一下右上角的 “Watch” 按钮,每当小彭老师提交新 commit,GitHub 会向你发送一封电子邮件,提醒你小彭老师更新了。</p>
423
423
</blockquote>
424
- <p>更新时间:2024年09月17日 22:05:37 (UTC+08:00)</p>
424
+ <p>更新时间:2024年09月18日 12:29:27 (UTC+08:00)</p>
425
425
<p><a href="https://parallel101.github.io/cppguidebook">在 GitHub Pages 浏览本书</a> | <a href="https://142857.red/book">在小彭老师自己维护的镜像上浏览本书</a></p>
426
426
<h2 id="index-_2">格式约定</h2>
427
427
<blockquote>
@@ -17727,7 +17727,83 @@ <h2 id="threading-_2">为什么数据竞争</h2>
17727
17727
} else {
17728
17728
return 0;
17729
17729
}
17730
- </code></pre></section><section class="print-page" id="test_and_safe"><h1 id="test_and_safe-_1">测试与安全话题(未完工)</h1></section><section class="print-page" id="undef"><h1 id="undef-_1">未定义行为完整列表</h1>
17730
+ </code></pre>
17731
+ <h2 id="threading-_3">小彭老师对话一则</h2>
17732
+ <p>关于 SharedPtr 的原子安全实现。</p>
17733
+ <ul>
17734
+ <li>对话地址:https://github.com/parallel101/stl1weekend/issues/4</li>
17735
+ <li>代码地址:https://github.com/parallel101/stl1weekend/blob/main/SharedPtr.hpp</li>
17736
+ </ul>
17737
+ <p>sharedptr引用计数减被封装成如下的函数</p>
17738
+ <pre><code class="language-c++">void _M_decref() noexcept {
17739
+ if (_M_refcnt.fetch_sub(1, std::memory_order_relaxed) == 1) {
17740
+ delete this;
17741
+ }
17742
+ }
17743
+ </code></pre>
17744
+ <p>该if块先判断原始引用计数是否等于1,如果为真则进行delete。然而判断和delete是两个操作,并不是一个原子操作。是否存在这样一种情况:判断条件成立,但在delete前有其它线程给引用计数+1?此时进行delete就出错了吧</p>
17745
+ <p><strong>小彭老师</strong></p>
17746
+ <p>没有问题的,因为fetch_sub返回1,实际上说明引用计数已经是0了,fetch_sub返回的是“旧值”,相当于后置i–,知道吧。如果已经为0,那就没有任何其他人持有该指针,我是独占的,那随便delete。</p>
17747
+ <p>这样吧,我也听不懂你在讲什么,你来写一份你认为会产生问题的代码,让我分析。</p>
17748
+ <p><strong>同学</strong></p>
17749
+ <p>我设想了如下的代码:</p>
17750
+ <pre><code class="language-c++">shared_ptr<int> a = make_shared<int>();
17751
+ void fun1() {
17752
+ a = nullptr; // 析构a
17753
+ }
17754
+ void func2() {
17755
+ auto b = a; // 拷贝a到b
17756
+ }
17757
+
17758
+ int main(){
17759
+ auto t1=std::thread(func1);
17760
+ auto t2=std::thread(func2);
17761
+ t1.join();
17762
+ t2.join();
17763
+ return 0;
17764
+ }
17765
+ </code></pre>
17766
+ <p>在这段代码中,线程1析构a,而线程2拷贝a到b。由于多线程的缘故,我认为会出现以下的情况,线程1执行判断时</p>
17767
+ <pre><code class="language-c++">if (_M_refcnt.fetch_sub(1, std::memory_order_relaxed) == 1)
17768
+ </code></pre>
17769
+ <p>由于func2刚进入尚未执行拷贝,此时引用计数等于1还不是2,所以该判断为true。于是,线程1准备执行<code>delete this</code>将_SpCounter释放,就在这时线程2将func2彻底执行了,此时引用计数又从0变为了1,然而线程1并不知道这个变化,它仍然按照原本的轨迹去执行delete this。所以我认为这就出错了,而出错的原因是</p>
17770
+ <pre><code class="language-c++">void _M_decref() noexcept {
17771
+ if (_M_refcnt.fetch_sub(1, std::memory_order_relaxed) == 1) {
17772
+ delete this;
17773
+ }
17774
+ }
17775
+ </code></pre>
17776
+ <p>此处的判断和delete是两个操作,而非一个原子操作。</p>
17777
+ <p>很抱歉之前未能及时回复</p>
17778
+ <p><strong>小彭老师</strong></p>
17779
+ <p>是的,这段代码有未定义行为!
17780
+ 然而 C++ 标准只要求了:
17781
+ - 析构+拷贝 同时发生,是未定义行为。
17782
+ - 拷贝+拷贝 同时发生,是安全的。
17783
+ 我的原子变量已经保证了 拷贝+拷贝 的安全,符合 C++ 标准的要求。
17784
+ 析构+拷贝 的情况,C++ 标准就并不要求安全,所以我的 shared_ptr 也没有责任去保证这种情况下的安全。</p>
17785
+ <p>比如标准不要求 vector 的 clear 和 push_back 同时调用是线程安全的,那么我就不需要把 vector 实现为安全的。
17786
+ 如果标准规定了哪两个函数同时调用是安全的,我再去做。
17787
+ 比如标准就规定了 size 和 data 两个函数同时调用是线程安全的,我只需要符合这个就可以。
17788
+ 标准都没有规定必须安全的情况,我的容器如果产生未定义行为,我不负责任。</p>
17789
+ <p>例如,C++ 标准对 <code>shared_ptr<T></code> 的要求:
17790
+ 析构+拷贝 同时发生,是未定义行为。
17791
+ 拷贝+拷贝 同时发生,是安全的。
17792
+ C++ 标准对 <code>atomic<shared_ptr<T>></code> 的要求:
17793
+ 析构+拷贝 同时发生,是安全的。
17794
+ 拷贝+拷贝 同时发生,是安全的。
17795
+ 所以,只有当我是在实现atomic_shared_ptr时,才需要考虑你说的这种情况,而我现在实现的是shared_ptr,不需要考虑 析构+拷贝 的安全。</p>
17796
+ <p>为什么拷贝+拷贝是安全的?我怎么没看到cppreference说?这很复杂,是另一句话里透露的通用规则,适用于所有容器,包括shared_ptr、unique_ptr、vector等全部的容器:
17797
+ 两个const成员函数,同时发生,没有未定义行为。
17798
+ 一个非const成员函数+一个const成员函数,同时发生,是未定义行为。
17799
+ 这句话自动适用于所有的容器了,所以你看到shared_ptr里没有说,但是我知道他是在另一个关于线程安全的页面上。</p>
17800
+ <p>那么很明显,拷贝构造函数<code>shared_ptr(shared_ptr const &that)</code>是const的(对于被拷贝的that),而析构函数都是非const的,所以如果没有特别说明,一个容器同时调用拷贝+析构是未定义行为。而atomic_shared_ptr就属于特别说明了,所以他特别地同时访问const和非const函数是安全的。</p>
17801
+ <p>完整的多线程安全规则表:
17802
+ 读+读=安全
17803
+ 读+写=未定义行为
17804
+ 写+写=未定义行为</p>
17805
+ <p>所以实际上sharedptr所谓的“线程安全”,只不过是拷贝+拷贝这一情况的安全和拷贝+析构不同<code>shared_ptr</code>实例,同一个<code>shared_ptr</code>的并发非const访问是没保证的,<code>shared_ptr<T></code>指向的那个<code>T</code>也是不保证的(由<code>T</code>的实现者“你”来保证)。
17806
+ <code>shared_ptr</code>不是有三层吗?通俗的说就是他只需要保证中间这层控制块的线程安全性,不保证<code>shared_ptr</code>对象和<code>T</code>对象的安全性。</p></section><section class="print-page" id="test_and_safe"><h1 id="test_and_safe-_1">测试与安全话题(未完工)</h1></section><section class="print-page" id="undef"><h1 id="undef-_1">未定义行为完整列表</h1>
17731
17807
<div class="toc">
17732
17808
<ul>
17733
17809
<li><a href="#undef-_1">未定义行为完整列表</a><ul>
0 commit comments