Rust
学习中关于创建可执行程序的三种方式:
直接用cargo
来创建可执行项目,如下:
该命令将创建名字为multi_bins
的可执行程序项目。项目目录结构如下:
其中的main.rs
文件为可执行程序的源码文件,它包含了main
函数,其简单输出一句"Hello, world!"
执行如下命令将编译得到可执行程序:
可执行文件创建成功后,其路径为:
执行该程序
在src
目录下创建bin
目录
然后在bin
目录下创建包含main
函数的源码文件。可以创建多个这样的文件,例如创建如下两个文件:
可以直接用cargo run --bin
来运行这两个可执行程序。实际上是在target/debug
下生成这两个文件的可执行程序并且运行它们
也可以编译得到可执行文件:
在Cargo.toml
文件中进行配置,指定可执行程序的名称以及它的源码路径。这种方法可以使得可执行程序的源码既不是main.rs
,也不需要放置于src/bin
目录下,还可以随意指定它的名字。
执行如下构建命令:
生成可执行文件如下:
上述三种方式中,第一种是最常用的,它是Rust的默认方式,main.rs
默认为可执行项目的执行程序源码,main
函数就是其入口。第二种方式把所有可执行程序的源码都放置到src/bin
目录下,Cargo
编译项目的时候会将这个目录下所有的包含main
函数的源码文件都编译成和源码文件同名的可执行程序。第三种方式是最灵活的,它可以在Cargo.toml
里进行灵活的配置,使得可执行程序的名称与它的源码路径解耦;从另一个角度来说这种方式使得可执行程序源码可以不必一定是src/main.rs
或src/bin/
中的包含main
函数的文件。
我们已经在这个博客里写了几篇关于我们如何大量使用Erlang/OTP来构建我们的实时竞价平台服务器的文章。
这些系统很大,到现在已经存在了很长时间。 就像任何大型旧系统一样,它们包含一些不再使用的代码片段。 需要明确的是:它们没有被破坏,它们甚至被测试所覆盖,但它们都没有在生产中使用。
在Erlang中,这些死代码表现为未使用的函数。 确切地说:它们是未使用的导出函数,因为在编译时会检测到未导出并且未使用的函数。
在一个大系统里找出未使用的导出函数是很困难的。幸运的是,Erlang/OTP已经给我们提供了一个工具来做这个事情,它就是:Xref。
Xerf是一个交叉引用工具,可用于查找函数、模块、应用程序和发布之间的依赖关系。
如果你使用rebar3管理你的项目,那么你就可以运行如下简单的命令来使用Xref:
|
|
如果你没有在 rebar.config 文件里为 Xref 进行任何配置的话,它将会检查你整个项目,并且进行所有可能的检查。这样的话,对于大项目来说,警告列表就会大量生成,因此,人们常常在 rebar.config 里进行如下配置:
|
|
也就是说,这样的配置产生的报告就是如下三种:
你可以在 里找到所有可检查的列表,但是,我想要你注意的是,后两项是编译器已经检查到的(如果你启用了正确的警告),而真正有效的检查是对 undefined_function_calls 的执行。这是一个很好的运行检查,但它不会帮助我们解决原始的死代码问题。
那么让我们来看看我们没有执行的检查。 通常,undefined_functions将报告与undefined_function_calls相同的结果,但是没有用实际的函数调用(不是很有用)。 deprecated_functions和deprecated_function_calls也是如此。 但是,我们有exports_not_used,这正是我们正在寻找的检查。
把 exports_not_used 添加到我们的配置列表里,它将会报告那些我们导出了但是没有使用的函数。真是太棒了!
但是为什么没有人使用它呢?🤔
使用exports_not_used时有一些注意事项。 我现在列出它们,我会告诉你如何解决或至少让它们正常运作。
Xref将为在代码中找不到使用的每个导出函数报告一个警告。但是,Xref找不到它在哪里被使用,并不意味着实际使用函数的地方不存在。例如,Xref不能处理动态函数调用,但是它们是完全有效的使用方式。假设你有一个模块看起来像下面代码所示(别问我为什么):
|
|
而在另一些模块里,你可能这么调用:sample:some_function(sample)
。Xref没有足够聪明到探测到 sample:some_other_function/1
被真正使用了,因为它仅仅是通过动态评估来进行检查。上面的例子只是执行函数动态调用的其中一种方式。你可以通过如下示例看看其他方式:
|
|
注:如果添加{xref_warnings,true}. 到你的 rebar.config文件中,Xref至少会为这些无法解析的动态调用打印警告,如下所示:
|
|
无论如何,只要你的系统比原型稍微大一点,你就会开始在代码的各处出现导出而未使用的函数。但不要惊慌,实际上有一种方法可以避免这些警告,而且它还有一些额外的好处。这种方法就是使用ignore_xref
。
-ignore_xref是一个属性,你可以把它添加到你到模块中来阻止Xref对特定对函数发出警告。它的使用看起来如下所示:
|
|
现在如果你查看OTP中Xref的文档,你不会找到有关这个属性的片言只语。这是因为它不是官方属性。ignore_xref 是 rebar3 xref(xref_run也一样) 的未公开文档属性。这个属性可以被添加到你的模块中,在当中列出那些你不想被Xref检查的函数。它的语法如下所示:
|
|
使用这种方式,你可以有效地移除掉有关那些被导出的只被动态调用的函数的警告。另外,如上例所示,你可以在这个属性的上方添加注释,这些函数将在哪里被使用。
你可能不是一个动态生成代码的发烧友,但是有时候你还真不可避免地要遇到动态生成的代码。
例如,我们在系统的几个地方使用了protobuf
,导致我们使用了gpb和它的rebar3
插件。当用gpb写模块的时候,它是不知道这个模块是如何被使用的,因此它就无法判断函数是否不需要被导出。这就意味着,当我们使用Xref
的时候,会得到由gpb生成的所有被导出而未被使用的函数的警告。
如何避免这些警告呢?我们不能使用 -ignore_xref
,因为代码不是我们手写的,而是gpb自动生成的。事实证明,还有另一种方式。 我们可以在rebar.config中使用读起来怪怪的的xref_ignores属性。 它基本上允许你拥有一个可以在任何地方被忽略的全局的函数列表。 它看起来像这样:
|
|
目前还没有办法忽略一个模块里的所有函数,不过我已经给这个项目提了 issue。或许你也可以把这个问题作为一个 hacktoberfest 项目来攻克。
如果你有一些函数仅仅因为当你远程登录到生产中的服务器时,会在shell中使用它们而被导出,那会怎么样?如果它们被外部脚本在你的节点执行RPC调用的时候被使用或诸如此类的情况,那会怎么样?
在这种情况下,我建议你使用 -ignore_xref
,并在那里添加一个适当的注释,说明如何/何时/在哪里使用这些函数。我保证,这么做将来会有回报的。
我有时候会遇到另一种不同的场景(特别是有关遗留代码的时候),就是被导出只是为了它们可以被测试。我们的想法是模拟它们或访问一些内部逻辑,否则这些内部逻辑应该隐藏在生产系统中。
首先,如果你使用 ,你就不需要导出那些函数。你可以在测试中使用非导出的函数。
现在,如果你使用通用测试或其他需要在被测模块外部编写测试的框架,那就是另一回事了。 我认为重要的是要考虑导出函数只是为了在测试中使用它们一般是不可取的,因为……
尽管如此,有时候还是没有办法解决它:你需要模拟一些外部世界看不到的东西,你需要验证一些只以非常复杂的格式暴露的数据或者检测真正难以捕获的副作用。 在这些场景中,ignore_xref
以及一个简洁的注释对于未来的开发人员来说是避免意外和挫折的好工具,使得他们可以发现一个未使用的函数,决定删除它。
最后,还有另外一种情况,你确实需要导出应用程序中没有实际使用的函数:当你的应用程序是一个库时(例如,当你正在构建一个应用程序以便在其他系统中用作依赖项时)。在这种情况下,一些函数构成了应用程序的接口,它们不会被应用程序使用。它们是公开的,所以你的用户可以在他们的应用程序中调用它们。
这些函数都将被报告为未使用的导出函数,并且必须为所有这些函数编写ignore_xref
/ xref_ignore
,这样做不是好的办法。但是真正好的办法是在测试中覆盖它们。如果你这样做了,你就有了避免警告的方法并且实际上只为导出、未使用和未测试的函数生成警告。你可以像如下方式运行Xref
:
|
|
使用test profile
,rebar3
将把所有测试模块包含到分析中,并且由于你的接口函数将在这里使用,所以它不会警告您。
虽然Xref
是一个强大的工具,但是它需要一些调整来挖掘它的全部潜力。
首先,你必须使用正确的检查。我们的推荐检查列表是:
|
|
然后,你必须适当地使用-ignore_xref
属性和xref_ignores
配置参数来标识有意导出和未使用的所有函数。 如果你正在编写库,则还应该使用rebar3 as test xref
来考虑分析中的测试。
有了这些,你应该会得到0个警告的报告,因此你可以确定项目中没有任何死代码。
嗯,实际上,你没有任何死函数(未使用的导出)。 但你仍然会有未使用的函数子句,未使用的case子句等形式的死代码,这些代码还不少,Xref
将不会检测到这些问题。
为此,你需要一个更强大的工具:dialyzer。 我们不会在本文中介绍它,但请继续关注后续的文章。
]]>原文链接: http://tech.adroll.com/blog/dev/2018/10/09/remove-erlang-dead-code-xref.html
在设置开发环境时,必须执行的一个强制性步骤是配置GOPATH环境变量。$GOPATH
是Go编译器在构建Go应用程序时用来搜索依赖项的。$GOPATH
包含源代码和二进制文件。
GOPATH包含以下目录:
在最新的主要版本Go v1.11中,GOPATH不再是强制性的。 Go团队在这个新版本中引入了模块。 模块是相关Go包的集合。 这是Go团队在Go中改进包管理的重要一步。
使用当前版本1.11,你现在可以在GOPATH之外构建Go应用程序。在1.11中,当你将源代码放在$GOPATH
中时,它会忽略模块特性,并使用GOPATH搜索源代码依赖项。但是,如果将源代码放在GOPATH之外,模块支持就会自动启用。这意味着,你现在可以从任意目录构建应用程序!
我创建了一个样例Go模块,你可以用它来学习Go模块的概念。这是源代码的地址:https://github.com/donvito/hellomod。你可以随意fork,并用于你自己的学习。该模块有3个版本。
为了演示如何在应用程序中使用Go模块,让我们假设hellomod是应用程序中需要的第三方模块。
让我们创建一个客户端应用程序来使用hellomod模块。 如下是我们可以开始的一些代码。 将此代码保存为main.go在你希望的任何目录中。
main.go
|
|
我们要初始化对模块的支持。确保GOPATH没有被设置,这样我们就可以自动使用Go 1.11中的模块特性。在终端中执行下面的命令。
|
|
这将创建一个go.mod文件。除了你刚刚初始化的模块名,这里没有什么可以看到的。go.mod将包含如下内容:
|
|
在执行“go mod init hello”之后,执行如下命令来构建你的应用程序:
|
|
“go build”的作用是根据import语句解析应用程序的依赖,并添加最新版本的依赖,然后编译应用程序并生成二进制文件。
由于我们最新版本的hellomod是v1.0.1,go.mod现在包含内容如下:
|
|
“go build”还会创建一个go.sum文件,其中包含特定模块版本内容的预期加密校验和。 它的定义来自这里。https://github.com/golang/go/wiki/Modules#releasing-modules-all-versions
|
|
以下是我们迄今执行的命令:
你可以运行由“go build”创建的二进制文件来检查我们是否在使用hellomod的1.0.1版本。
|
|
假设你想恢复到hellomod模块的先前版本v1.0.0。 你只需执行以下命令即可。 这将使用以前的版本。
|
|
作为参考,这是v1.0.0的样子:
hellomod v1.0.0
|
|
要升级主版本,你需要更import语句中的版本。
main.go
|
|
然后构建应用程序并运行二进制文件“hello”。
|
|
以下是go.mod在执行“go build”后的样子:
|
|
是的,可以使用同一模块的不同版本。 一下是一个示例,我们可以看到模块的两个不同版本可以独立使用。 你需要为每个版本使用别名以避免冲突。
main.go ( 客户端应用程序使用两个不同版本的hellomod模块 )
|
|
本文到此结束!希望你能从这篇文章中学到一些东西!: )
]]>
我们发现,对于具有“copy trait”的类型(即其数据可以存储在栈上的类型),所有权模型的行为类似于其他可能使用不同范式的语言,如垃圾收集。但是对于没有这种trait的类型,我们需要更加意识到所有权规则。
尽管所有权可能会带来设计上的妥协,但它以灵活性、明确性和安全性来弥补。
在第一个例子里,我们先传递一个字符串字面值(它的数据存储在栈上)给函数foo()。在第二个例子里,我们传递一个字符串类型的值(它的值存储在堆上)给不同函数foo()。在这两个例子的main()和foo()的实现,我们把在各自作用域变量的内存地址打印出来。
在第一个例子中,当我们复制变量的值并将其绑定到一个新变量时,我们看到类似的行为。 这是因为字符串字面量使用栈;存储指针所需的大小在编译时是已知的,因此,我们可以轻松地复制它的值并将其压入栈中。
这意味着函数main()和foo()都拥有自己的存储在变量string的指针副本。 当foo()的作用域结束时,foo()负责删除它自己的string变量,当main()的作用域结束时,它也负责删除它拥有的string变量。
相反,在第二个例子里,main()把变量string的所有权移交给foo()。这意味着,main()不再有变量string的所有权,即指针指向的内存的所有权。如果在所有权移交后,我们还在main()里尝试访问string变量,我们将收到错误。
复制很昂贵,Rust不复制,而是使得foo()负责内存地址0x7efced01c010的数据,如示例注释所示。
现在,仅当foo()超过作用域,Rust才释放那个地址的内存,从而使指向同一地址的任何其他变量失效。同样,我们这样做是为了避免重复释放的错误。
对于第二个示例,如果我们确实要复制string的值,那么main()和foo()都拥有自己的副本,类似于在栈上使用字符串字面量时,我们可以通过使用clone()方法进行“深度复制” :
如注释所示,main()和foo()有各自的string复制的所有权。虽然这是一个有效的解决方案,但它不是最有效率的,因为Rust每次都需要在堆上进行内存分配的过程。有时你真的希望这两个函数与同一个数据交互!(稍后将详细介绍)。
正如通过调用另一个函数并传入变量来获取所有权一样,可以通过从不同函数返回来赋予函数所有权。
foo()现在通过将字符串返回到调用foo()的位置来赋予main()所有权。正如所料,只有当main()的作用域结束时,Rust才会释放0x7fc98be1c010地址的内存。
如果我们遵循这一趋势,那么我们既可以给予所有权,也可以通过接收foo()返回相同的字符串类型将所有权归还给我们。
但是,将这些值传入和传出函数似乎很麻烦。 幸运的是,这是Rust维护者考虑到的头痛问题:
取得所有权然后通过各种函数归还所有权有点单调乏味。 如果我们想让一个函数使用一个值但不取得所有权,该怎么办? 如果我们想要再次使用它,除了我们可能想要返回的函数体所产生的任何数据之外,我们传入的任何内容都需要传回。 对我们来说幸运的是,Rust有一个这个概念的特性,称为引用。
— Rust Book
所有权适用于共享和传递数据,但是,你必须遵循一些规则。
借用看起来像这样:
main()让foo()访问string,但是,(如果图中的标签所示),main()仍然是string的所有者。这意味着,foo()的作用域结束后,string不会被从内存中释放;main()仍然负责string的内存空间。
以下是我们如何在Rust中编写这种交互:
就像我们的绘图一样,main()将字符串的引用传递给foo(),而foo()接收String类型引用。 引用由&符号表示。 在foo()的作用域结束之后,执行返回到它的调用者,main(),并且字符串仍然有效。 foo()不必返回所有权,因为它从未获得所有权,只是借用了它。
&表示引用,它允许传递值而不需要放弃所有权!当我们传递引用的时候,Rust知道,所有权以及内存空间的释放的责任依然属于原来的所有者。
Rust允许我们创建任意数量的引用。
不管我们传递string的引用多少次,它的所有权依然是原来的所有者。(在这种情况下,所有权返回到字符串最初被实例化的地方,但是请记住,我们可以传递所有权,然后创建引用。)
最后要提到的是可变性。 Rust通常以函数式编写,但作者非常务实,并且理解现代语言并不总是那么非黑即白,因此Rust适应了可变性。
Rust允许我们使用 mut 关键字来使得值可变。注意内存地址的变化,这表明必须在堆上重新分配字符串。
既然我们有一个可变变量,我们就可以做出一个可变引用!
此处的语法有点特别,但是我们看到首先我们需要声明一个可变变量 let mut string
。然后当我们传递可变引用的时候,使用 &mut
。最后,我们在函数签名上用 &mut
严格声明我们的函数接收一个可变引用。
现在,我们仍然可以确定只有main()负责string变量的销毁,而同时允许其他函数来修改string。
那些熟悉内存管理的人可能会想,如果不加以控制,这将是多么危险。如果几个函数持有一个可变引用,并试图同时异步更新同一内存位置,会发生什么情况;比如当使用线程时?这会导致数据竞争情况。
当两个或多个线程可以访问共享数据并试图同时更改数据时,就会出现竞争情况。因为线程调度算法可以在任何时候在线程之间切换,所以你不知道线程试图访问共享数据的顺序。因此,数据变化的结果取决于线程调度算法,即两个线程都“竞相”访问或改变数据。
— Lehane & Amit Joki 在 SO上的回答
当使用低级语言(如Rust )时,这个问题会更加严重。Rust允许我们访问原始指针,这可能会导致许多不安全的情况。
这是所有权被设置来防止的事情,它通过强制实施以下规则来做到这一点:“在任何给定的时间点,你可以有一个可变引用或者任何数量的不可变引用。”
具有此限制的好处是Rust可以在编译时阻止数据争用。 数据竞争类似于竞争条件,并且在发生以下三种行为时发生:
- 两个或多个指针同时访问相同的数据。
- 至少有一个指针用于写入数据。
- 没有用于同步访问数据的机制。
数据争用会导致未定义的行为,并且在你尝试在运行时跟踪它们时可能难以诊断和修复;Rust可以防止这个问题的发生,因为它甚至不会编译通过有数据争用的代码!
—Rust Book
Rust的所有权规则再次拯救了我们,这被强调为Rust提供的核心安全特性,超过了其他系统语言。这意味着Ruby程序员和我一样,仍然不需要熟悉内存管理的内部工作。
最后一件事是,在传递引用时,还有另一个条件会导致称为悬空引用的bug。
悬空引用是指向已释放的数据的指针,例如:
在这个例子中,foo()
返回一个 string 的引用。但是,一旦 foo()
的作用域结束,string的内存被释放,这意味着引用指向无效的内存空间!
Rust在编译这样的代码的时候会抛出错误:
Rust使用者们可以享受所有权带来的好处而不需要理解它提供的保护。然而,理解所有权解决的问题只会帮助我们写出更好的代码而不需要和编译器斗争。
还有一些关于Rust所有权的问题没有涉及到,但是有了这两篇文章,希望你能有足够的机会开始使用这个优雅的解决方案来解决一个其他棘手的问题。
]]>原文链接: https://medium.com/@thomascountz/ownership-in-rust-part-2-c3e1da89956e
因此,当我阅读Rust Book并看到Rust的一个明确的特性是垃圾收集的替代品时,我有点担心。
处理内存管理的责任是不是要摊派到我的身上?
显然,对于其他系统编程语言,如C语言,处理内存分配是一件大事,如果做得不好,可能会产生重大后果。
随着所有其他新事物的学习,我觉得事情开始堆积起来。
不,它不是一个时髦的服装品牌,栈和堆是在运行时管理内存的方法。
首先,我们有栈。栈被认为是快速的,因为它根据顺序存储和访问数据。最后一个放在(推入)栈上的数据是从栈中取出(弹出)的第一个数据。这被称为LIFO,即后进先出,这意味着我们只需要在释放内存的时候跟踪堆栈顶部的位置。
栈很快的另一个原因是堆栈所需的空间量在编译时是已知的。 这意味着我们可以在将任何内容存储到其中之前分配一个固定大小的内存部分。
例如,如果你知道有4来你家吃饭聚会,你就可以预先安排好他们的座位,准备好食物,并在他们到来之前练习如何叫他们的名字。这样非常高效!
接下来,我们有另一个选择,堆。当你不能预先知道来你家吃饭聚会的确切人数,你可以使用堆。随着越来越多的人来到你的晚餐聚会,使用堆意味着找到额外的椅子并发出姓名标签。
当运行时需要存储未知大小的数据时,计算机搜索堆上的内存,标记它,并返回指针,指针指向内存中的那个位置。这叫做内存分配。然后,你可以将指针压入栈,但是,当你想要检索实际数据时,你需要跟随指针返回到堆。
当我继续深入研究栈和堆,感觉到似乎在堆中管理数据是很困难的。例如,你需要确保在使用完内存后,允许计算机重新分配内存中的位置。但是,如果代码的某个部分释放了内存中的一个位置,而代码的另一部分仍然有指针指向这个位置,那么就会发生一些奇怪的事情。
跟踪代码的哪些部分正在使用堆上的哪些数据,最小化堆上重复数据的数量,清理堆上未使用的数据,以免耗尽空间,这些都是所有权要解决的问题。
-Rust Book
Rust的所有权有三个规则:
这种所有权魔力的最简单说明是变量的作用域:
一旦当前函数范围结束,由 }
表示,变量hello超出作用域,并被删除。
“嗯,呸!”这就是我第一次看到这个时的想法。这在大多数其他编程语言中是一样的。这就是我所知道的“局部作用域变量”的行为。”
如果这就是所有权的全部知识,我真就有点不知所措了。
然而,当我们开始传递值并从使用存储在栈中的字符串字面值切换到使用存储在堆中的字符串类型时,事情变得更加有趣。
我们可以看到,当使用字符串字面量的时候,Rust把hello的值复制到hello1里,这个符合我们的预期。但是当使用String类型的时候,Rust反而是移动值。当我们尝试获取已经被移走的值的时候,Rust会报错:error[E0382]: use of moved value: ‘hello’。
看起来在使用字符串字面量时,Rust会将该一个变量的值复制到另一个变量中,但是当我们使用String类型时,它会移动该值。
要找到哪些类型实现了copy trait
,“你可以查看文档,但是一般来说,任何一组简单标量值都可以被复制,任何需要分配或某种形式的资源都不会被复制”。
字符串字面量”Hello, World!”存储在只读内存的某个地方(既不在栈中也不在堆中),而指向这个字符串字面量的指针存储在栈中。因为它是一个字符串字面量,它通常显示为引用,这意味着我们使用指向永久内存中存储的字符串的指针(关于引用的更多信息,请参见Rust的所有权,第2部分),并且它保证在整个程序的持续时间内有效,(它有一个静态生命周期)。
这里,hello和hello1中存储的指针正在使用栈。当我们使用=运算符时,Rust将hello中存储的指针的新副本压入栈,并将其绑定到hello1。在作用域的末尾,Rust添加了一个drop调用,从栈中弹出值以释放内存。这些指针可以被存储并容易地复制到栈中,因为它们的大小在编译时是已知的。
在堆上,字符串类型值为“Hello,World!”使用String : : from
方法绑定到变量hello。然而,与字符串字面量不同,绑定到hello的数据不仅仅是指针,这些数据的大小会在运行时发生变化。在这里,=运算符将hello中的数据绑定到新变量hello1,有效地将数据从一个变量移动到另一个变量。可怜的hello现在无效,因为根据所有权第二条规则:“一次只能有一个所有者。”
但是为什么要这样做?Rust为什么不一直仅仅拷贝数据并把它绑定到新变量?
如果我们回想一下栈和堆之间的差异,我们会记得堆上存储的数据大小在编译时是未知的,这意味着我们需要在运行时运行一些内存分配步骤。 这可能是很昂贵的操作。 根据我们存储的数据量,如果我们整天都在制作数据副本,我们可能会快速耗尽内存。
除此之外,Rust的默认行为有助于保护我们免受在其他语言中可能遇到的记忆问题。
在堆上存储数据的一部分是在栈上存储指向该数据的指针。然而,与使用指针定位只读存储器不同,比如当使用字符串字面量时,指向堆的指针末端的数据可能会改变。指针是绑定到存储字符串类型的hello变量的数据的一部分。如果我们将同一个指针数据绑定到两个不同的变量,它可能如下所示:
我们有两个变量,hello和hello1,它们共享同一份数据的所有权,这违反所有权第二条规则:“一次只能有一个所有者。”让我们继续往下看。
在hello和hello 1的作用域结束时,我们必须将堆中的内存释放,以便在其他地方再次使用。
首先,我们对绑定到hello1的指针指向的存储数据调用drop,但是当我们在hello上调用drop时,接下来会发生什么?
这称为重复释放错误,我认为对此最好的总结是 Stack Overflow 上的回答:
从技术上讲,在C语言中,重复释放会导致未定义的行为。这意味着程序可以完全任意地运行,并且对所发生的一切都没有把握。发生这种事当然是件坏事!在实践中,重复释放内存块将破坏内存管理器的状态,这可能导致现有内存块被破坏,或者导致未来的分配以奇怪的方式失败(例如,相同的内存被两个连续的不同malloc调用来处理)。
重复释放在各种情况下都可能发生。一个相当常见的情况是,多个不同的对象都有指向彼此的指针,并开始通过调用free来清理。当这种情况发生时,如果你不小心,你可能会在清理对象时多次释放同一个指针。不过,还有很多其他的情况。
— templatetypedef
这就是Rust试图防止发生的!通过使hello无效,编译器知道只在hello1上调用drop(在幕后调用free)。
这一切都很好,但是在某些情况下,我们确实希望复制存储在堆中的数据。Rust为此提供了一种简单的方法,就是clone()
函数。
请记住,对clone()的调用可能很昂贵,这就是Rust默认禁止这种“深度复制”的原因。
显然,关于Rust所有权的问题比这里所提到的要多得多;还有一些概念叫做借用、引用和切片!
迄今为止,似乎了解所有权更多的是为了浏览Rust的内存管理解决方案,而不是了解它解决的问题。但是,Rust Book鼓励你去了解为什么语言的作者渴望创造一种更安全的语言,而不是把它当作语言的怪癖。
]]>原文链接: https://medium.com/@thomascountz/ownership-in-rust-part-1-112036b1126b
在这篇文章中,我将通过一些破坏Rust规则的代码示例来演示这些概念是如何工作的,并解释为什么这些代码存在问题。 我假设读者对Rust编程语言知之甚少。 我还在所有代码块中添加了注释来说明代码是否是有效Rust代码。
本例中,有两个Vector。其中一个是 myvec ,它预先被添加了一些值,而且它被设置为不可变的。另一个被实例化为可变的,并且不包含任何值。 然后我们从 myvec 取出一个值,并且尝试用函数 Vec::push()
将这个值添加到 othervec 中。
|
|
这似乎是一件完全合理的事情——在许多其他编程语言中,你可以从数组或列表中获取一个值,并将其粘贴到另一个数据结构中。
但是在Rust里会怎么样呢?
|
|
可怕的类型不匹配错误!
这里发生的结果是,myvec.get(1)实际上并没有返回类型为String的值,而是返回了对字符串的引用,或叫做 &String。对字符串的引用可以在只读上下文中使用,例如,你可以写如下代码:
|
|
但是,由于 othervec 是一个 Vec<String> 类型的 Vector,你不可以将一个 &String 类型的值添加给它。那么,我们可以使用解引用符号 * ,对不对?
|
|
不对!这次的错误是 “cannot move out of borrowed content”。
编译器在此处告诉我们的是,因为 myvec.get(1) 返回的值是一个 “借用的”值,它不能被赋给别的变量。
为了更详细地阐述这一点,Rust中的值一次只能分配给一个变量。 将值分配给其他变量会导致无法再使用以前的变量访问该值。 举个例子:
|
|
上述代码编译的时候报错:“Use of moved value ‘x’”。这种行为在Rust中非常重要,因为在Rust中,当变量超出作用域时,它们引用的内存被释放 —— 没有垃圾收集。 为了保持这种行为,并防止无效的内存访问,数据一次只能有一个“所有者”。 在此示例中,x是原始所有者,然后y成为所有者。
也就是说,如果数据需要在其他上下文中被读取,例如,在其他函数中,可以将对数据的引用分配给其他变量。例如,以下代码编译成功并可以正常运行:
|
|
表达式 &x 就是“借用”的一个例子。变量y被赋予了对x值的借用引用。 默认情况下引用不可变(在Rust中也存在可变引用),并且对它们有其他约束,我将在后面进一步详细解释。
回到我们的原来的例子,myvec.get(1).unwrap()
返回对myvec中索引1处的值的引用,并且没有发生移动。 但是,在将此值分配给变量时,将发生移动。 该值的当前所有者是myvec。 如果我们写如下代码:
|
|
这会将数据的所有者转给变量val。这是不被允许的,因为你不能移动借用的值。
思考一下能够做到这一点的含义。如果有可能将myvec.get (1)
的所有权转移给val,这意味着一旦val超出作用域,myvec中索引为1的元素现在将会指向无效的内存块。
此外,我们也可以做一些可怕的事情:
|
|
因为 .clear() 释放所有内存,读取 othervec 的值将导致无效内存访问。
要解决这个问题,我们可以将引用分配给变量val,如下所示:
|
|
但是我们不能将这个值添加到Vec<String>类型的Vec,因为val是&String类型的。
那么我们如何解决这个问题来实现我们想要的呢?这将取决于我们到底想要完成什么。我们不能将myvecec .get(1)的所有权转移到othervec,所以如果我们想让othervec指向Vector中实际的位置,我们必须将othervec改为&String的一个Vector:
|
|
然后这样写的结果是,我们无法修改 othervec 里的值,我将在下一节中更详细地讨论这个问题。在othervec超出作用域之前,myvec都不能超出作用域。
另一方面,如果我们只关心位置1的值,那么我们可以复制这个字符串:
|
|
&String上的to_string()方法创建一个新的字符串并将所有权给了othervec。
Rust具有的这种约束,要求不能将值分配给多个所有者,这解决了其他编程语言(如C)中可能继续读取无效内存的大问题。 解决这个问题的意义也非常疯狂:另一种看待这个问题的方式是,当我们用myvec.get(1).unwrap()从myvec中取出一个值时,得到的值知道它来自哪里, 并且知道因为它有一个所有者,所以不能将它分配给另一个变量。 在其他编程语言中,如果从类似列表的对象中提取变量,就像在下面的Python中一样:
|
|
现在存储在y中的值来自哪里是无关紧要的,它可以像任何其他字符串一样被对待。 在Rust中,情况并非如此!
更多关于借用和所有权的内容,请阅读《Rust程序设计语言》那本书的相关章节:
那么如前所述的一个代码示例,我提到我们可以把othervec构建为一个Vec<&String>,也就是一个字符串引用的Vector。在本节中,我将更详细地介绍处理引用。
为了说明这一点,如下是无效代码的另一部分:
|
|
编译器会报告如下错误:
|
|
这个例子和第一个例子很像。这个例子的主要不同是,我们将从myvec第一个位置拷贝值的逻辑抽取到一个叫做 copy_to_new_vec 函数里。
我们不是将myvec和othervec的所有权传递给copy_to_new_vec,而是传递对myvec的引用,以及对othervec的可变引用。 othervec需要作为可变引用传递,因为我们在copy_to_new_vec中为其写入值。 copy_to_new_vec返回对othervec的引用。 而newvec是对othervec的引用。
这看起来很合理,为什么编译器会报错呢?
返回对其他对象的引用的函数的缺点是,如果被引用的对象超出作用域或被释放,那么从这个函数返回的引用中读取数据将导致无效的内存读取。
为了解决这个问题,Rust有一个叫做“生命周期参数”的概念。你可以给Rust编译器一些信息,告诉它,你的引用正在引用的确切对象。 有了这些信息,如果引用的对象超出作用域而引用仍在作用域内,则Rust编译器会报错。
生命周期参数通常由编译器推断出来,但在此示例中,因为返回值有两个参数可引用,所以必须显式提供生命周期参数。 它们看起来类似于Rust中的泛型:
|
|
在尖角括号之间添加带单引号的字符串可以使用生命周期参数对函数进行参数化。 然后可以将它们附加到参数和返回值(如上所示)以指示关系。 在上述代码里添加了生命周期参数,我们向Rust编译器指出copy_to__new_vec的返回值依赖于第二个参数。 但是,如果我们尝试写如下类似的代码:
|
|
编译器将会报如下错误:
|
|
这个错误的含义是,由于copy_to_new_vec的返回值在作用域范围上比copy_to_new_vec中的othervec更长,这是有问题的。
如果允许这样做,则newvec.get(1)将失败,因为newvec引用的值已经被释放。 这是因为在 let newvec = copy_to_new_vec(&vec, &mut Vec::new());
这行代码后,copy_to_new_vec的第二个参数就已经超出作用域了。
在这里查看有关Rust生命周期的更多信息。
希望这些例子能帮助你理解Rust中的借用系统。这肯定要花很多时间去适应,一开始可能会很困难。但是一旦你了解了它,这个借用系统就变成了一个难以置信的安全网,它可以防止在其他系统编程语言中常见的许多问题。
祝玩得开心!
]]>原文链接: http://www.squidarth.com/rc/rust/2018/05/31/rust-borrowing-and-ownership.html
&
是Elixir的捕获操作符,常用来捕获和创建匿名函数。
在深入捕获操作符之前,让我们先熟悉匿名函数和arity。
例子如下:
|
|
我们定义了一个函数,但是它没有被绑定一个全局名字,所以它是一个匿名函数或者是一个lambda表达式。
这个函数有一个参数,所以它的arity是1。
&
我们先来谈谈捕获函数。捕获就意味着&
将一个函数转成匿名函数,这个匿名函数可以被当作参数传递给其他函数,或者被绑定到一个变量。
&
可以捕获两种类型的函数:
使用的方式:&(module_name.function_name/arity)
,例如:
我们从IO
模块捕获puts
函数并且将它绑定到局部变量speak
上。
如下例子中,put_in_columns
和 put_in_one_row
被定义在相同的模块里,因此我们可以用 &put_in_one_row/1
来捕获 put_in_one_row
,注意,我们在此没有包含模块名。
|
|
捕获操作符也可以用来创建匿名函数,例如:
|
|
上述例子和下面的效果一样:
|
|
你可以注意到第一个例子里用了 &1
。它叫做值占位符,它标识了这个函数的第几个参数,此例中就是第一个参数。
另外,{}
和 []
在Elixir里也是操作符,&
也可以和它们一起使用。
|
|
一开始很难理解,我们只需要从另一个角度来思考,如下图:
]]>原文链接: https://dockyard.com/blog/2016/08/05/understand-capture-operator-in-elixir
在今年的EmberConf大会上有很多非常有趣的讨论话题 - 尤其是Tom Dale和Yehuda Katz的开场主题演讲。 他们通过讨论使用WebAssembly在glimmer进行的工作来结束演讲。 事实上,他们甚至展示了在WebAssembly中运行的EmberConf网站版本(尽管由于漏洞而在iOS中崩溃)。
大家都对他们的演讲报以掌声,我也做了我通常做的:微笑着点点头,就像我知道发生了什么。
因此,我决定深入研究它,并试图了解WebAssembly是什么,它真正解决了什么问题。
如果你阅读过关于WebAssembly(又名WASM)的很多博客文章,你会发现很多人提出的最重要的一点是,它使他们能够用JavaScript以外的语言构建网站。 对于拥有多年其他语言经验的人来说,我知道这可能是一个巨大的好处。 但是,作为一名JavaScript开发人员,我仍然有一个问题:它对我有什么用?
为了回答这个问题,我必须提高我对网站如何运行、JavaScript从何开始执行以及WebAssembly如何融入当前生态系统的知识。
大多数人可能知道,JavaScript是在1995年创建的,目的是使web开发人员能够添加一些功能。它是一种松散类型的语言,希望开发人员能够更快地启动和运行程序。这意味着,与C、C++或Rust等语言不同,它的变量可以从整数开始,更改为字符串,然后更改为对象,而不会导致问题。虽然这使学习变得容易,但这意味着语言的效率相当低。
当一个带有JavaScript的网站运行时,这个过程看起来像这样:
这是因为JavaScript使用所谓的“解释器”来运行代码。就像现实生活中的专业翻译一样,它会对每一行代码都进行解释和翻译,最终达到系统所理解的语言——就像有人在实时对话中把英语翻译成西班牙语一样。
这里的问题是,当你必须多次运行相同的代码块,循环或函数时它会变得低效。 解释器每次翻译都没有性能提升。 这导致浏览器开始实现即时(JIT)编译器,使得用户开始在浏览器中看到巨大的性能提升。
JIT所做的是创建函数的编译版本,以便在后续调用中更有效地运行它们。但是,由于我们使用的仍然是松散类型的语言,因此需要创建这些函数的多个编译版本。如下函数:
|
|
如果a和b是数字,那么我们得到的是它们之和。但是,如果它们是字符串,我们得到则是它们的拼接字符串。这种模式在JavaScript里是可行的,但是低层次语言需要知道a和b的确切类型来执行正确的操作。这就是为什么JIT有一个“监视器”,它创建了这个函数的两个编译版本 —- 一个接收数字,一个接收字符串。 调用该函数时,会找到并执行正确的编译版本。 这就是所谓的基线编译器(Baseline Compiler)。
需要注意的是,JIT的具体内容要复杂很多,但我们会保持简单的示例。 如果你想了解更多信息,我建议你阅读Lin Clark写的文章。
因此,现在JIT帮助浏览器更有效地运行,因为它不是解释每一行,而是解释类型并找到函数的正确编译版本。为了进一步提高性能,监视器会观察这些基线编译函数中的哪一个被调用得最多。如果发现一个函数被大量调用,它会将其发送给优化编译器,以创建更快的版本。
例如,如果它发现我们总是使用数字调用putTogether
函数,那么它会假设它应该始终以这种方式调用并创建优化版本。 现在我们的函数接近原生速度。
我们新的过程看起来如下:
虽然我们有更多的步骤,但是在浏览器中引入的JIT在实现的头几年里使站点速度提高了十倍以上。这通常是编译程序与运行时的工作流程。 我们花了一点时间预先编译,但由于我们不再动态翻译,因此执行时间减少,这样会带来重大收益。
上述过程有明显改善,但仍存在一个主要问题。 以我们的putTogether
函数为例。 假设我们执行该函数1000次并仅传递数字。 优化编译器就假设它只用数字调用并编写该函数的低级编译版本。
但是,由于某种原因,在第1,001次我们调用函数时,我们传递给它的是字符串。
当编译的代码看到它做出错误的假设时,它会废弃优化的函数并开始重新优化的过程。 这就是所谓的“拯救”,如果它发生得足够多,最终优化编译器将放弃,我们永远不会得到最有效的函数版本。
WebAssembly允许你(开发人员)编写自己的这些函数的编译版本。这意味着JIT不再创建函数的基线版本、监视它们、优化它们、拯救和重新优化它们。相反,你会说“我知道我想让这个函数以这种方式运行”,而不需要JIT的任何参与。
所以现在我们的流程看起来像这样:
现在,你可以让部分应用程序以与本机应用程序相同的速度运行。 虽然我们不会完全放弃JavaScript,但可以将大量计算转移到这些较低级别的编译语言。 对于大多数Web开发人员来说,这些收益将来自他们的应用程序使用的第三方软件包,他们不必自己编写任何WebAssembly。 但仅仅因为你可能没有直接写它,这并不意味着不能很好地理其解幕后所发生的事情。
]]>原文链接: https://dockyard.com/blog/2018/05/14/intro-to-web-assembly
sbroker这个Erlang库提供了用于创建池和(或)负载调节器的构建模块。 它使用broker模式,其中与服务worker的通信由负责worker与其调用之间协调的broker处理。
让我们看一个如何在Elixir应用程序中使用sbroker库的简单示例。
首先我们在命令行运行如下命令:
|
|
它将在当前目录下创建一个样例工程,名字就叫做 “example” 。在该示例中,我们将模拟在一个worker中对外部服务的调用并由broker处理相关通信。为此,我们编辑example/mix.exs文件,以便将sbroker库添加到我们的应用程序中:
|
|
我们将sbroker库添加到上面的依赖项和应用程序中。我们也在18行指定了我们应用的模块 mod: {Example, []},这个模块我们稍后创建。现在我们准备添加一个broker,所以我们在example/lib/example/broker.ex中创建我们对broker模块:
|
|
上面的模块实现了sbroker行为。在第9行我们启动sbroker,并且在选项里设置超时为10秒。这个超时的意思是,当这些调用在队列里等候worker的时间超过10秒,它们将被丢弃。在init/1函数里,我们给broker定义了客户端和worker的队列。我们定义了broker模块后,我们需要定义worker模块,它负责定义worker,并且向broker请求任务。我们在 example/lib/example/worker.ex里定义worker模块:
|
|
fetchfrom_external_resource/1
函数是一个简单的模拟函数,它将使得进程等待一秒,然后返回 {:ok, “External service called with #{inspect(params)}”} 。当worker这个GenServer进程收到 {tag, {:go, ref, {pid, {:fetch, [params]}},, }} 这样的消息时,这个函数将被调用。这个元组中的tag变量是一个唯一标识符,它被用来标识worker并且被存储在GenServer进程的状态中。
在worker获得所需数据后,它会向broker请求新的任务。我们已经定义了broker和worker模块,因此我们现在可以定义一个监督者,它将启动broker和一个worker池。监督者在 example/lib/example/supervisor.ex 里定义:
|
|
在这个例子中,我们的worker池有5个worker。我们基本完成工作了,还剩下要创建一个application模块,这个模块定义在 example/lib/example.ex 中:
|
|
这个模块启动监督者,并且它有一个函数:fetch_from_external_resource/1,这个函数向broker请求一个worker,当broker能够为我们的请求分配一个worker的时候,会向这个worker发送了消息 {:fetch, [params]} 。当broker不能分配给我们worker的时候,返回的是 {:drop, time} 消息,这样的话,我们的私有函数 perform/1,将返回 {:error, :overload} 。函数 fetch_from_external_resource/1 将打印worker的返回值,或者因为broker丢弃了我们的请求而打印 {:error, :overload} 。
我们现在可以在iex里测试这个例子:
|
|
然后我们可以运行如下语句来从外部资源获取数据:
|
|
一秒后在iex里会输出如下信息:
|
|
为了模拟和测试更多的调用,我们可以通过运行以下语句多次并行地调用 Example.fetch_from_external_resource(“test”) :
|
|
这将打印相同的行并一次打印五行,因为我们的示例worker池包含五个worker。我们也将得到 {:error, :overload} 响应,因为broker不能分配worker并且任务在队列等待太长时间。 {:error, :overload} 响应是用于防止外部服务过载的反压力示例。例如,我们的系统现在可以通过HTTP / 1.1 429 Too Many Requests回复请求服务的客户端,并且它不会因为过载而崩溃。
使用带有worker池和队列的异步进程来扩展系统是一种很好的方式,但是由于我们不希望系统崩溃,我们还应该考虑处理过载的方法。一种方法是将反压机制应用到我们的系统中。在Elixir应用程序中,可以使用sbroker库轻松完成此任务。
]]>原文链接: https://pspdfkit.com/blog/2018/back-pressure-queuing-system-with-sbroker/
你可能以前编写过多个单线程程序。编程中的一个常见模式是具有执行特定任务的多个函数,但是直到程序的前一部分为下一个函数准备好数据时,才会调用这些函数。
这就是我们最初设定的第一个例子,即开采矿石。 本例中的函数执行:寻找矿石、开采矿石和冶炼矿石。 在我们的例子中,矿和矿石被表示为一个字符串数组,每个函数接收并返回一个“已处理的”字符串数组。 对于单线程应用程序,程序设计如下。
有3个主要函数。 finder
,miner
和 smelter
。 在这个版本的程序中,我们的函数在单个线程上运行,一个接一个地运行 -- 而这个单线程(名为Gary的土拨鼠)需要完成所有工作。
|
|
在每个函数的末尾打印得到的“矿石”数组,我们得到以下输出:
|
|
这种编程方式具有易于设计的优点,但是当你希望利用多线程并执行彼此独立的函数时,会发生什么情况呢?这就是并发编程发挥作用的地方。
这种采矿设计效率要高得多。现在多线程( 土拨鼠们 )独立工作;因此,整个过程并不全由Gray这土拨鼠来做。有一个地鼠寻找矿石,有一个土拨鼠在开采矿石,另一个土拨鼠在冶炼矿石——可能所有这些都是同时发生的。
为了将这种功能引入我们的代码,我们需要两件事:一是创建独立工作的土拨鼠,二是土拨鼠相互通信(发送矿石 )的方式。这就是Go的并发原语: goroutine 和 channel。
Go routine可以被认为是轻量的线程。创建一个go routine只需在调用函数的前面加上 go
这个关键字如此简单。
举个简单的例子,让我们创建两个寻找矿石的函数,使用go
关键字调用它们,并让它们在每次发现矿井中的“矿石”时把他们打印出来。
|
|
以下是我们程序的输出结果:
|
|
从上面的输出可以看出,寻找矿石函数同时运行。谁先找到矿石没有真正的顺序,当多次运行时,顺序并不总是一样的。
这是很大的进步!现在我们有一个简单的方法来建立多线程(多个土拨鼠 )程序,但是当我们需要独立的goroutine
来相互通信时会发生什么呢?欢迎来到神奇的channel
世界。
channel
允许goroutine
相互通信。你可以将channel
视为管道,goroutine
可以从管道发送和接收来自其他goroutine
的信息。
|
|
goroutine
可以在一个channel
上发送和接收数据。这是通过使用指向数据方向的箭头( <- )来实现的。
|
|
现在通过使用一个channel
,我们可以让我们的寻找矿石的土拨鼠立即将它们发现的矿石发送给我们的矿石采集土拨鼠,而无需等待发现所有矿石后才将矿石送给矿石采集土拨鼠。
我已经更新了示例代码,以便将寻找矿石函数和采矿函数设置为未命名的函数。 如果你从来没有见过lambda
函数就先不需要太关注程序的那部分,只要知道每个函数都是用go
关键字调用的,所以它们就运行在自己的goroutine
里。 重要的是要注意如何使用叫做oreChan
的channel
来相互传递数据。 别担心,我会在最后解释未命名的函数。
|
|
在下面的输出中,你可以看到我们的矿工通过三次读取叫做“oreChan”的channel
,每一次收到一块“矿石”。
|
|
太好了,现在我们可以在程序中的不同goroutine
( 土拨鼠 )之间发送数据了。在我们开始编写带有channel
的复杂程序之前,让我们先介绍一些理解channel
属性的关键内容。
在各种情况下,channel
会阻塞goroutine
。这就让我们的goroutine
在各自独立快乐的道路上同步了一会儿。
一旦一个goroutine
(土拨鼠)在channel
上发送数据,这个发送数据的goroutine
就会阻塞,直到另一个goroutine
从channel
接收到发送的数据。
类似于在channel
上发送数据之后的阻塞,goroutine
可以阻塞在等待从没有任何数据的channel
上获取数据。
一开始阻塞这个概念可能有点让人不好理解,但你可以把它看作是两个goroutine
( 土拨鼠 )之间的事务。无论一个土拨鼠是在等钱还是在送钱,它都要等到交易中的另一个伙伴出现。
现在我们已经了解了goroutine
在通过channel
进行通信时阻塞的不同方式,让我们讨论两种不同类型的channel
:无缓冲channel
和缓冲channel
。选择你使用的channel
类型可以改变程序的行为方式。
channel
在前面的例子中,我们一直使用无缓冲channel
。使它们独一无二的是,一次只有一条数据适合通过channel
。
channel
在并发程序中,时序并不总是完美的。 在我们的采矿案例中,我们可能会遇到这样一种情况:我们的寻找矿石土拨鼠可以在采矿土拨鼠处理一块矿石的时间内找到三块矿石。 为了不让寻找矿石的土拨鼠将大部分时间花费在发送矿石给采矿土拨鼠并一直等待它接收处理完成,我们可以使用缓冲channel
。 让我们开始做一个容量为3的缓冲channel
。
|
|
缓冲channel
的工作原理与非缓冲channel
相似,只是有一点需要注意:我们可以在需要另外的goroutine
读取channel
之前将多条数据发送到channel
。
|
|
我们两个goroutine
之间的打印顺序是:
|
|
为了简单起见,我们不会在最终示例程序中使用缓冲channel
,但了解并发工具中可用的channel
类型很重要。
注意:使用缓冲
channel
不会阻止发生阻塞。 例如,如果寻矿土拨鼠比采矿土拨鼠快10倍,并且他们通过大小为2的缓冲channel
进行通信,则寻矿土拨鼠仍将在程序中多次被阻塞。
现在通过goroutine
和channel
的强大功能,我们可以编写一个程序,使用Go的并发原语充分利用多个线程。
|
|
上述代码的输出如下:
|
|
这比我们原来的例子有了很大的改进!现在,我们的每个函数都是独立运行的。而且,每次有一块矿石被加工,它就进入我们采矿线的下一个阶段。
为了将注意力集中在理解channel
和goroutine
上,我上面没有提到一些重要的信息——如果你不知道这些信息,在你开始编程时可能会引起一些麻烦。现在你已经了解了goroutine
和channel
的工作方式,让我们先看一看你应该知道的一些信息,然后再开始使用goroutine
和channel
进行编程。
goroutine
与我们如何用关键字 go
设置一个函数运行在它自己的goroutine
里相似,我们可以用如下格式来创建一个匿名函数运行在它自己的goroutine
里:
|
|
如此一来,如果我们只需要调用一次函数,我们可以将它放在它自己的goroutine
中运行,而不用创建正式的函数声明。
goroutine
主函数确实在其自己的goroutine
中运行! 更重要的是要知道,一旦主函数返回,它将关闭当前正在运行的其他所有goroutine
。 这就是为什么我们在主函数底部有一个定时器 -- 它创建了一个channel
,并在5秒后发送了一个值。
|
|
还记得一个goroutine
会如何在一个channel
读取数据的时候一直阻塞到有数据发送给了这个channel
吗? 通过添加上面的代码,主函数会阻塞,给我们其他的goroutine
5秒额外的时间来运行。
现在有更好的方法来处理阻塞主函数,直到所有其他的goroutine
完成。 通常的做法是创建 done
channel
,主函数在它上面读取数据从而被阻塞。 一旦你完成你的工作,写入这个channel
,程序将结束。
|
|
channel
上使用range
在前面的一个例子中,我们让矿工从for循环中经过3次迭代从channel
中读取数据。如果我们不知道到底有多少矿石会从发现者那里送过来,会发生什么?好吧,类似于在集合上使用range
,你可以在一个channel
使用range
。
更新我们以前的矿工函数,我们可以写成这样:
|
|
因为矿工需要读取寻矿者发送给他的所有内容,所以通过在这里的channel
使用range
可以确保我们收到发送的所有内容。
注意:在一个
channel
上使用range
将被阻塞,直到在channel
上发送了另一个数据。在所有需要发送的数据发送完后,停止goroutine
被阻塞的唯一方法是用“close(channel)”关闭channel
channel
上进行非阻塞读取但你不是刚刚告诉我们channel
如何阻塞goroutine
的吗?! 确实如此,但有一种技术可以使用Go的select case结构在channel
上进行非阻塞式读取。 通过使用下面的结构,如果有某些事情发生,你的goroutine
将从channel
中读取,或运行默认情况。
|
|
运行后,上述例子的输出如下:
|
|
channel
上进行非阻塞发送非阻塞发送使用相同的select case结构来执行它们的非阻塞操作,唯一的区别是我们的情况看起来像发送而不是接收。
|
|
有很多讲座和博客文章涵盖channel
和goroutine
的更详细内容。 既然你对这些工具的目的和应用有了深刻的理解,那么你应该能够充分利用以下文章和讲座。
Google I/O 2012 — Go Concurrency Patterns
Rob Pike — ‘Concurrency Is Not Parallelism’
GopherCon 2017: Edward Muller — Go Anti-Patterns
谢谢你抽出时间来阅读这篇文章。我希望你能够了解goroutine
、channel
以及它们为编写并发程序带来的好处。
]]>原文链接: https://medium.com/@trevor4e/learning-gos-concurrency-through-illustrations-8c4aff603b3
我们来执行一个小实践任务,它将演示这种铁路方式的函数式编程方法。
考虑一下我们使用Erlang构建POP3电子邮件客户端的情况。 我们的目标是实现与POP服务器建立连接的控制流程。
下图说明完成此操作所需的步骤:
首先,让我们写一个函数来实现建立连接的功能:
|
|
上述代码非常漂亮,仅仅四行就完成了我们想要的功能。但是等等……上面的实现是非常完美的场景。 显然我们需要添加一些错误处理来应对边界条件的场景 :( 。我的意思是,“可能会出错的地方”?
让我们总结如下图中所有可能的边界情况:
我们增加错误处理的代码,然后看看代码变成什么样子!
剧透:下面的例子很简单,可以通过将操作分成单独的函数来美化,但是嵌套的case语句是不可避免的。
|
|
现在我们添加了所有的错误处理代码。但是,代码的大小增加了400 %……可读性也相应降低了!
也许有一个更清晰的方式来实现这一点?
铁路方法背后的想法是用铁路道岔作为模拟物来分解“每一步”功能块:
这种方法可以翻译为如下的Erlang代码:
|
|
一旦为所有需要的操作创建了两种方式( ok / error )切换分支,就可以像在铁路上一样优雅地组合它们:
所以,简单来说,确切的情况是:
在成功的情况下,所有功能(“铁路道岔”)都按顺序执行,我们沿着“成功轨道”行进。否则,我们的列车将切换到“错误轨道”,并沿该路线行驶,绕过所有其他步骤:
我们发布了一个很小的Erlang库,它简化了Erlang的“铁路道岔”分解方式。那么,考虑上面的例子,让我们来看一下如何使用Epipe实现我们的用例:
|
|
与嵌套的case语句实现相比,最终的代码在代码行方面不会更小,但它确实更具可读性,使得调试和支持变得更加容易。
如果你希望看到真实的实现案例,请查看使用铁路方法执行的重构例子。
]]>原文链接: https://www.erlang-solutions.com/blog/railway-oriented-development-with-erlang.html
Spring Boot带有智能构建功能,可以轻松创建Web或独立应用程序。Spring Boot可以为我们做很多事情,甚至不需要我们为Web应用程序编写一行代码。本文中,我们只介绍其中几个配置。
web应用最常见的一个配置是HTTP端口号,我们可以用下列几种方式轻松地为我们的web应用配置HTTP端口号:
对于properties文件:
|
|
对于YAML文件:
|
|
我们也可以在Spring Boot中编程设置HTTP端口:
|
|
Spring Boot Web应用程序的默认上下文路径是“/”,Spring Boot提供了通过配置或以编程方式设置上下文路径的选项。
对于properties文件:
|
|
对于YAML文件:
|
|
我们在Spring Boot中也可以通过编程来设置Context路径:
|
|
如果你正在用Spring Boot应用程序,那么你应该熟悉 While Label Error Page。 如果我们没有指定自己的自定义bean,Spring Boot会自动注册BasciErrorController bean。 我们可以通过扩展ErrorController来定制这个bean。
|
|
Spring Boot提供了一种基于错误代码使用我们自己的自定义错误页面的方法。 我们需要在/error目录下添加基于错误代码的页面,并且Spring Boot将根据错误代码使用正确的页面。
我们可以使用静态HTML,也可以使用模板来构建我们的自定义错误页面。 文件的名称应该是确切的状态码或系列通配符。
我们可以使用类似的结构来组织我们的模板。
|
|
|
|
Spring Boot对日志记录没有必要的依赖(通用日志API除外)。 Spring Boot内部使用LoggingSystem,试图根据类路径的内容配置日志。
我们可以在 application.properties 文件里用 logging.level 这个前缀来设置日志级别从而可以微调Spring Boot应用的日志输出。
|
|
我们可以在Spring Boot应用程序中使用不同的日志框架(Logback,Log4j2)。
在这篇文章中,我们介绍了Spring Boot Web应用程序配置,这是为正确设置Web应用程序或按照你的需要设置所必需的。 有关更多详细信息,你可以随时参阅Spring Boot文档。
]]>原文链接: https://www.javadevjournal.com/spring-boot/spring-boot-web-application-configuration/
启动一个新项目的主要挑战之一是该项目的初始设置。 我们需要对不同的目录结构进行调用,并且需要确保我们遵循所有行业标准。对于使用Spring Boot创建Web应用程序,我们需要以下工具:
有多种方式可以使用Spring Boot Initializr为你生成项目结构:
为了简化这篇文章,我们使用Spring Initializer的网页界面来生成项目结构。
用你的浏览器访问Spring Initializr Web界面,你将看到一个向导来开始你的配置。
你需要填写网页界面中的一些信息才能开始。
依赖是Web界面中的一个有趣功能,根据你选择的依赖,Web界面会自动在生成的pom.xml文件中添加Spring Boot Starter依赖。如果你希望对生成的项目结构进行更多控制,或者不确定所有你想要添加到项目中的依赖,请单击“Switch to the full version”按钮。
在本文中,我们将使用Web和Thymeleaf(用于用户界面)两个Starter。
Spring Boot不需要任何特定的代码布局或结构。我们始终可以遵循Spring Boot团队提出的一些最佳实践,但最终结构将由你的项目需求驱动。
下图是我们例子应用的项目结构:
我们来看看pom.xml文件,详细地了解Spring Boot配置。 我将仅涵盖pom.xml中与Spring Boot相关的更改。 以下是我们示例项目中的pom.xml文件。
|
|
Spring Boot的主要特性之一是“Starter”,它们是在我们的类路径中添加所需依赖项(jar包)的简单方法。 当使用Spring Boot时,我们不必在我们的类路径中添加jar包或依赖项(如果starter不可用,你可能必须将这些依赖项添加到pom.xml中,或者可以创建自己的自定义starter)。 我们只需要在我们的pom.xml文件中添加正确的“Starter”,Spring Boot将确保自动添加这些依赖。
如下所示是我们的Spring Boot应用程序主类,它也是一个Spring配置类。 注解@SpringBootApplication启用Spring上下文以及Spring Boot的所有启动魔法。
|
|
@SpringBootApplication相当于使用@Configuration,@EnableAutoConfiguration和@ComponentScan以及它们的默认值。如果要开始项目,建议使用这个注解。
在主类中使用@SpringBootApplication相当于同时使用以下3个注解:
我们主类的另一个有趣特点是主方法。 这是遵循标准Java工作流程的标准方法。 我们的主类将把控制权交给Spring Boot SpringApplication类。
SpringApplication类的run方法将用于引导一个应用程序。
我们设置的最后一部分,我们将创建一个welcome controller,负责通过返回View的名称(在本例中为“welcome”)处理/greeting的GET请求。 视图负责呈现HTML内容。
|
|
这是一个非常简单的控制器,但在我们的设置中涵盖了很多要点。
如下我们简单的Thymeleaf HTML模板。
|
|
当使用Thymeleaf作为我们的模板引擎时,Spring Boot将通过在视图名称前后加上前缀和后缀(配置参数分别是:spring.thymeleaf.prefix和spring.thymeleaf.suffix,它们的默认的值是:’classpath:/templates/‘和’html’)。
我们完成了我们简单的Web应用程序,现在是时候运行我们的应用程序了。 尽管可以将此服务作为传统WAR文件打包以部署到外部应用程序服务器上,但更简单的方法是创建独立应用程序。 要从IDE运行我们的应用程序,我们需要将我们的Web应用程序作为独立的Java应用程序运行。
现在,该网站已启动并正在运行,请访问 http://localhost:8080/welcome ,如果一切正常,则应在Web浏览器中输出以下内容。
|
|
在这篇文章中,我们学习了使用Spring Boot创建Web应用程序。 Spring Boot具有许多功能,可以更快,更轻松地创建和运行Web应用程序。
]]>原文链接: https://www.javadevjournal.com/spring/creating-a-web-application-with-spring-boot/
出于科学的考虑,Erlang的私有函数是私有的!这与Python中的情况不同,在Python中,你在各处添加了几个下划线,突然之间,两秒钟前是私有的,现在已经公开了。在Erlang,我们的私有函数在保险箱里安全地呆了25年!没有人可以调用我们声明为私有的函数!或者……有办法吗?
事实证明,在Erlang中调用私有函数的方式并不那么简单,即使假设代码已经在启用debug_info的情况下编译。 让我们看看事情是如何运作的。
我们首先定义一个名为test的模块。 该模块包含两个函数,其中只有一个被导出:
|
|
让我们编译模块,并确保包含调试信息。 注意在Erlang中如何包含调试信息是相当常见的,因为包括调试器,交叉引用工具和覆盖分析工具在内的许多工具都需要这些信息才能工作:
|
|
编译器通知我们该私有函数不可访问,这是预期到会有的。 现在让我们打开一个shell并尝试调用这两个函数:
|
|
我们可以调用公共函数,但不能调用私有函数。没什么新鲜的。现在,让我们定义一个小的匿名函数(我马上将解释它的作用),并将其绑定到变量Open:
|
|
然后让我们调用Open函数,将test模块作为参数传递给它:
|
|
现在让我们尝试访问私有函数:
|
|
保险箱现在大开着。
那么,Open函数背后有什么样的黑魔法? 实际上并不多。 让我们再看一遍:
|
|
在向code server请求test模块的绝对文件名路径之后,我们使用强大的beam_lib接口从test模块的beam文件中包含的调试信息中提取抽象语法形式(还记得我们使用debug_info选项编译它吗?),然后我们从这些语法形式开始重新编译模块,并添加臭名昭著的export_all选项,这将导致导出模块中定义的所有函数。我们重新加载了模块的新版本。
为了简单起见,在上面的例子中有一些情况是Open函数没有考虑的,但你应该明白它的要点。
快乐黑客!
]]>原文链接: https://medium.com/about-erlang/how-private-are-erlang-private-functions-36382c6abfa4
某些设计决策会产生灵活的系统。例如,Erlang的异步消息发送和带有超时的同步消息接收为开发人员提供了全面的消息传递行为。如果要同步消息的发送,最好带上一个唯一(reference)引用来发送消息,然后执行receive语句,对这个引用进行接收模式匹配。这基本上就是使用gen_*:call这一类函数调用时内部的实际情况。如果要异步接收消息,则可以使用带有 after 0 的receive语句,在没有匹配的消息的情况下,立即超时。下面是示例代码:
|
|
问题:如果Erlang给提供你同步发送和异步接收,你将如何去实现异步发送和同步接收?
Erlang消息传递的核心设计足够灵活,可以表达默认情况下未实现的消息传递语义。 当我们将注意力转向gen_event时,请记住这一点。
对于那些不熟悉gen_event的人来说,它的工作方式或多或少可能像这样:gen_event:start_link启动所谓的事件管理器进程。 当你编写一个实现gen_event行为的模块时,你正在编写所谓的事件处理程序。 事件管理器有一个已安装事件处理程序的列表,每个事件处理程序都有自己的状态。 当进程调用gen_event:notify时,事件管理器将一次调用一个其安装的处理程序(毕竟,事件管理器只是一个进程)。 我会重复重点:事件管理器进程一次执行一个所安装的事件处理程序。 这个设计引发了一些争议,特别是José Valim,他有一篇关于如何用监督者(作为事件管理器)和一堆gen_server(作为事件处理程序)替换gen_event的博客文章。
我知道我有点怪,但是我认为gen_event的默认行为非常好,并且最终比José建议的并发处理程序的解决方案灵活得多。如果希望并发处理事件,则可以通过安装事件处理程序(将消息转发到现有进程或派生进程以执行事件处理代码)来轻松实现事件的扇出。但是,如果事件的并发处理是默认行为,你将如何实现事件的顺序处理呢?我怀疑你最终会实现一个效率较低的gen_event版本;因为你将发送不必要的消息,这导致效率较低。
问题:如果gen_event在默认情况下并发运行事件处理程序,那么你将如何依次运行处理程序呢?
总之,gen_event很像Erlang的消息传递,它提供了一个灵活的基础,你可以轻松地实现不同的行为。 我很高兴gen_event的存在,并且它被设计为顺序处理事件是默认行为。
]]>原文链接: http://www.rkallos.com/blog/2018/05/22/in-defense-of-gen-event/
在启动任何项目(无论是小型项目还是企业级应用程序)之前,其中关键的方面之一是依赖管理,手动为小型应用程序执行依赖管理并不是一项困难的工作,但对于复杂的应用程序,手动管理所有项目依赖并不理想,容易出现许多问题以及浪费时间,而这些时间可以用于项目的其他一些重要方面。
Spring Boot背后的基本原理之一就是解决类似的问题。Spring Boot Starter是一套方便的依赖描述符,可以很容易地包含在任何级别的应用程序中。这些Starters作为Spring相关技术的引导过程,我们 不再需要担心依赖关系,它们将由Spring Boot Starters自动管理。
Starters包含了许多你需要的依赖项,以使项目快速启动和运行,并且具有一致的、被支持的一组管理传递依赖项。
当我们用Spring Boot开始开发应用时,我们想到的一个基本问题就是为什么我们需要Spring Boot Starters? 或者这些Starters在我的应用中如何帮助到我?
如前所述,这些Starters用于引导应用程序,我们需要的只是在应用程序中包括正确的Starters,而Spring Boot将确保所选Starters所需的所有依赖项都在你的classpath中。
为了更清楚地理解它,我们举一个例子,我们想构建一个简单的Spring Web MVC应用程序,我们需要在开始编写我们的Web应用程序代码之前考虑以下几点。
使用Spring Boot Starters来引导我们的Spring MVC Web应用程序非常简单,我们需要在我们的pom.xml中包含spring-boot-starter-web 这个starter:
|
|
以上pom.xml中的条目将确保所有必需的依赖项都应位于classpath中,因此我们都准备好开始开发web应用程序了。
目前,Spring Boot提供的Starters约有50多个,这还不包括第三方的Starters。有关Starters的更新列表,请参阅Spring Boot Starter
接下来,我将介绍一些常用的Starters。
这是最常用的Spring Boot Starter之一,该Starter将确保创建Spring Web应用程序(包括REST)所需的所有依赖包括在你的calsspath中,它还将添加tomcat-starter作为默认服务器来运行我们的Web应用程序。 要在我们的应用程序中包含Web Starter,请在pom.xml中添加以下条目。
|
|
现在我们可以创建我们的Spring MVC Controller
|
|
如果你运行应用程序并访问http://localhost:8080/greeting,你应该能够获得"Hello Word”作为响应。我们使用最少的代码创建了一个REST控制器。
大多数应用程序需要一些持久性机制,而JPA是持久性的标准,Spring Boot Starters带有JPA Starters,你不再需要手动配置这些JPA依赖,而是可以通过在应用程序中添加JPA Starter轻松实现。
|
|
Spring JPA Starter提供对H2,Derby和Hsqldb的自动支持。让我们看看使用JPA starter创建一个JPA样例应用程序是多么容易。
|
|
如下是我们的UserRepository:
|
|
接下来我们可以测试我们的代码了,如下是JUnit代码:
|
|
正如我们在上面的代码中看到的那样,你不再需要指定那些数据库配置或额外的数据库配置,通过添加JPA starter,我们无需配置或编码即可获得许多开箱即用的功能。
如果需要,你始终可以修改或自定义这些配置。
从应用程序发送电子邮件是非常常见的任务,现在每个应用程序都需要从系统发送电子邮件。Spring Boot Mail starter提供了一种隐藏所有复杂性的简单方法来处理此功能。
我们可以通过在应用程序中添加Mail starter来启用电子邮件支持。
|
|
我正在使用Mailgun作为我的SMTP服务器,以下是添加到我的application. properties文件中的SMTP详细信息:
|
|
我们的EmailService类负责发送邮件:
|
|
我们使用Spring提供的JavaMailSender来发送电子邮件。 JUnit测试代码如下:
|
|
同样,只需简单的代码和配置即可发送一封简单的电子邮件,Spring Boot Mail Starter确保所有必需的工具已经到位,以快速开始解决实际问题。
请注意,我们在JavaEmailService bean中使用JavaMailSender - 该bean是由Spring Boot自动创建的。
我们通常使用Junit、Mockito或Spring Test来测试我们的应用程序。我们可以通过添加Spring Boot Test starter轻松地将所有这些库包含在我们的应用程序中。
|
|
Spring Boot会自动找到我们正确的版本用于我们的应用程序测试。 这是一个JUnit示例测试:
|
|
除了这些starter之外,下面还有其他常用的Spring Boot Starter
如前所述,请参阅Spring Boot Starter获取Spring Boot提供的Starter的最新列表。
本文提供了一个Spring Boot Starters简介,我们讨论了为什么我们需要这些Starter以及他们如何帮助我们快速引导我们的应用程序。 我们探索了一些最常用的Spring Boot Starter。
建议阅读:
使用Spring Boot构建应用程序
]]>原文链接: https://www.javadevjournal.com/spring/spring-boot-starters/
Spring Boot有自己一套的约定,而且约定优于配置。Spring Boot通过用Spring平台自有的约定来消除大部分项目设置,这样新用户和现有用户就可以快速到达他们需要的开发节点。Spring Boot使创建一个以Spring为动力的企业应用程序变得非常容易,而且操作简单。
Spring Boot提供以下开箱即用的功能:
启动一个项目的最主要挑战之一是初始化该项目的配置。我们需要对不同的目录结构进行调用,并且需要确保我们遵循所有行业标准。如果你使用的是Maven,那么你可能已经在使用Maven启动工件,它可以帮助我们更快地完成初始设置。
Spring Initializr是另外一个非常棒的快速启动一个Spring Boot项目的工具。Spring Initializr是一个生成Spring Boo项目的web应用。请记住,它只会生成项目结构,而不会根据你的偏好为你生成任何代码( Maven或Gradle )。如果你正在启动你的项目,我的推荐是使用Spring Initializr。
有几种方式来使用Spring Boot Initializr为你生成项目结构:
这是为你的应用生成项目结构的最简单方式。在你的浏览器打开Spring Initializr Web界面,你将看到一个向导来开始你的配置。
你需要在这个web界面填写一些信息:
在web界面里选择依赖是一个有趣的功能。基于你选择的依赖,web界面将自动在生成的pom.xml文件中增加 Spring Boot Starter 依赖。如果你想要更多地控制生成的项目结构,或者不确定要添加到项目中的依赖,请单击“切换到完整版”。
使用完整版本界面,你可以选择Java版本,打包模式(比如用于传统部署的war包)以及为项目选择依赖的选项。一旦你点击“生成项目”按钮,Spring Initializr将生成项目,你将下载得到一个zip文件。 你可以在IDE中将解压缩的项目作为基于Maven 或 Gradle的项目导入。
我将不会详细介绍如何在IDE中导入项目。 有关更多详细信息,请参阅相关的IDE文档。
我们也可以使用Srping Boot CLI来生成项目的结构。只要你安装了CLI,就可以打开终端输入spring。如果你正确安装了CLI,输入spring,回车后将看到如下类似的输出:
|
|
我们可以在spring命令后使用init作为额外的参数来创建一个新的项目。Spring Boot CLI将在其内部使用start.spring.io来为你创建项目结构。如下例子:
|
|
上述命令将创建一个基于Maven的使用spring-boot-starter-web的项目,目录名为springboot-demo-project。这和使用start.spring.io的web界面创建的项目是一样的效果。我们可以传递不同的参数来自定义项目的生成。
比如,我们想创建基于Java 1.7的项目,我们可以传递 --java-version=1.7 作为额外参数给Spring Boot CLI:
|
|
当执行上面的命令后,在项目的pom.xml文件里自动增加了Java 1.7相关的信息:
|
|
如果你不确认Spring init的功能有什么,可以带上标志 --list :
|
|
我们来看看pom.xml文件的内容,来更详细地了解Spring Boot的配置。我将只关注pox.xml里与Spring Boot有关的变化。如下是我们创建样例项目的pom.xml文件:
|
|
Spring Boot最主要的特色就是“Starters”,它们是增加依赖包到classpath的便捷方式。使用Spring Boot的时候,我们不需要增加jar包或依赖到classpath(如果一个starter不可用了,你可以自己增加所需依赖到classpath,或者创建你自己的starter)。我们仅需要增加正确的“Starters”到我们的pom.xml里,Spring Boot会确保自动增加那些依赖。
|
|
我们的主类用了@SpringBootApplication注解。@SpringBootApplication等同于同时使用@Configuration、@EnableAutoConfiguration和@ComponentScan。如果你启动你的项目,建议使用这个注解。使用@SpringBootApplication等同于同时使用如下三个注解:
@Configuration
作为bean定义的来源。@EnableAutoConfiguration
它使得Spring Boot自动配置应用程序。@ComponentScan
它会自动扫描所有的Spring组件,包括使用了@Configuration
注解的类。我们主类的另一个有趣的地方是主方法。它是遵循标准Java工作流程的标准方法。 我们的主类将把控制权交给Spring Boot的 SpringApplication类。 SpringApplication类的run方法将用于引导应用程序。 我们将在后面更深入地观察SpringApplication。
|
|
控制器非常简单。它是使用Spring MVC标准注解的Spring MVC控制器。
是时候运行我们第一个Spring Boot程序了。我们有几种方式来运行我们的Spring Boot程序。
mvn spring-boot:run
命令来启动我们的第一个Spring Boot程序。
|
|
打开浏览器,输入 http://localhost:8080 ,回车,我们将看到 Hello World 。
Spring Boot为基于Spring的应用程序提供了很好的推动力。在这篇文章中,我们学习了使用Spring Boot构建应用程序的不同选项。设置新项目始终是一项具有挑战性的任务,我们需要确保管理所有依赖,但是使用Spring Boot,这些都变得非常容易,我们能够只用几行代码就能运行第一个web应用程序,而无需过多考虑所需的依赖或程序的部署。
]]>原文链接: https://www.javadevjournal.com/spring-boot/introduction-to-spring-boot/
作为一名Java开发人员,我们很可能已经直接或间接地在工作中使用基于Spring Framework的应用程序。Spring有许多方法来配置它的行为,它提供三种方式进行配置,基于XML的配置或基于注解的配置,以及基于Java的配置,而基于Java的配置正在成为新的Spring应用程序的事实标准。尽管这些选项看起来非常好,但大型企业应用程序涉及数百个模块和复杂的业务规则,但这些配置可能会变得非常复杂。 以下是大型应用程序可以带来的一些复杂情况。
以上所有问题或多或少都与确保我们拥有一切,然后开发团队才能开始处理实际任务有关。 现在让我们来谈谈另一个情况,我们用它来处理任何基于Spring的应用程序。假设我们要创建一个基于Web的应用程序,下面是我们大多数人常用的常用步骤:
上面的列表可以根据我们的应用程序的类型显着增长。
以上所有步骤对我们来说都很重要,但是它们给开发团队增加了很多开销,而不是专注于解决实际的业务问题,最初的时间将被消耗在确保一切都就位后才开始实际的工作。我们可以将Spring Boot视为可以自动完成这些初始任务的工具。Spring Boot对我们所使用的Spring平台有自己的视角,并确保团队可以快速开始解决实际业务问题,而不是花时间在初始配置和设置上。
Spring Boot提供以下开箱即用的功能:
使用Spring Boot,可以轻松管理和处理介绍部分突出显示的问题。在升级过程中,我们不需要手动搜索兼容的jar包,Spring Bug将确保我们的应用程序升级到正确的版本(这称为麻烦最小化开发应用程序)。
让我们来看看我们的Web应用程序的一个示例pom.xml,以了解Spring Boot例子程序的配置。
|
|
请看<packaging>标签,Spring Boot可以灵活地将我们的应用程序作为jar包来运行,而不是强迫我们将war作为所需的打包类型。
只要仔细检查配置,你不会找到所有这些Spring依赖项的条目(如web MVC,核心,AOP,ORM,验证API等等),你可能已经注意到类似的条目spring-boot-starter- *,这是Spring Boot依赖管理过程。 我们在我们的pom.xml中添加了spring-boot-starter-web,Spring Boot将拉取所有为Spring MVC应用程序必需的依赖(不再需要手动配置)。
自动配置是Spring Boot的另一个有趣功能,这就是为什么Spring Boot团队认为它有意思的地方。以下是Spring Boot为你做的一些工作:
请阅读这篇文章以更加深入地理解Spring boot的自动配置
每当我们做出那些小的改变,并且需要将你的应用程序部署到应用服务器来测试我们的改变时,你是否记得在Servlet容器(Tomcat等)上部署它们的过程?Spring Boot提供对嵌入式Servlet容器的支持,我们不再需要在应用服务器上部署我们的应用程序(这可以使用标准main方法轻松运行),同时我们可以使用http://\
我们的pom.xml中的Spring-boot-starter-web配置条目将为我们的Web应用程序提供嵌入式servlet容器,Apache Tomcat是Spring Boot提供的默认servlet容器,然而,Spring boot也提供了使用其他servlet容器的方法(我们所要做的就是将相应的 starter 添加到pom.xml中)。
请阅读我们的文章使用Spring Boot构建应用程序来开始使用Spring Boot构建你的应用程序。
在这篇文章中,我们了解了Spring Boot,我们介绍了什么是Spring Boot? Spring Boot有什么好处? 我们讨论了Spring Boot的不同特性。 Spring Boot内部为我们做了很多事情,这对我们来说似乎很神奇。 在本系列文章中,我们将揭开Spring Boot的所有内部细节。
]]>原文链接: https://www.javadevjournal.com/spring-boot/what-is-spring-boot/
Rust是一种年轻的编程语言,为程序代码带来了新的质量。 你可能听说过它是快速的、安全的或容易实现并发的。 本文集中介绍Rust最重要的核心特性:内存管理。 该核心特性是Rust的主要创新之一,它的许多独特的特点是基于这种核心特性的。
本文是写给不知道Rust或刚刚开始学习它的程序员的。对于熟悉C、C++或其他使用手动管理内存以及使用垃圾回收器语言的读者来说会更容易理解Rust的特点。 本文是一个旨在介绍Rust核心概念并鼓励进一步学习的高层次介绍。本文不是教程,最后也没有Hello Wrold的Rust例子。
现代应用程序使用计算机的内存主要有两种方式:栈和堆。这可能不适用于使用汇编或编写嵌入式系统软件的情况,但让我们还是关注一般的应用程序的场景。
随着程序进入和退出某些区域(通常是函数),以及循环和分支代码块,栈会自动扩展和缩小。所有现代的、高于汇编语言的语言都会自动执行此操作。它们的行为都是相似的,程序员声明变量,使用它,然后丢弃它。 编译器基于代码区域边界知道何时必须保留内存以及何时清除内存。 这是一个严格的流程,但它快速、安全且易于使用。
对堆的处理更自由。 程序员可以从代码中的任何一点来请求它的一部分,然后在任何其他点释放它。 它并不明显与程序流程结合,编译器无法确定何时以及如何处理它。程序员有责任对其进行正确处理。
内存首先必须被获取到,然后被使用,最后被仅释放一次。这三个步骤似乎很简单,但将其与其他应用程序的流程混合会变得棘手,并且违反其中一个步骤都是灾难性的。 有时候一个错误可能没有任何后果,但是在其他时候,应用程序可能会被终止,甚至更糟糕的是,它的内存可能会悄无声息地被破坏。 这种行为不是确定性的。
|
|
当内存没有被正确释放当时候,泄露就发生了。内存泄漏成为一个致命的负担,使得应用程序比实际所需使用更多的资源。在极端情况下,如果所有的内存都被占用,并且仍然有更多的需求,它会使程序甚至整个系统崩溃。
|
|
当内存被释放后程序还尝试去使用这块内存,这就是释放后使用。如果内存被还给了操作系统,而我们又尝试去访问它,这会导致致命的段错误,程序会立即被结束。另一个有趣的部分是当被释放的内存被分配器缓存并在下次获取时被重用,这使两个随机部分的代码使用相同位置的内存。
|
|
内存被释放两次就是重复释放。如果内存被还回操作系统,它就终止程序对它对访问。重复释放的后果很大程度上取决于分配器,释放内存在其他地方使用或只是崩溃。
|
|
堆管理是个非常古老的问题,程序员发明了许多工具来减轻它。有两种主要的方法,都被证明是有用的,但每一种都有严重缺陷。
这是一个简单的方法。程序获得特殊的机制检测到从某时刻开始给定的内存块将永远不会被使用,因此它可以安全释放。该方法防止了内存泄露、释放后使用、重复释放。证明内存永远不会被再次使用的最简单的方法是证明它是不可访问的。当程序将内存的地址存储在栈上、静态变量或堆上时,该内存是可到访问的,堆本身是可到达的,因此可以在不猜测的情况下获得。而内存本身是可访问的,因此可以毫无疑问地获得它。
|
|
有许多智能的策略来检查可访问性,但它们都会产生显著的开销。例如,引用计数器会增加内存使用量并为每个堆访问增加开销。另一方面,追踪垃圾回收器允许自由访问,但引入了大量的内存可访问性分析,这些分析可以在后台不断运行,或者为了清理内存可以完全停止程序的执行。 无论如何,垃圾回收器都会为应用程序增加额外的工作量并增加内存使用量。
因此垃圾回收器是一个很好但消耗大量资源的解决方案。但是,如果成本难以承受或者根本没有可能使用它,我们可以做些什么呢? 程序员发明了一个特殊的规则,它使内存管理更容易。 它是基于所有权和生命周期的规则。
所有权是这样的一个想法,可以有很多指向分配内存的指针,但只有其中一个被视为拥有该内存。当拥有所有权的指针被销毁时,应该使用它来释放分配给它的内存。非所有权的指针可以被创建和销毁任意个,但它们永远不应该用于释放内存。这使得内存管理更加清晰,因为只有一个重要指针要跟踪和释放。它还解决了前面提到的三个堆问题中的两个问题:泄漏和重复释放。所有权可能是API和程序流程中的一个软性协议,但某些语言和库提供的工具使得此策略的执行更加明确且不易出错。例如,现代C++提供了内置的智能指针,它明确表示有拥有权的指针并实现像销毁时释放的合适行为。
|
|
生命周期是程序执行过程中的一段时间,而这段时间内一段特定的数据被有效使用。处理堆分配的内存指针时,这是非常重要的属性,这些指针并不拥有内存。 只要拥有内存的指针不释放内存,它们就可以安全使用。而有所有权的指针释放内存之后,再使用它们就是错误,因为它们的生命周期结束了。值得注意的是,任何包含给定生命期周期的指针的结构都应该被认为具有不超过指针的生命期周期。这不是一个可以执行的简单的规则,但它可以防止前面提到的第三个堆内存问题:释放后使用。 这补充了所有权的保证,使得程序的完全内存安全,而无需垃圾回收器这样的开销。
|
|
有时Rust被描述为混合解决方案。 实际上,它所做的只是强化代码中的所有权和生命周期规则,然而结果是,用Rust写代码非常安全和无忧无虑,它类似于垃圾回收语言。编译器进行静态校验该程序是内存安全的,如果无法校验它是内存安全的,编译器会产生一个指出潜在风险的错误。 当编译通过后,代码保证不会导致内存损坏。 因为这些校验在构建输出二进制文件之前都发生了,所以这个过程对程序的执行没有任何影响,就像它是用纯C或C++编写的一样轻量。
Rust有非常严格的所有权概念。每一块被分配的内存被一些结构的单独实例所拥有。这些结构可以是任何类型,但通常他们最终是某种来自标准库的集合或Box(Rust的智能指针)。这些包装器负责在自己被销毁的时候释放所拥有的内存。没有简单的方法来显式分配内存和获取原始指针,而不需要任何负责任的包装器。
|
|
所有权是递归的,所以如果一个结构存储另一个结构的值,它将获得后者及其所有子结构的所有权。这也意味着,当容器被销毁时,它必须递归地销毁其所有内容。Rust处理这样的情况可以说是开箱即用一样轻松。所有结构都定义了析构器,它遍历所有字段并首先销毁它们。结构的作者可以在销毁期间添加自己的步骤,例如在编写客户端时关闭数据库连接,但是在此之后字段仍然会被逐一销毁。默认的处理行为在绝大多数情况下都是足够的,因此结构很少会定义析构函数,但是不管有没有定义析构函数,它们都不会泄漏内存。
|
|
Rust的所有权模式带来了一个强大的特性:复杂的堆管理简化为简单的栈管理。程序员不需要担心如何分配和释放内存,这些工作都通过使用局部变量来处理。甚至即使结构里嵌套了许多堆内存的引用,在栈上也总是只有一个根结构,当程序不再需要它的时候,它会自动销毁。
不幸的是,编写那些访问数据需要拥有这些数据的程序并不方便。Rust提供普通的、非智能的、没有所有权的引用,这种引用使得没有所有权的访问成为可能。当这样的引用被创建时,它引用的值是借用的。借用会创建一个双向关系:引用必须具有不超过它引用的值的生命周期,但该值在引用的生命周期内不得移动。这两条规则任何一条被破坏的话,引用所指向的就是无效内存。Rust静态地跟踪并强制执行生命周期的正确性并拒绝危险的程序执行流程。
|
|
|
|
|
|
结构的生命周期永远不能超过它们的任何字段的生命周期。如果其中一个字段恰好是引用,则必须证明整个结构实例在引用值之前被销毁。如果存在对具有生命周期限制的结构的引用,则引用本身的生命周期不能超过结构。只要编译器可以证明它是安全的,这种关系就可以嵌套并绑定任意次数。当编译器无法猜测正确的关系时,可以用简单的语法明确定义它们。
|
|
认为每一个系统都可以用限制性的、静态证明的安全性代码来表达是天真的想当然。在绝大多数的情况下,规则可以胜任,但有时规则也必须进行妥协,Rust提供工具来做这件事。
标准库提供一些包装器将所有权和借用的检查推迟到运行时。这就使有效性检查程序不那么繁忙,并提供了灵活性和很少的运行时开销。例如,Rc是一个没有所有者的Box(带有智能指针的内存)。 它是一个有引用计数器的可被垃圾回收的内存,它的最后一个引用消失的时候,它就被销毁。 Rust提供了更多的包装器,但它们稍微超出了本介绍的范围,它们适用于运行时规则,本文没有涉及。
当在库和工具中进入足够低的层次时,Rust的安全保证变得无法应用。 例如,box和集合触及内存分配和指针,但没有安全保证,因为它们自己做安全保证。 它们可以写在Rust中,因为它们的代码明确标记为不安全。这使得完全忽略安全检查,但这非常危险。 所有外部C库包装器在某些层次也必须使用不安全的代码。 他们定义安全规则,使其与其余代码无缝集成。 不安全的代码是Rust强大能力的来源,但它带来了巨大的责任。 应尽可能避免使用它。
Rust看起来不错,它是由聪明的人使用其他聪明人的学术研究设计的,但它真的有用吗?是的,的确有用。大多数情况下,它只会强制元素之间的明确关系,进行合理安全的设计。毕竟,Rust是与Firefox Web浏览器的未来引擎Servo并行设计的。从一开始,它不仅在理论上是好的,而且在实际的、复杂的软件开发中也被证明是可用的。经过一年使用Rust进行商业编程后,我可以确认,Rust的规则不是一种负担,而是在架构和稳定性保证方面提供了很大的帮助。 我真的相信,Rust这种语言是属于未来的,我强烈推荐大家使用它。
]]>原文链接: https://anixe.pl/content/news/rust_memory_safety_revolution