Skip to content

Commit d86143b

Browse files
committed
deploy: 8f898da
1 parent 8cd2dae commit d86143b

File tree

5 files changed

+161
-5
lines changed

5 files changed

+161
-5
lines changed

index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ <h2 id="_1">前言</h2>
292292
<blockquote>
293293
<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>
294294
</blockquote>
295-
<p>更新时间:2024年09月17日 22:05:37 (UTC+08:00)</p>
295+
<p>更新时间:2024年09月18日 12:29:27 (UTC+08:00)</p>
296296
<p><a href="https://parallel101.github.io/cppguidebook">在 GitHub Pages 浏览本书</a> | <a href="https://142857.red/book">在小彭老师自己维护的镜像上浏览本书</a></p>
297297
<h2 id="_2">格式约定</h2>
298298
<blockquote>

print_page/index.html

+78-2
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ <h2 id="index-_1">前言</h2>
421421
<blockquote>
422422
<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>
423423
</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>
425425
<p><a href="https://parallel101.github.io/cppguidebook">在 GitHub Pages 浏览本书</a> | <a href="https://142857.red/book">在小彭老师自己维护的镜像上浏览本书</a></p>
426426
<h2 id="index-_2">格式约定</h2>
427427
<blockquote>
@@ -17727,7 +17727,83 @@ <h2 id="threading-_2">为什么数据竞争</h2>
1772717727
} else {
1772817728
return 0;
1772917729
}
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&ndash;,知道吧。如果已经为0,那就没有任何其他人持有该指针,我是独占的,那随便delete。</p>
17747+
<p>这样吧,我也听不懂你在讲什么,你来写一份你认为会产生问题的代码,让我分析。</p>
17748+
<p><strong>同学</strong></p>
17749+
<p>我设想了如下的代码:</p>
17750+
<pre><code class="language-c++">shared_ptr&lt;int&gt; a = make_shared&lt;int&gt;();
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&lt;T&gt;</code> 的要求:
17790+
析构+拷贝 同时发生,是未定义行为。
17791+
拷贝+拷贝 同时发生,是安全的。
17792+
C++ 标准对 <code>atomic&lt;shared_ptr&lt;T&gt;&gt;</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 &amp;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&lt;T&gt;</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>
1773117807
<div class="toc">
1773217808
<ul>
1773317809
<li><a href="#undef-_1">未定义行为完整列表</a><ul>

search/search_index.json

+1-1
Large diffs are not rendered by default.

sitemap.xml.gz

0 Bytes
Binary file not shown.

threading/index.html

+81-1
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@
251251
<li class="nav-item" data-bs-level="2"><a href="#_2" class="nav-link">为什么数据竞争</a>
252252
<ul class="nav flex-column">
253253
</ul>
254+
</li>
255+
<li class="nav-item" data-bs-level="2"><a href="#_3" class="nav-link">小彭老师对话一则</a>
256+
<ul class="nav flex-column">
257+
</ul>
254258
</li>
255259
</ul>
256260
</li>
@@ -275,7 +279,83 @@ <h2 id="_2">为什么数据竞争</h2>
275279
} else {
276280
return 0;
277281
}
278-
</code></pre></div>
282+
</code></pre>
283+
<h2 id="_3">小彭老师对话一则</h2>
284+
<p>关于 SharedPtr 的原子安全实现。</p>
285+
<ul>
286+
<li>对话地址:https://github.com/parallel101/stl1weekend/issues/4</li>
287+
<li>代码地址:https://github.com/parallel101/stl1weekend/blob/main/SharedPtr.hpp</li>
288+
</ul>
289+
<p>sharedptr引用计数减被封装成如下的函数</p>
290+
<pre><code class="language-c++">void _M_decref() noexcept {
291+
if (_M_refcnt.fetch_sub(1, std::memory_order_relaxed) == 1) {
292+
delete this;
293+
}
294+
}
295+
</code></pre>
296+
<p>该if块先判断原始引用计数是否等于1,如果为真则进行delete。然而判断和delete是两个操作,并不是一个原子操作。是否存在这样一种情况:判断条件成立,但在delete前有其它线程给引用计数+1?此时进行delete就出错了吧</p>
297+
<p><strong>小彭老师</strong></p>
298+
<p>没有问题的,因为fetch_sub返回1,实际上说明引用计数已经是0了,fetch_sub返回的是“旧值”,相当于后置i&ndash;,知道吧。如果已经为0,那就没有任何其他人持有该指针,我是独占的,那随便delete。</p>
299+
<p>这样吧,我也听不懂你在讲什么,你来写一份你认为会产生问题的代码,让我分析。</p>
300+
<p><strong>同学</strong></p>
301+
<p>我设想了如下的代码:</p>
302+
<pre><code class="language-c++">shared_ptr&lt;int&gt; a = make_shared&lt;int&gt;();
303+
void fun1() {
304+
a = nullptr; // 析构a
305+
}
306+
void func2() {
307+
auto b = a; // 拷贝a到b
308+
}
309+
310+
int main(){
311+
auto t1=std::thread(func1);
312+
auto t2=std::thread(func2);
313+
t1.join();
314+
t2.join();
315+
return 0;
316+
}
317+
</code></pre>
318+
<p>在这段代码中,线程1析构a,而线程2拷贝a到b。由于多线程的缘故,我认为会出现以下的情况,线程1执行判断时</p>
319+
<pre><code class="language-c++">if (_M_refcnt.fetch_sub(1, std::memory_order_relaxed) == 1)
320+
</code></pre>
321+
<p>由于func2刚进入尚未执行拷贝,此时引用计数等于1还不是2,所以该判断为true。于是,线程1准备执行<code>delete this</code>将_SpCounter释放,就在这时线程2将func2彻底执行了,此时引用计数又从0变为了1,然而线程1并不知道这个变化,它仍然按照原本的轨迹去执行delete this。所以我认为这就出错了,而出错的原因是</p>
322+
<pre><code class="language-c++">void _M_decref() noexcept {
323+
if (_M_refcnt.fetch_sub(1, std::memory_order_relaxed) == 1) {
324+
delete this;
325+
}
326+
}
327+
</code></pre>
328+
<p>此处的判断和delete是两个操作,而非一个原子操作。</p>
329+
<p>很抱歉之前未能及时回复</p>
330+
<p><strong>小彭老师</strong></p>
331+
<p>是的,这段代码有未定义行为!
332+
然而 C++ 标准只要求了:
333+
- 析构+拷贝 同时发生,是未定义行为。
334+
- 拷贝+拷贝 同时发生,是安全的。
335+
我的原子变量已经保证了 拷贝+拷贝 的安全,符合 C++ 标准的要求。
336+
析构+拷贝 的情况,C++ 标准就并不要求安全,所以我的 shared_ptr 也没有责任去保证这种情况下的安全。</p>
337+
<p>比如标准不要求 vector 的 clear 和 push_back 同时调用是线程安全的,那么我就不需要把 vector 实现为安全的。
338+
如果标准规定了哪两个函数同时调用是安全的,我再去做。
339+
比如标准就规定了 size 和 data 两个函数同时调用是线程安全的,我只需要符合这个就可以。
340+
标准都没有规定必须安全的情况,我的容器如果产生未定义行为,我不负责任。</p>
341+
<p>例如,C++ 标准对 <code>shared_ptr&lt;T&gt;</code> 的要求:
342+
析构+拷贝 同时发生,是未定义行为。
343+
拷贝+拷贝 同时发生,是安全的。
344+
C++ 标准对 <code>atomic&lt;shared_ptr&lt;T&gt;&gt;</code> 的要求:
345+
析构+拷贝 同时发生,是安全的。
346+
拷贝+拷贝 同时发生,是安全的。
347+
所以,只有当我是在实现atomic_shared_ptr时,才需要考虑你说的这种情况,而我现在实现的是shared_ptr,不需要考虑 析构+拷贝 的安全。</p>
348+
<p>为什么拷贝+拷贝是安全的?我怎么没看到cppreference说?这很复杂,是另一句话里透露的通用规则,适用于所有容器,包括shared_ptr、unique_ptr、vector等全部的容器:
349+
两个const成员函数,同时发生,没有未定义行为。
350+
一个非const成员函数+一个const成员函数,同时发生,是未定义行为。
351+
这句话自动适用于所有的容器了,所以你看到shared_ptr里没有说,但是我知道他是在另一个关于线程安全的页面上。</p>
352+
<p>那么很明显,拷贝构造函数<code>shared_ptr(shared_ptr const &amp;that)</code>是const的(对于被拷贝的that),而析构函数都是非const的,所以如果没有特别说明,一个容器同时调用拷贝+析构是未定义行为。而atomic_shared_ptr就属于特别说明了,所以他特别地同时访问const和非const函数是安全的。</p>
353+
<p>完整的多线程安全规则表:
354+
读+读=安全
355+
读+写=未定义行为
356+
写+写=未定义行为</p>
357+
<p>所以实际上sharedptr所谓的“线程安全”,只不过是拷贝+拷贝这一情况的安全和拷贝+析构不同<code>shared_ptr</code>实例,同一个<code>shared_ptr</code>的并发非const访问是没保证的,<code>shared_ptr&lt;T&gt;</code>指向的那个<code>T</code>也是不保证的(由<code>T</code>的实现者“你”来保证)。
358+
<code>shared_ptr</code>不是有三层吗?通俗的说就是他只需要保证中间这层控制块的线程安全性,不保证<code>shared_ptr</code>对象和<code>T</code>对象的安全性。</p></div>
279359
</div>
280360
</div>
281361

0 commit comments

Comments
 (0)