You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trpl-zh-cn/docs/ch16-02-message-passing.html

323 lines
24 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>消息传递 - Rust 程序设计语言 简体中文版</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="Rust 程序设计语言 简体中文版">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="">
<link rel="stylesheet" href="book.css">
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'>
<link rel="shortcut icon" href="favicon.png">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<!-- MathJax -->
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<!-- Fetch JQuery from CDN but have a local fallback -->
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script>
if (typeof jQuery == 'undefined') {
document.write(unescape("%3Cscript src='jquery.js'%3E%3C/script%3E"));
}
</script>
</head>
<body class="light">
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme = localStorage.getItem('theme');
if (theme == null) { theme = 'light'; }
$('body').removeClass().addClass(theme);
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var sidebar = localStorage.getItem('sidebar');
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
</script>
<div id="sidebar" class="sidebar">
<ul class="chapter"><li><a href="ch01-00-introduction.html"><strong>1.</strong> 介绍</a></li><li><ul class="section"><li><a href="ch01-01-installation.html"><strong>1.1.</strong> 安装</a></li><li><a href="ch01-02-hello-world.html"><strong>1.2.</strong> Hello, World!</a></li></ul></li><li><a href="ch02-00-guessing-game-tutorial.html"><strong>2.</strong> 猜猜看教程</a></li><li><a href="ch03-00-common-programming-concepts.html"><strong>3.</strong> 通用编程概念</a></li><li><ul class="section"><li><a href="ch03-01-variables-and-mutability.html"><strong>3.1.</strong> 变量和可变性</a></li><li><a href="ch03-02-data-types.html"><strong>3.2.</strong> 数据类型</a></li><li><a href="ch03-03-how-functions-work.html"><strong>3.3.</strong> 函数如何工作</a></li><li><a href="ch03-04-comments.html"><strong>3.4.</strong> 注释</a></li><li><a href="ch03-05-control-flow.html"><strong>3.5.</strong> 控制流</a></li></ul></li><li><a href="ch04-00-understanding-ownership.html"><strong>4.</strong> 认识所有权</a></li><li><ul class="section"><li><a href="ch04-01-what-is-ownership.html"><strong>4.1.</strong> 什么是所有权</a></li><li><a href="ch04-02-references-and-borrowing.html"><strong>4.2.</strong> 引用 &amp; 借用</a></li><li><a href="ch04-03-slices.html"><strong>4.3.</strong> Slices</a></li></ul></li><li><a href="ch05-00-structs.html"><strong>5.</strong> 结构体</a></li><li><ul class="section"><li><a href="ch05-01-method-syntax.html"><strong>5.1.</strong> 方法语法</a></li></ul></li><li><a href="ch06-00-enums.html"><strong>6.</strong> 枚举和模式匹配</a></li><li><ul class="section"><li><a href="ch06-01-defining-an-enum.html"><strong>6.1.</strong> 定义枚举</a></li><li><a href="ch06-02-match.html"><strong>6.2.</strong> <code>match</code>控制流运算符</a></li><li><a href="ch06-03-if-let.html"><strong>6.3.</strong> <code>if let</code>简单控制流</a></li></ul></li><li><a href="ch07-00-modules.html"><strong>7.</strong> 模块</a></li><li><ul class="section"><li><a href="ch07-01-mod-and-the-filesystem.html"><strong>7.1.</strong> <code>mod</code>和文件系统</a></li><li><a href="ch07-02-controlling-visibility-with-pub.html"><strong>7.2.</strong> 使用<code>pub</code>控制可见性</a></li><li><a href="ch07-03-importing-names-with-use.html"><strong>7.3.</strong> 使用<code>use</code>导入命名</a></li></ul></li><li><a href="ch08-00-common-collections.html"><strong>8.</strong> 通用集合类型</a></li><li><ul class="section"><li><a href="ch08-01-vectors.html"><strong>8.1.</strong> vector</a></li><li><a href="ch08-02-strings.html"><strong>8.2.</strong> 字符串</a></li><li><a href="ch08-03-hash-maps.html"><strong>8.3.</strong> 哈希 map</a></li></ul></li><li><a href="ch09-00-error-handling.html"><strong>9.</strong> 错误处理</a></li><li><ul class="section"><li><a href="ch09-01-unrecoverable-errors-with-panic.html"><strong>9.1.</strong> <code>panic!</code>与不可恢复的错误</a></li><li><a href="ch09-02-recoverable-errors-with-result.html"><strong>9.2.</strong> <code>Result</code>与可恢复的错误</a></li><li><a href="ch09-03-to-panic-or-not-to-panic.html"><strong>9.3.</strong> <code>panic!</code>还是不<code>panic!</code></a></li></ul></li><li><a href="ch10-00-generics.html"><strong>10.</strong> 泛型、trait 和生命周期</a></li><li><ul class="section"><li><a href="ch10-01-syntax.html"><strong>10.1.</strong> 泛型数据类型</a></li><li><a href="ch10-02-traits.html"><strong>10.2.</strong> trait定义共享的行为</a></li><li><a href="ch10-03-lifetime-syntax.html"><strong>10.3.</strong> 生命周期与引用有效性</a></li></ul></li><li><a href="ch11-00-testing.html"><strong>11.</strong> 测试</a></li><li><ul class="section"><li><a href="ch11-01-writing-tests.html"><strong>11.1.</strong> 编写测试</a></li><li><a href="ch11-02-running-tests.html"><strong>11.2.</strong> 运行测试</a></li><li><a href="ch11-03-test-organization.html"><strong>11.3.</strong> 测试的组织结构</a></li></ul></li><li><a href="ch12-00-an-io-project.html"><strong>12.</strong> 一个 I/O 项目</a></li><li><ul class="section"><li><a href="ch12-01-accepting-command-line-arguments.html"><strong>12.1.</strong> 接受命令行参数</a></li><li><a href="ch12-02-reading-a-file.html"><strong>12.2.</strong> 读取文件</a></li><li><a href="ch12-03-improving-error-handling-and-modularity.html"><strong>12.3.</strong> 增强错误处理和模块化</a></li><li><a href="ch12-04-testing-the-librarys-functionality.html"><strong>12.4.</strong> 测试库的功能</a></li><li><a href="ch12-05-working-with-environment-variables.html"><strong>12.5.</strong> 处理环境变量</a></li><li><a href="ch12-06-writing-to-stderr-instead-of-stdout.html"><strong>12.6.</strong> 输出到<code>stderr</code>而不是<code>stdout</code></a></li></ul></li><li><a href="ch13-00-functional-features.html"><strong>13.</strong> Rust 中的函数式语言功能</a></li><li><ul class="section"><li><a href="ch13-01-closures.html"><strong>13.1.</strong> 闭包</a></li><li><a href="ch13-02-iterators.html"><strong>13.2.</strong> 迭代器</a></li><li><a href="ch13-03-improving-our-io-project.html"><strong>13.3.</strong> 改进 I/O 项目</a></li><li><a href="ch13-04-performance.html"><strong>13.4.</strong> 性能</a></li></ul></li><li><a href="ch14-00-more-about-cargo.html"><strong>14.</strong> 更多关于 Cargo 和 Crates.io</a></li><li><ul class="section"><li><a href="ch14-01-release-profiles.html"><strong>14.1.</strong> 发布配置</a></li><li><a href="ch14-02-publishing-to-crates-io.html"><strong>14.2.</strong> 将 crate 发布到 Crates.io</a></li><li><a href="ch14-03-cargo-workspaces.html"><strong>14.3.</strong> Cargo 工作空间</a></li><li><a href="ch14-04-installing-binaries.html"><strong>14.4.</strong> 使用<code>cargo install</code>从 Crates.io 安装文件</a></li><li><a href="ch14-05-extending-cargo.html"><strong>14.5.</strong> Cargo 自定义扩展命令</a></li></ul></li><li><a href="ch15-00-smart-pointers.html"><strong>15.</strong> 智能指针</a></li><li><ul class="section"><li><a href="ch15-01-box.html"><strong>15.1.</strong> <code>Box&lt;T&gt;</code>用于已知大小的堆上数据</a></li><li><a href="ch15-02-deref.html"><strong>15.2.</strong> <code>Deref</code> Trait 允许通过引用访问数据</a></li><li><a href="ch15-03-drop.html"><strong>15.3.</strong> <code>Drop</code> Trait 运行清理代码</a></li><li><a href="ch15-04-rc.html"><strong>15.4.</strong> <code>Rc&lt;T&gt;</code> 引用计数智能指针</a></li><li><a href="ch15-05-interior-mutability.html"><strong>15.5.</strong> <code>RefCell&lt;T&gt;</code>和内部可变性模式</a></li><li><a href="ch15-06-reference-cycles.html"><strong>15.6.</strong> 引用循环和内存泄漏是安全的</a></li></ul></li><li><a href="ch16-00-concurrency.html"><strong>16.</strong> 无畏并发</a></li><li><ul class="section"><li><a href="ch16-01-threads.html"><strong>16.1.</strong> 线程</a></li><li><a href="ch16-02-message-passing.html" class="active"><strong>16.2.</strong> 消息传递</a></li><li><a href="ch16-03-shared-state.html"><strong>16.3.</strong> 共享状态</a></li><li><a href="ch16-04-extensible-concurrency-sync-and-send.html"><strong>16.4.</strong> 可扩展的并发:<code>Sync</code><code>Send</code></a></li></ul></li><li><a href="ch17-00-oop.html"><strong>17.</strong> 面向对象</a></li><li><ul class="section"><li><a href="ch17-01-what-is-oo.html"><strong>17.1.</strong> 什么是面向对象?</a></li><li><a href="ch17-02-trait-objects.html"><strong>17.2.</strong> 为使用不同类型的值而设计的 trait 对象</a></li><li><a href="ch17-03-oo-design-patterns.html"><strong>17.3.</strong> 面向对象设计模式的实现</a></li></ul></li><li><a href="ch18-00-patterns.html"><strong>18.</strong> 模式用来匹配值的结构</a></li><li><ul class="section"><li><a href="ch18-01-all-the-places-for-patterns.html"><strong>18.1.</strong> 所有可能会用到模式的位置</a></li><li><a href="ch18-02-refutability.html"><strong>18.2.</strong> refutable何时模式可能会匹配失败</a></li><li><a href="ch18-03-pattern-syntax.html"><strong>18.3.</strong> 模式的全部语法</a></li></ul></li></ul>
</div>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar" class="menu-bar">
<div class="left-buttons">
<i id="sidebar-toggle" class="fa fa-bars"></i>
<i id="theme-toggle" class="fa fa-paint-brush"></i>
</div>
<h1 class="menu-title">Rust 程序设计语言 简体中文版</h1>
<div class="right-buttons">
<i id="print-button" class="fa fa-print" title="Print this book"></i>
</div>
</div>
<div id="content" class="content">
<a class="header" href="#使用消息传递在线程间传送数据" name="使用消息传递在线程间传送数据"><h2>使用消息传递在线程间传送数据</h2></a>
<blockquote>
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch16-02-message-passing.md">ch16-02-message-passing.md</a>
<br>
commit da15de39eaabd50100d6fa662c653169254d9175</p>
</blockquote>
<p>最近人气正在上升的一个并发方式是<strong>消息传递</strong><em>message passing</em>),这里线程或 actor 通过发送包含数据的消息来沟通。这个思想来源于口号:</p>
<blockquote>
<p>Do not communicate by sharing memory; instead, share memory by
communicating.</p>
<p>不要共享内存来通讯;而是要通讯来共享内存。</p>
<p>--<a href="http://golang.org/doc/effective_go.html">Effective Go</a></p>
</blockquote>
<p>实现这个目标的主要工具是<strong>通道</strong><em>channel</em>。通道有两部分组成一个发送者transmitter和一个接收者receiver。代码的一部分可以调用发送者和想要发送的数据而另一部分代码可以在接收的那一端收取消息。</p>
<p>我们将编写一个例子使用一个线程生成值并向通道发送他们。主线程会接收这些值并打印出来。</p>
<p>首先,如列表 16-6 所示,先创建一个通道但不做任何事:</p>
<p><span class="filename">Filename: src/main.rs</span></p>
<pre><code class="language-rust">use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
# tx.send(()).unwrap();
}
</code></pre>
<p><span class="caption">Listing 16-6: Creating a channel and assigning the two
halves to <code>tx</code> and <code>rx</code></span></p>
<p><code>mpsc::channel</code>函数创建一个新的通道。<code>mpsc</code><strong>多个生产者,单个消费者</strong><em>multiple producer, single consumer</em>)的缩写。简而言之,可以有多个产生值的<strong>发送端</strong>,但只能有一个消费这些值的<strong>接收端</strong>。现在我们以一个单独的生产者开始,不过一旦例子可以工作了就会增加多个生产者。</p>
<p><code>mpsc::channel</code>返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,很多人使用<code>tx</code><code>rx</code>作为<strong>发送者</strong><strong>接收者</strong>的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个<code>let</code>语句和模式来解构了元组。第十八章会讨论<code>let</code>语句中的模式和解构。</p>
<p>让我们将发送端移动到一个新建线程中并发送一个字符串,如列表 16-7 所示:</p>
<p><span class="filename">Filename: src/main.rs</span></p>
<pre><code class="language-rust">use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from(&quot;hi&quot;);
tx.send(val).unwrap();
});
}
</code></pre>
<p><span class="caption">Listing 16-7: Moving <code>tx</code> to a spawned thread and sending
&quot;hi&quot;</span></p>
<p>正如上一部分那样使用<code>thread::spawn</code>来创建一个新线程。并使用一个<code>move</code>闭包来将<code>tx</code>移动进闭包这样新建线程就是其所有者。</p>
<p>通道的发送端有一个<code>send</code>方法用来获取需要放入通道的值。<code>send</code>方法返回一个<code>Result&lt;T, E&gt;</code>类型,因为如果接收端被丢弃了,将没有发送值的目标,所以发送操作会出错。在这个例子中,我们简单的调用<code>unwrap</code>来忽略错误,不过对于一个真实程序,需要合理的处理它。第九章是你复习正确错误处理策略的好地方。</p>
<p>在列表 16-8 中,让我们在主线程中从通道的接收端获取值:</p>
<p><span class="filename">Filename: src/main.rs</span></p>
<pre><code class="language-rust">use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from(&quot;hi&quot;);
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!(&quot;Got: {}&quot;, received);
}
</code></pre>
<p><span class="caption">Listing 16-8: Receiving the value &quot;hi&quot; in the main thread
and printing it out</span></p>
<p>通道的接收端有两个有用的方法:<code>recv</code><code>try_recv</code>。这里,我们使用了<code>recv</code>,它是 <em>receive</em> 的缩写。这个方法会阻塞执行直到从通道中接收一个值。一旦发送了一个值,<code>recv</code>会在一个<code>Result&lt;T, E&gt;</code>中返回它。当通道发送端关闭,<code>recv</code>会返回一个错误。<code>try_recv</code>不会阻塞;相反它立刻返回一个<code>Result&lt;T, E&gt;</code></p>
<p>如果运行列表 16-8 中的代码,我们将会看到主线程打印出这个值:</p>
<pre><code>Got: hi
</code></pre>
<a class="header" href="#通道与所有权如何交互" name="通道与所有权如何交互"><h3>通道与所有权如何交互</h3></a>
<p>现在让我们做一个试验来看看通道与所有权如何在一起工作:我们将尝试在新建线程中的通道中发送完<code>val</code>之后再使用它。尝试编译列表 16-9 中的代码:</p>
<p><span class="filename">Filename: src/main.rs</span></p>
<pre><code class="language-rust,ignore">use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from(&quot;hi&quot;);
tx.send(val).unwrap();
println!(&quot;val is {}&quot;, val);
});
let received = rx.recv().unwrap();
println!(&quot;Got: {}&quot;, received);
}
</code></pre>
<p><span class="caption">Listing 16-9: Attempting to use <code>val</code> after we have sent
it down the channel</span></p>
<p>这里尝试在通过<code>tx.send</code>发送<code>val</code>到通道中之后将其打印出来。这是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们在此使用它之前就修改或者丢弃它。这会由于不一致或不存在的数据而导致错误或意外的结果。</p>
<p>尝试编译这些代码Rust 会报错:</p>
<pre><code>error[E0382]: use of moved value: `val`
--&gt; src/main.rs:10:31
|
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!(&quot;val is {}&quot;, val);
| ^^^ value used here after move
|
= note: move occurs because `val` has type `std::string::String`, which does
not implement the `Copy` trait
</code></pre>
<p>我们的并发错误会造成一个编译时错误!<code>send</code>获取其参数的所有权并移动这个值归接收者所有。这个意味着不可能意外的在发送后再次使用这个值;所有权系统检查一切是否合乎规则。</p>
<p>在这一点上,消息传递非常类似于 Rust 的单所有权系统。消息传递的拥护者出于相似的原因支持消息传递,就像 Rustacean 们欣赏 Rust 的所有权一样:单所有权意味着特定类型问题的消失。如果一次只有一个线程可以使用某些内存,就没有出现数据竞争的机会。</p>
<a class="header" href="#发送多个值并观察接收者的等待" name="发送多个值并观察接收者的等待"><h3>发送多个值并观察接收者的等待</h3></a>
<p>列表 16-8 中的代码可以编译和运行,不过这并不是很有趣:通过它难以看出两个独立的线程在一个通道上相互通讯。列表 16-10 则有一些改进会证明这些代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一段时间。</p>
<p><span class="filename">Filename: src/main.rs</span></p>
<pre><code class="language-rust">use std::thread;
use std::sync::mpsc;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from(&quot;hi&quot;),
String::from(&quot;from&quot;),
String::from(&quot;the&quot;),
String::from(&quot;thread&quot;),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::new(1, 0));
}
});
for received in rx {
println!(&quot;Got: {}&quot;, received);
}
}
</code></pre>
<p><span class="caption">Listing 16-10: Sending multiple messages and pausing
between each one</span></p>
<p>这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个<code>Duration</code>值调用<code>thread::sleep</code>函数来暂停一秒。</p>
<p>在主线程中,不再显式的调用<code>recv</code>函数:而是将<code>rx</code>当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当通道被关闭时,迭代器也将结束。</p>
<p>当运行列表 16-10 中的代码时,将看到如下输出,每一行都会暂停一秒:</p>
<pre><code>Got: hi
Got: from
Got: the
Got: thread
</code></pre>
<p>在主线程中并没有任何暂停或位于<code>for</code>循环中用于等待的代码,所以可以说主线程是在等待从新建线程中接收值。</p>
<a class="header" href="#通过克隆发送者来创建多个生产者" name="通过克隆发送者来创建多个生产者"><h3>通过克隆发送者来创建多个生产者</h3></a>
<p>差不多在本部分的开头,我们提到了<code>mpsc</code><em>multiple producer, single consumer</em> 的缩写。可以扩展列表 16-11 中的代码来创建都向同一接收者发送值的多个线程。这可以通过克隆通道的发送端在来做到,如列表 16-11 所示:</p>
<p><span class="filename">Filename: src/main.rs</span></p>
<pre><code class="language-rust"># use std::thread;
# use std::sync::mpsc;
# use std::time::Duration;
#
# fn main() {
// ...snip...
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![
String::from(&quot;hi&quot;),
String::from(&quot;from&quot;),
String::from(&quot;the&quot;),
String::from(&quot;thread&quot;),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::new(1, 0));
}
});
thread::spawn(move || {
let vals = vec![
String::from(&quot;more&quot;),
String::from(&quot;messages&quot;),
String::from(&quot;for&quot;),
String::from(&quot;you&quot;),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::new(1, 0));
}
});
// ...snip...
#
# for received in rx {
# println!(&quot;Got: {}&quot;, received);
# }
# }
</code></pre>
<p><span class="caption">Listing 16-11: Sending multiple messages and pausing
between each one</span></p>
<p>这一次,在创建新线程之前,我们对通道的发送端调用了<code>clone</code>方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的通道发送端传递给第二个新建线程,这样每个线程将向通道的接收端发送不同的消息。</p>
<p>如果运行这些代码,你<strong>可能</strong>会看到这样的输出:</p>
<pre><code>Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you
</code></pre>
<p>虽然你可能会看到这些以不同的顺序出现。这依赖于你的系统!这也就是并发既有趣又困难的原因。如果你拿<code>thread::sleep</code>做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定并每次都会产生不同的输出。</p>
<p>现在我们见识过了通道如何工作,再看看共享内存并发吧。</p>
</div>
<!-- Mobile navigation buttons -->
<a href="ch16-01-threads.html" class="mobile-nav-chapters previous">
<i class="fa fa-angle-left"></i>
</a>
<a href="ch16-03-shared-state.html" class="mobile-nav-chapters next">
<i class="fa fa-angle-right"></i>
</a>
</div>
<a href="ch16-01-threads.html" class="nav-chapters previous" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-left"></i>
</a>
<a href="ch16-03-shared-state.html" class="nav-chapters next" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-right"></i>
</a>
</div>
<!-- Local fallback for Font Awesome -->
<script>
if ($(".fa").css("font-family") !== "FontAwesome") {
$('<link rel="stylesheet" type="text/css" href="_FontAwesome/css/font-awesome.css">').prependTo('head');
}
</script>
<!-- Livereload script (if served using the cli tool) -->
<script src="highlight.js"></script>
<script src="book.js"></script>
</body>
</html>