EffectiveModernCppChinese/7.TheConcurrencyAPI/Item35.html
2023-05-06 06:19:14 +00:00

232 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 35:优先考虑基于任务的编程而非基于线程的编程 - 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" class="active">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">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>
<h1 id="第7章-并发api"><a class="header" href="#第7章-并发api">第7章 并发API</a></h1>
<p><strong>CHAPTER 7 The Concurrency API</strong></p>
<p>C++11的伟大成功之一是将并发整合到语言和库中。熟悉其他线程API比如pthreads或者Windows threads的开发者有时可能会对C++提供的斯巴达式译者注应该是简陋和严谨的意思功能集感到惊讶这是因为C++对于并发的大量支持是在对编译器作者约束的层面。由此产生的语言保证意味着在C++的历史中,开发者首次通过标准库可以写出跨平台的多线程程序。这为构建表达库奠定了坚实的基础,标准库并发组件(任务<em>tasks</em>,期望<em>futures</em>,线程<em>threads</em>,互斥<em>mutexes</em>,条件变量<em>condition variables</em>,原子对象<em>atomic objects</em>等)仅仅是成为并发软件开发者丰富工具集的基础。</p>
<p>在接下来的条款中,记住标准库有两个<em>future</em>的模板:<code>std::future</code><code>std::shared_future</code>。在许多情况下,区别不重要,所以我们经常简单的混于一谈为<em>futures</em></p>
<h2 id="条款三十五优先考虑基于任务的编程而非基于线程的编程"><a class="header" href="#条款三十五优先考虑基于任务的编程而非基于线程的编程">条款三十五:优先考虑基于任务的编程而非基于线程的编程</a></h2>
<p><strong>Item 35: Prefer task-based programming to thread-based</strong></p>
<p>如果开发者想要异步执行<code>doAsyncWork</code>函数,通常有两种方式。其一是通过创建<code>std::thread</code>执行<code>doAsyncWork</code>,这是应用了<strong>基于线程</strong><em>thread-based</em>)的方式:</p>
<pre><code class="language-cpp">int doAsyncWork();
std::thread t(doAsyncWork);
</code></pre>
<p>其二是将<code>doAsyncWork</code>传递给<code>std::async</code>,一种<strong>基于任务</strong><em>task-based</em>)的策略:</p>
<pre><code class="language-cpp">auto fut = std::async(doAsyncWork); //“fut”表示“future”
</code></pre>
<p>这种方式中,传递给<code>std::async</code>的函数对象被称为一个<strong>任务</strong><em>task</em>)。</p>
<p>基于任务的方法通常比基于线程的方法更优,原因之一上面的代码已经表明,基于任务的方法代码量更少。我们假设调用<code>doAsyncWork</code>的代码对于其提供的返回值是有需求的。基于线程的方法对此无能为力,而基于任务的方法就简单了,因为<code>std::async</code>返回的<em>future</em>提供了<code>get</code>函数(从而可以获取返回值)。如果<code>doAsycnWork</code>发生了异常,<code>get</code>函数就显得更为重要,因为<code>get</code>函数可以提供抛出异常的访问,而基于线程的方法,如果<code>doAsyncWork</code>抛出了异常,程序会直接终止(通过调用<code>std::terminate</code>)。</p>
<p>基于线程与基于任务最根本的区别在于基于任务的抽象层次更高。基于任务的方式使得开发者从线程管理的细节中解放出来对此在C++并发软件中总结了“<em>thread</em>”的三种含义:</p>
<ul>
<li><strong>硬件线程</strong>hardware threads是真实执行计算的线程。现代计算机体系结构为每个CPU核心提供一个或者多个硬件线程。</li>
<li><strong>软件线程</strong>software threads也被称为系统线程OS threads、system threads是操作系统假设有一个操作系统。有些嵌入式系统没有。管理的在硬件线程上执行的线程。通常可以存在比硬件线程更多数量的软件线程因为当软件线程被阻塞的时候比如 I/O、同步锁或者条件变量操作系统可以调度其他未阻塞的软件线程执行提供吞吐量。</li>
<li><strong><code>std::thread</code></strong> 是C++执行过程的对象,并作为软件线程的句柄(<em>handle</em>)。有些<code>std::thread</code>对象代表“空”句柄,即没有对应软件线程,因为它们处在默认构造状态(即没有函数要执行);有些被移动走(移动到的<code>std::thread</code>就作为这个软件线程的句柄);有些被<code>join</code>(它们要运行的函数已经运行完);有些被<code>detach</code>(它们和对应的软件线程之间的连接关系被打断)。</li>
</ul>
<p>软件线程是有限的资源。如果开发者试图创建大于系统支持的线程数量,会抛出<code>std::system_error</code>异常。即使你编写了不抛出异常的代码,这仍然会发生,比如下面的代码,即使 <code>doAsyncWork</code><code>noexcept</code></p>
<pre><code class="language-cpp">int doAsyncWork() noexcept; //noexcept见条款14
</code></pre>
<p>这段代码仍然会抛出异常:</p>
<pre><code class="language-cpp">std::thread t(doAsyncWork); //如果没有更多线程可用,则抛出异常
</code></pre>
<p>设计良好的软件必须能有效地处理这种可能性,但是怎样做?一种方法是在当前线程执行<code>doAsyncWork</code>但是这可能会导致负载不均而且如果当前线程是GUI线程可能会导致响应时间过长的问题。另一种方法是等待某些当前运行的软件线程结束之后再创建新的<code>std::thread</code>,但是仍然有可能当前运行的线程在等待<code>doAsyncWork</code>的动作(例如产生一个结果或者报告一个条件变量)。</p>
<p>即使没有超出软件线程的限额,仍然可能会遇到<strong>资源超额</strong><em>oversubscription</em>的麻烦。这是一种当前准备运行的即未阻塞的软件线程大于硬件线程的数量的情况。情况发生时线程调度器操作系统的典型部分会将软件线程时间切片分配到硬件上。当一个软件线程的时间片执行结束会让给另一个软件线程此时发生上下文切换。软件线程的上下文切换会增加系统的软件线程管理开销当软件线程安排到与上次时间片运行时不同的硬件线程上这个开销会更高。这种情况下1CPU缓存对这个软件线程很冷淡即几乎没有什么数据也没有有用的操作指南2“新”软件线程的缓存数据会“污染”“旧”线程的数据旧线程之前运行在这个核心上而且还有可能再次在这里运行。</p>
<p>避免资源超额很困难因为软件线程之于硬件线程的最佳比例取决于软件线程的执行频率那是动态改变的比如一个程序从IO密集型变成计算密集型执行频率是会改变的。而且比例还依赖上下文切换的开销以及软件线程对于CPU缓存的使用效率。此外硬件线程的数量和CPU缓存的细节比如缓存多大相应速度多少取决于机器的体系结构即使经过调校在某一种机器平台避免了资源超额而仍然保持硬件的繁忙状态换一个其他类型的机器这个调校并不能提供较好效果的保证。</p>
<p>如果你把这些问题推给另一个人做,你就会变得很轻松,而使用<code>std::async</code>就做了这件事:</p>
<pre><code class="language-cpp">auto fut = std::async(doAsyncWork); //线程管理责任交给了标准库的开发者
</code></pre>
<p>这种调用方式将线程管理的职责转交给C++标准库的开发者。举个例子,这种调用方式会减少抛出资源超额异常的可能性,因为这个调用可能不会开启一个新的线程。你会想:“怎么可能?如果我要求比系统可以提供的更多的软件线程,创建<code>std::thread</code>和调用<code>std::async</code>为什么会有区别?”确实有区别,因为以这种形式调用(即使用默认启动策略——见<a href="../7.TheConcurrencyAPI/item36.html">Item36</a>)时,<code>std::async</code>不保证会创建新的软件线程。然而,他们允许通过调度器来将特定函数(本例中为<code>doAsyncWork</code>)运行在等待此函数结果的线程上(即在对<code>fut</code>调用<code>get</code>或者<code>wait</code>的线程上),合理的调度器在系统资源超额或者线程耗尽时就会利用这个自由度。</p>
<p>如果考虑自己实现“在等待结果的线程上运行输出结果的函数”,之前提到了可能引出负载不均衡的问题,这问题不那么容易解决,因为应该是<code>std::async</code>和运行时的调度程序来解决这个问题而不是你。遇到负载不均衡问题时,对机器内发生的事情,运行时调度程序比你有更全面的了解,因为它管理的是所有执行过程,而不仅仅个别开发者运行的代码。</p>
<p>有了<code>std::async</code>GUI线程中响应变慢仍然是个问题因为调度器并不知道你的哪个线程有高响应要求。这种情况下你会想通过向<code>std::async</code>传递<code>std::launch::async</code>启动策略来保证想运行函数在不同的线程上执行(见<a href="../7.TheConcurrencyAPI/item36.html">Item36</a>)。</p>
<p>最前沿的线程调度器使用系统级线程池(<em>thread pool</em>)来避免资源超额的问题,并且通过工作窃取算法(<em>work-stealing algorithm</em>来提升了跨硬件核心的负载均衡。C++标准实际上并不要求使用线程池或者工作窃取实际上C++11并发规范的某些技术层面使得实现这些技术的难度可能比想象中更有挑战。不过库开发者在标准库实现中采用了这些技术也有理由期待这个领域会有更多进展。如果你当前的并发编程采用基于任务的方式在这些技术发展中你会持续获得回报。相反如果你直接使用<code>std::thread</code>编程,处理线程耗尽、资源超额、负责均衡问题的责任就压在了你身上,更不用说你对这些问题的解决方法与同机器上其他程序采用的解决方案配合得好不好了。</p>
<p>对比基于线程的编程方式,基于任务的设计为开发者避免了手动线程管理的痛苦,并且自然提供了一种获取异步执行程序的结果(即返回值或者异常)的方式。当然,仍然存在一些场景直接使用<code>std::thread</code>会更有优势:</p>
<ul>
<li><strong>你需要访问非常基础的线程API</strong>。C++并发API通常是通过操作系统提供的系统级APIpthreads或者Windows threads来实现的系统级API通常会提供更加灵活的操作方式举个例子C++没有线程优先级和亲和性的概念。为了提供对底层系统级线程API的访问<code>std::thread</code>对象提供了<code>native_handle</code>的成员函数,而<code>std::future</code>(即<code>std::async</code>返回的东西)没有这种能力。</li>
<li><strong>你需要且能够优化应用的线程使用</strong>。举个例子,你要开发一款已知执行概况的服务器软件,部署在有固定硬件特性的机器上,作为唯一的关键进程。</li>
<li><strong>你需要实现C++并发API之外的线程技术</strong>比如C++实现中未支持的平台的线程池。</li>
</ul>
<p>这些都是在应用开发中并不常见的例子,大多数情况,开发者应该优先采用基于任务的编程方式。</p>
<p><strong>请记住:</strong></p>
<ul>
<li><code>std::thread</code> API不能直接访问异步执行的结果如果执行函数有异常抛出代码会终止执行。</li>
<li>基于线程的编程方式需要手动的线程耗尽、资源超额、负责均衡、平台适配性管理。</li>
<li>通过带有默认启动策略的<code>std::async</code>进行基于任务的编程方式会解决大部分问题。</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../6.LambdaExpressions/item34.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/item36.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="../6.LambdaExpressions/item34.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/item36.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>