EffectiveModernCppChinese/7.TheConcurrencyAPI/item38.html
2024-01-02 12:53:33 +00:00

254 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE HTML>
<html lang="zh" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Item 38:关注不同线程句柄析构行为 - Effective Modern C++</title>
<!-- Custom HTML head -->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
<!-- MathJax -->
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
</head>
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded "><a href="../Introduction.html">简介</a></li><li class="chapter-item expanded "><div>第一章 类型推导</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../1.DeducingTypes/item1.html">Item 1:理解模板类型推导</a></li><li class="chapter-item expanded "><a href="../1.DeducingTypes/item2.html">Item 2:理解auto类型推导</a></li><li class="chapter-item expanded "><a href="../1.DeducingTypes/item3.html">Item 3:理解decltype</a></li><li class="chapter-item expanded "><a href="../1.DeducingTypes/item4.html">Item 4:学会查看类型推导结果</a></li></ol></li><li class="chapter-item expanded "><div>第二章 auto</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../2.Auto/item5.html">Item 5:优先考虑auto而非显式类型声明</a></li><li class="chapter-item expanded "><a href="../2.Auto/item6.html">Item 6:auto推导若非己愿使用显式类型初始化惯用法</a></li></ol></li><li class="chapter-item expanded "><div>第三章 移步现代C++</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item7.html">Item 7:区别使用()和{}创建对象</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item8.html">Item 8:优先考虑nullptr而非0和NULL</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item9.html">Item 9:优先考虑别名声明而非typedefs</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item10.html">Item 10:优先考虑限域枚举而非未限域枚举</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item11.html">Item 11:优先考虑使用deleted函数而非使用未定义的私有声明</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item12.html">Item 12:使用override声明重载函数</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item13.html">Item 13:优先考虑const_iterator而非iterator</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item14.html">Item 14:如果函数不抛出异常请使用noexcept</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item15.html">Item 15:尽可能的使用constexpr</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item16.html">Item 16:让const成员函数线程安全</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item17.html">Item 17:理解特殊成员函数函数的生成</a></li></ol></li><li class="chapter-item expanded "><div>第四章 智能指针</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../4.SmartPointers/item18.html">Item 18:对于独占资源使用std::unique_ptr</a></li><li class="chapter-item expanded "><a href="../4.SmartPointers/item19.html">Item 19:对于共享资源使用std::shared_ptr</a></li><li class="chapter-item expanded "><a href="../4.SmartPointers/item20.html">Item 20:当std::shared_ptr可能悬空时使用std::weak_ptr</a></li><li class="chapter-item expanded "><a href="../4.SmartPointers/item21.html">Item 21:优先考虑使用std::make_unique和std::make_shared而非new</a></li><li class="chapter-item expanded "><a href="../4.SmartPointers/item22.html">Item 22:当使用Pimpl惯用法请在实现文件中定义特殊成员函数</a></li></ol></li><li class="chapter-item expanded "><div>第五章 右值引用,移动语义,完美转发</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item23.html">Item 23:理解std::move和std::forward</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item24.html">Item 24:区别通用引用和右值引用</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item25.html">Item 25:对于右值引用使用std::move对于通用引用使用std::forward</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item26.html">Item 26:避免重载通用引用</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item27.html">Item 27:熟悉重载通用引用的替代品</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item28.html">Item 28:理解引用折叠</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item29.html">Item 29:认识移动操作的缺点</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item30.html">Item 30:熟悉完美转发失败的情况</a></li></ol></li><li class="chapter-item expanded "><div>第六章 Lambda表达式</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../6.LambdaExpressions/item31.html">Item 31:避免使用默认捕获模式</a></li><li class="chapter-item expanded "><a href="../6.LambdaExpressions/item32.html">Item 32:使用初始化捕获来移动对象到闭包中</a></li><li class="chapter-item expanded "><a href="../6.LambdaExpressions/item33.html">Item 33:对于std::forward的auto&&形参使用decltype</a></li><li class="chapter-item expanded "><a href="../6.LambdaExpressions/item34.html">Item 34:优先考虑lambda表达式而非std::bind</a></li></ol></li><li class="chapter-item expanded "><div>第七章 并发API</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/Item35.html">Item 35:优先考虑基于任务的编程而非基于线程的编程</a></li><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/item36.html">Item 36:如果有异步的必要请指定std::launch::async</a></li><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/item37.html">Item 37:从各个方面使得std::threads unjoinable</a></li><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/item38.html" class="active">Item 38:关注不同线程句柄析构行为</a></li><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/item39.html">Item 39:考虑对于单次事件通信使用void</a></li><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/item40.html">Item 40:对于并发使用std::atomicvolatile用于特殊内存区</a></li></ol></li><li class="chapter-item expanded "><div>第八章 微调</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../8.Tweaks/item41.html">Item 41:对于那些可移动总是被拷贝的形参使用传值方式</a></li><li class="chapter-item expanded "><a href="../8.Tweaks/item42.html">Item 42:考虑就地创建而非插入</a></li></ol></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Effective Modern C++</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/CnTransGroup/EffectiveModernCppChinese" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h2 id="条款三十八关注不同线程句柄的析构行为"><a class="header" href="#条款三十八关注不同线程句柄的析构行为">条款三十八:关注不同线程句柄的析构行为</a></h2>
<p><strong>Item 38Be aware of varying thread handle destructor behavior</strong></p>
<p><a href="../7.TheConcurrencyAPI/item37.html">Item37</a>中说明了可结合的<code>std::thread</code>对应于执行的系统线程。未延迟non-deferred任务的<em>future</em>(参见<a href="../7.TheConcurrencyAPI/item36.html">Item36</a>)与系统线程有相似的关系。因此,可以将<code>std::thread</code>对象和<em>future</em>对象都视作系统线程的<strong>句柄</strong><em>handles</em>)。</p>
<p>从这个角度来说,有趣的是<code>std::thread</code><em>future</em>在析构时有相当不同的行为。在<a href="../7.TheConcurrencyAPI/item37.html">Item37</a>中说明,可结合的<code>std::thread</code>析构会终止你的程序,因为两个其他的替代选择——隐式<code>join</code>或者隐式<code>detach</code>都是更加糟糕的。但是,<em>future</em>的析构表现有时就像执行了隐式<code>join</code>,有时又像是隐式执行了<code>detach</code>,有时又没有执行这两个选择。它永远不会造成程序终止。这个线程句柄多种表现值得研究一下。</p>
<p>我们可以观察到实际上<em>future</em>是通信信道的一端,被调用者通过该信道将结果发送给调用者。(<a href="../7.TheConcurrencyAPI/item39.html">Item39</a>说,与<em>future</em>有关的这种通信信道也可以被用于其他目的。但是对于本条款,我们只考虑它们作为这样一个机制的用法,即被调用者传送结果给调用者。)被调用者(通常是异步执行)将计算结果写入通信信道中(通常通过<code>std::promise</code>对象),调用者使用<em>future</em>读取结果。你可以想象成下面的图示,虚线表示信息的流动方向:</p>
<p><img src="media/item38_fig1.png" alt="item38_fig1" /></p>
<p>但是被调用者的结果存储在哪里?被调用者会在调用者<code>get</code>相关的<em>future</em>之前执行完成,所以结果不能存储在被调用者的<code>std::promise</code>。这个对象是局部的,当被调用者执行结束后,会被销毁。</p>
<p>结果同样不能存储在调用者的<em>future</em>,因为(当然还有其他原因)<code>std::future</code>可能会被用来创建<code>std::shared_future</code>(这会将被调用者的结果所有权从<code>std::future</code>转移给<code>std::shared_future</code>),而<code>std::shared_future</code><code>std::future</code>被销毁之后可能被复制很多次。鉴于不是所有的结果都可以被拷贝(即只可移动类型),并且结果的生命周期至少与最后一个引用它的<em>future</em>一样长,这些潜在的<em>future</em>中哪个才是被调用者用来存储结果的?</p>
<p>因为与被调用者关联的对象和与调用者关联的对象都不适合存储这个结果,所以必须存储在两者之外的位置。此位置称为<strong>共享状态</strong><em>shared state</em>)。共享状态通常是基于堆的对象,但是标准并未指定其类型、接口和实现。标准库的作者可以通过任何他们喜欢的方式来实现共享状态。</p>
<p>我们可以想象调用者,被调用者,共享状态之间关系如下图,虚线还是表示信息流方向:</p>
<p><img src="media/item38_fig2.png" alt="item38_fig2" /></p>
<p>共享状态的存在非常重要,因为<em>future</em>的析构函数——这个条款的话题——取决于与<em>future</em>关联的共享状态。特别地,</p>
<ul>
<li><strong>引用了共享状态——使用<code>std::async</code>启动的未延迟任务建立的那个——的最后一个<em>future</em>的析构函数会阻塞住</strong>,直到任务完成。本质上,这种<em>future</em>的析构函数对执行异步任务的线程执行了隐式的<code>join</code></li>
<li><strong>其他所有<em>future</em>的析构函数简单地销毁<em>future</em>对象</strong>。对于异步执行的任务,就像对底层的线程执行<code>detach</code>。对于延迟任务来说如果这是最后一个<em>future</em>,意味着这个延迟任务永远不会执行了。</li>
</ul>
<p>这些规则听起来好复杂。我们真正要处理的是一个简单的“正常”行为以及一个单独的例外。正常行为是<em>future</em>析构函数销毁<em>future</em>。就是这样。那意味着不<code>join</code>也不<code>detach</code>,也不运行什么,只销毁<em>future</em>的数据成员(当然,还做了另一件事,就是递减了共享状态中的引用计数,这个共享状态是由引用它的<em>future</em>和被调用者的<code>std::promise</code>共同控制的。这个引用计数让库知道共享状态什么时候可以被销毁。对于引用计数的一般信息参见<a href="../4.SmartPointers/item19.html">Item19</a>。)</p>
<p>正常行为的例外情况仅在某个<code>future</code>同时满足下列所有情况下才会出现:</p>
<ul>
<li><strong>它关联到由于调用<code>std::async</code>而创建出的共享状态</strong></li>
<li><strong>任务的启动策略是<code>std::launch::async</code></strong>(参见<a href="../7.TheConcurrencyAPI/item36.html">Item36</a>),原因是运行时系统选择了该策略,或者在对<code>std::async</code>的调用中指定了该策略。</li>
<li><strong>这个<em>future</em>是关联共享状态的最后一个<em>future</em></strong>。对于<code>std::future</code>,情况总是如此,对于<code>std::shared_future</code>,如果还有其他的<code>std::shared_future</code>,与要被销毁的<em>future</em>引用相同的共享状态,则要被销毁的<em>future</em>遵循正常行为(即简单地销毁它的数据成员)。</li>
</ul>
<p>只有当上面的三个条件都满足时,<em>future</em>的析构函数才会表现“异常”行为,就是在异步任务执行完之前阻塞住。实际上,这相当于对由于运行<code>std::async</code>创建出任务的线程隐式<code>join</code></p>
<p>通常会听到将这种异常的析构函数行为称为“<code>std::async</code>来的<em>futures</em>阻塞了它们的析构函数”。作为近似描述没有问题,但是有时你不只需要一个近似描述。现在你已经知道了其中真相。</p>
<p>你可能想要了解更加深入。比如“为什么由<code>std::async</code>启动的未延迟任务的共享状态,会有这么个特殊规则”,这很合理。据我所知,标准委员会希望避免隐式<code>detach</code>(参见<a href="../7.TheConcurrencyAPI/item37.html">Item37</a>)的有关问题,但是不想采取强制程序终止这种激进的方案(就像对可结合的<code>sth::thread</code>做的那样(译者注:指析构时<code>std::thread</code>若可结合则调用<code>std::terminal</code>终止程序),同样参见<a href="../7.TheConcurrencyAPI/item37.html">Item37</a>),所以妥协使用隐式<code>join</code>。这个决定并非没有争议并且认真讨论过在C++14中放弃这种行为。最后决定先不改变所以C++11和C++14中这里的行为是一致的。</p>
<p><em>future</em>的API没办法确定是否<em>future</em>引用了一个<code>std::async</code>调用产生的共享状态,因此给定一个任意的<em>future</em>对象,无法判断会不会阻塞析构函数从而等待异步任务的完成。这就产生了有意思的事情:</p>
<pre><code class="language-cpp">//这个容器可能在析构函数处阻塞因为其中至少一个future可能引用由std::async启动的
//未延迟任务创建出来的共享状态
std::vector&lt;std::future&lt;void&gt;&gt; futs; //std::future&lt;void&gt;相关信息见条款39
class Widget { //Widget对象可能在析构函数处阻塞
public:
private:
std::shared_future&lt;double&gt; fut;
};
</code></pre>
<p>当然,如果你有办法知道给定的<em>future</em><strong></strong>满足上面条件的任意一条(比如由于程序逻辑造成的不满足),你就可以确定析构函数不会执行“异常”行为。比如,只有通过<code>std::async</code>创建的共享状态才有资格执行“异常”行为,但是有其他创建共享状态的方式。一种是使用<code>std::packaged_task</code>,一个<code>std::packaged_task</code>对象通过包覆wrapping方式准备一个函数或者其他可调用对象来异步执行然后将其结果放入共享状态中。然后通过<code>std::packaged_task</code><code>get_future</code>函数可以获取有关该共享状态的<em>future</em></p>
<pre><code class="language-cpp">int calcValue(); //要运行的函数
std::packaged_task&lt;int()&gt; //包覆calcValue以异步运行
pt(calcValue);
auto fut = pt.get_future(); //从pt获取future
</code></pre>
<p>此时,我们知道<em>future</em>没有关联<code>std::async</code>创建的共享状态,所以析构函数肯定正常方式执行。</p>
<p>一旦被创建,<code>std::packaged_task</code>类型的<code>pt</code>就可以在一个线程上执行。(也可以通过调用<code>std::async</code>运行,但是如果你想使用<code>std::async</code>运行任务,没有理由使用<code>std::packaged_task</code>,因为在<code>std::packaged_task</code>安排任务并执行之前,<code>std::async</code>会做<code>std::packaged_task</code>做的所有事。)</p>
<p><code>std::packaged_task</code>不可拷贝,所以当<code>pt</code>被传递给<code>std::thread</code>构造函数时,必须先转为右值(通过<code>std::move</code>,参见<a href="../5.RRefMovSemPerfForw/item23.html">Item23</a></p>
<pre><code class="language-cpp">std::thread t(std::move(pt)); //在t上运行pt
</code></pre>
<p>这个例子使你对于<em>future</em>的析构函数的正常行为有一些了解,但是将这些语句放在一个作用域的语句块里会使其更易于阅读:</p>
<pre><code class="language-cpp">{ //开始代码块
std::packaged_task&lt;int()&gt;
pt(calcValue);
auto fut = pt.get_future();
std::thread t(std::move(pt)); //见下
} //结束代码块
</code></pre>
<p>此处最有趣的代码是在创建<code>std::thread</code>对象<code>t</code>之后,代码块结束前的“<code></code>”。使代码有趣的事是,在“<code></code>”中<code>t</code>上会发生什么。有三种可能性:</p>
<ul>
<li><strong><code>t</code>什么也不做</strong>。这种情况,<code>t</code>会在语句块结束时是可结合的,这会使得程序终止(参见<a href="../7.TheConcurrencyAPI/item37.html">Item37</a>)。</li>
<li><strong><code>t</code>调用<code>join</code></strong>。这种情况,不需要<code>fut</code>在它的析构函数处阻塞,因为<code>join</code>被显式调用了。</li>
<li><strong><code>t</code>调用<code>detach</code></strong>。这种情况,不需要在<code>fut</code>的析构函数执行<code>detach</code>,因为显式调用了。</li>
</ul>
<p>换句话说,当你有一个关联了<code>std::packaged_task</code>创建的共享状态的<em>future</em>时,不需要采取特殊的销毁策略,因为通常你会代码中做终止、结合或分离这些决定之一,来操作<code>std::packaged_task</code>的运行所在的那个<code>std::thread</code></p>
<p><strong>请记住:</strong></p>
<ul>
<li><em>future</em>的正常析构行为就是销毁<em>future</em>本身的数据成员。</li>
<li>引用了共享状态——使用<code>std::async</code>启动的未延迟任务建立的那个——的最后一个<em>future</em>的析构函数会阻塞住,直到任务完成。</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../7.TheConcurrencyAPI/item37.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../7.TheConcurrencyAPI/item39.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../7.TheConcurrencyAPI/item37.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../7.TheConcurrencyAPI/item39.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script type="text/javascript">
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="../clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="../book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
</body>
</html>