Darcy's Blog

不如烂笔头


  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

Erlang垃圾回收

发表于 2017-05-21 | 更新于 2018-05-04 | 分类于 Erlang深入 |

由于Erlang的官方文档并没有介绍垃圾回收机制,本文参考一些论文和博客试着来解释下Erlang的内存回收机制,如果有存在错误,欢迎指正。

要解释Erlang的垃圾回收机制必须先知道Erlang的内存管理,在Erlang中存在三种架构来实现不同的内存管理方式,在之前的很长一段时间内用的都是process-centric的架构,现在不知道有没有改成Hybrid,没有找到相关的说明文档。接下来分别介绍下这三种架构: 图1 Hybrid架构内存组织

  • Process-centric 在该架构中,进程间通信需要复制消息,因此是O(n)操作,其中n是消息大小。内存碎片通常比较多。预计垃圾回收的时间和次数预计会很小(因为根集只需要堆叠的进程需要收集),并且在进程终止之后,其分配的内存区域可以在不间断的时间内被回收。在图1中相当于没有Shared Heap区域。
  • Communal 这种架构的最大的优点是非常快(O(1))的通信,只需将指针传递给接收进程,由于消息共享,所以内存的需求也比较小,并且分散性低。缺点在于,必须将所有进程的堆栈作为根集的一部分(导致增加GC延迟),并且由于进程的数据在共享堆上交错而导致可能的缓存性能差。此外,这种架构不能很好地扩展到多线程或多处理器实现,因为需要锁定以便以并行设置分配和收集共享存储器区域。简单的说所有的消息都是共享的,进程内只存了指针。
  • Hybrid 这是一种尝试结合上述两个架构的优点的架构:进程间通信可能很快,并且进程本地堆的频繁收集的GC延迟预计会很小。对进程本地堆的垃圾收集不需要锁定,并且减少了共享堆上的压力,因此它不需要经常进行垃圾回收。而且,像Process-centric架构一样,当一个进程终止时,它的本地内存可以通过简单地将其附加到自由列表(free-list)来回收。图1是该架构的内存组织图。

由于之前Erlang里面默认采用的是Process-centric的架构,所以我们这边介绍Process-centric架构的内存回收方式,如果想要了解Hybrid的内存回收方式可以参考《Message Analysis-Guided Allocation and Low-Pause》这篇论文。

Process-centric架构由于没有Shared Heap,所以内存回收只涉及到进程的内存回收和Shared Area for Binaries的内存回收。

进程内存回收

如图1所示,Erlang的进程和Linux的进程非常的像,由进程控制块(PCB)、堆(Stack)和栈(Heap)组成。

  • 进程控制块:进程控制模块会保存一些关于进程的信息比如它在进程表中的标识符(PID)、当前状态(运行、等待)、它的注册名、初始和当前调用,同时PCB也会保存一些指向传入消息的指针,这些传入消息是存储在堆中连接表中的。
  • 栈:它是一个向下增长的存储区,这个存储区保存输入和输出参数、返回地址、本地变量和用于执行表达式的临时空间。
  • 堆:它是一个向上增长的存储区,这个存储区保存进程邮箱的物理消息,像列表、元组和Binaries这种的复合项以及比像浮点数这种一个机器字更大的对象。超过64机器字的二进制项不会存储在进程私有堆里,而是被存在图1的Shared Area for Binaries里面,进程堆维护一个列表(remembered list),该列表存储该进程指向Shared Area for Binaries区域的所有指针。

进程的内存回收机制采用的是分代标记清除(“stop the world” generational mark-sweep collector)的回收机制,通过这种机制把进程堆划分为了一个老年代(Old Generation)和新生代(Young Generation),使用minor collection对新生代进行垃圾回收,major collection进行整个堆的垃圾回收。进程垃圾回收的时候该进程会卡住,但是由于进程堆的大小一般都比较小所以回收的很快,而且这时候其他进程也在运行,所以垃圾回收不太会影响系统的响应能力。进程创建后的首次垃圾回收会使用major collection,后面在进程运行的过程中如果发现内存不够用的话会先使用minor collection进行回收,如果还是不能释放出足够的空间的话则会使用major collection进行回收,然后如果major collection还是不能释放出足够的空间的话,则会增加进程堆的大小。进程默认的min_heap_size的大小是233个字,进程堆大小的增长策略首先是斐波纳契序列增长,当堆的大小到达1.3M个字的时候堆每次只增长20%。

Shared Area for Binaries内存回收

这个区域的内存回收采用的是标记清除的方法,在每个Binary的头部上会有一个数字,记录着这个Binary被引用了几次。在进程结束之后,进程堆中的remembered list的指针指向的Binary的引用次数会被相应的减1,同样在进程垃圾回收的时候如果发现remembered list中有可以被回收的指针,该指针所指向的Binary的引用次数也会被相应的减1,当一个Bianry的引用次数为0时,这个Binary就可以被回收。

建议

通过了解Erlang垃圾回收的原理,可以在垃圾回收方面对系统进行一些调优,以及减少系统的内存使用量,以下是我总结的一些建议:

  • 进程默认的min_heap_size的大小是233个字,如果能提前知道进程大概需要多少空间的话,在进程创建的时候指定min_heap_size的大小可以减少内存回收的次数。
  • 由于进程内存回收是每个进程单独进行的,所以有些进程在申请了很多空间之后,很久没有运行,但是上次申请的空间其实有些已经没用了,如果进程一直不运行或者不触发回收,这部分内存就一直回收不了,这时候就需要手动的进行内存回收。建议可以定时执行以下代码进行内存回收:

    1
    2
    [erlang:garbage_collect(P) || P <- erlang:processes(),
    {status, waiting} =:= erlang:process_info(P, status)],

    我所在的项目在每次启动的时候都会进行大量的初始化,在项目启动成功后对所有进程一次手动回收也可以节省很多内存。

  • 对一些重量级的操作可以spawn一个进程出来处理,当进程结束后该部分空间就能被完全回收了,比在原进程上面执行应该会好些。
  • 之前看到一个开源项目在处理完一个请求后就对该进程进行一次手动回收,这个好像也是一个优化,因为一个请求过后再上来一个请求的话,可能需要秒的级别,进程在这段空闲时间进行一次回收不会影响系统的响应而且还能节省内存。

编程语言的垃圾回收机制简介

发表于 2017-05-09 | 更新于 2018-05-04 | 分类于 基本概念 |

现在的编程语言中大多都包括了垃圾回收(Garbage Collection)机制,垃圾回收机制是一种自动的内存管理机制,当计算机内存中的一个对象不再需要被使用时,就会自动的让出这块内存。在早期的C/C++编程语言中,程序员需要自己手动申请和释放内存,而因在编程的过程中往往会经常忘记释放那些不再使用的内存,进而造成内存泄漏。垃圾回收机制可以大大减轻程序员的负担,减少程序员犯错的机会。垃圾回收机制最早起源于LISP语言,目前的大多数高级语言都支持内存回收机制,比如:PHP、Java、C#、Erlang等等。

垃圾回收算法的原理:

  • 推算出某个对象在未来的程序运行中将不再会被访问。
  • 将这些对象占用的内存回收。

收集器实现

  • 引用计数收集器 最早期的垃圾回收实现方法,通过对数据存储的物理空间附加多一个计数器空间,当有其他数据与其相关时则加一,反之相关解除时减一,定期检查各储存对象的计数器,为零的话则认为已经被抛弃而将其所占物理空间回收。是最简单的实现,但存在无法回收循环引用的存储对象的缺陷。
  • 跟踪收集器 近现代的垃圾回收实现方法,通过定期对若干根储存对象开始遍历,对整个程序所拥有的储存空间查找与之相关的存储对象和没相关的存储对象进行标记,然后将没相关的存储对象所占物理空间回收。

回收算法

主要的回收算法可以分为以下几类:

  • 标记-清除 先暂停整个程序的全部运行线程,让回收线程以单线程进行扫描标记,并进行直接清除回收,然后回收完成,恢复运行线程。会导致大量零碎的空闲空间碎片,导致大容量对象不容易获得连续的内存空间,而造成空间浪费。
  • 标记-压缩 和“标记-清除”相似,不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间。从而集成空闲空间。
  • 复制 需要程序将所拥有的内存空间分成两个部分。程序运行所需的存储对象先存储在其中一个分区(定义为“分区0”)。同样暂停整个程序的全部运行线程后,进行标记后,回收期间将保留的存储对象搬运汇集到另一个分区(定义为“分区1”),完成回收,程序在本次回收后将接下来产生的存储对象会存储到“分区1”。在下一次回收时,两个分区的角色对调。
  • 增量回收器 需要程序将所拥有的内存空间分成若干分区。程序运行所需的存储对象会分布在这些分区中,每次只对其中一个分区进行回收操作,从而避免程序全部运行线程暂停来进行回收,允许部分线程在不影响回收行为而保持运行,并且降低回收时间,增加程序响应速度。
  • 分代 由于“复制”算法对于存活时间长,大容量的储存对象需要耗费更多的移动时间,和存在储存对象的存活时间的差异。需要程序将所拥有的内存空间分成若干分区,并标记为年轻代空间和年老代空间。程序运行所需的存储对象会先存放在年轻代分区,年轻代分区会较为频密进行较为激进垃圾回收行为,每次回收完成幸存的存储对象内的寿命计数器加一。当年轻代分区存储对象的寿命计数器达到一定阈值或存储对象的占用空间超过一定阈值时,则被移动到年老代空间,年老代空间会较少运行垃圾回收行为。一般情况下,还有永久代的空间,用于涉及程序整个运行生命周期的对象存储,例如运行代码、数据常量等,该空间通常不进行垃圾回收的操作。 通过分代,存活在局限域,小容量,寿命短的存储对象会被快速回收;存活在全局域,大容量,寿命长的存储对象就较少被回收行为处理干扰。

实现

上面是垃圾回收的基本算法,有些编程语言的垃圾回收机制会使用上面的算法然后再自己进行改造优化性能。比如Erlang就同时使用分代回收和标记清除的算法来实现垃圾回收机制。

Erlang代码热更新

发表于 2017-05-04 | 更新于 2018-05-04 | 分类于 Erlang入门教程 |

通过上一篇的文章Erlang动态代码载入小实验,我们可以了解到Erlang的热更机制,在Erlang里面会维护两个版本的代码。在新版本载入的时候如果有进程在老版本运行的话,运行那些内部调用的函数(只通过函数名调用的)代码将不会被更新,只有那些通过M:F格式调用的内部函数才能热更。举个例子:

1
2
3
4
5
6
7
8
9
%% 如果代码在loop上执行的话,有两种情况

loop() ->
io:format("v1 ~n"), %% 这条语句这种情况不能热更
loop().

loop() ->
io:format("v1 ~n"), %% 这条语句这种情况可以热更
?MODULE:loop().

在Erlang里面有分本地调用(local calls)和外部调用(external calls),本地调用的函数名是不需要被导出的。本地调用的格式是Fun(Args),外部调用的格式是M:F(Args)。

Erlang运行时会保存一份代码的两个版本,所有本地调用的函数地址都会指向程序运行时最初的那个版本(如上面例子的情况一),而所有外部调用的函数地址都会指向最新的版本(如上面例子的情况二)。所以如果想要让代码能够热更新的话,需要使用外部调用的格式。

在我们项目中一般热更的流程是先:code:soft_purge(ModName)或者code:purge(ModName)然后再code:load_file(ModName)进行热更,针对这一热更流程我之前一直存在两个问题,最近仔细研究下才找到了答案,分别是以下这两个问题:

  • 为什么load_file之前要先soft_purge或者purge一下呢? 这个是load_file函数的问题,如果在load_file执行的时候,本身要热更的模块就有一个老的版本的代码存在的话,load_file就会返回一个not_purged的错误代码,导致新版本不能正常的载入。如果load_file执行自动删除最老版本的话,就不需要purge了(像在Erlang Shell里面执行c(ModName)一样)。当然如果一个模块从来都没有热更过的话(在系统里面只有一个版本),直接使用load_file是没有问题的,不过之后就要先purge再load_file了。
  • soft_purge和purge有什么不同吗? 函数的功能上是有所不同的,但是在我们项目的使用中几乎是没有什么不同的。soft_purge和purge的函数的功能区别是如果清理的模块的老的版本中有进程在上面运行的话,purge就会杀掉进程,然后把老的版本给清理掉,soft_purge则会清理失败。热更的时候是先执行purge然后再loadfile,由于进程一般都是在当前的版本上面执行,这时候老的版本上面不会有进程在运行,所以执行purge和soft_purge是一样的,如果真的想要热更的时候把进程杀掉的话应该执行purge/soft_purge->loadfile->purge。

以上就是我对Erlang代码热更的总结~

Erlang动态代码载入小实验

发表于 2017-05-01 | 更新于 2018-05-04 | 分类于 Erlang入门教程 |

下面的内容为《Erlang程序设计(第2版)》8.10节的内容,这个动态代码载入的小实验非常的简单生动,通过这个小实验能够充分理解Erlang代码的载入机制。

动态代码载入是内建于Erlang核心的最惊人特性之一。它的美妙之处在于你无需了解后台的运作就能顺利实现它。 它的思路很简单:每当调用 someModule:someFunction(…)时,调用的总是最新版模块里的最新版函数,哪怕当代码在模块里运行时重新编译了该模块也是如此。 如果在a循环调用b时重新编译了b,那么下一次a调用b时就会自动调用新版的 b 。如果有许多不同进程正在运行而它们都调用了b,那么当b被重新编译后,所有这些进程就都会调用新版的b 。为了了解它的工作原理,我们将编写两个小模块:a和b。

1
2
3
4
5
%% b.erl
-module(b).
-export([x/0]).

x() -> 1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
%% a.erl
-module(a).
-compile(export_all).

start(Tag) ->
spawn(fun() -> loop(Tag) end).

loop(Tag) ->
sleep(),
Val = b:x(),
io:format("Vsn1 (~p) b:x() = ~p~n", [Tag, Val]),
loop(Tag).

sleep() ->
receive
after 3000 -> true
end.

现在可以编译a和b,然后启动两个a进程。

1> c(a). {ok,a} 2> c(b). {ok,b} 3> a:start(one).

<0.70.0> Vsn1 (one) b:x() = 1 Vsn1 (one) b:x() = 1 4> a:start(two).

<0.72.0> Vsn1 (one) b:x() = 1 Vsn1 (two) b:x() = 1

这些a进程休眠3秒钟后唤醒并调用b:x(),然后打印出结果。现在进入编辑器,把模块b改 成下面这样:

1
2
3
4
-module(b).
-export([x/0]).

x() -> 2.

然后在shell里面重新编译b。这是现在所发生的:

5> c(b). {ok,b} Vsn1 (one) b:x() = 2 Vsn1 (two) b:x() = 2

两个原版的a仍然在运行,但现在它们调用了新版的b。所以在模块a里调用b:x()时,实际上是在调用“b的最新版”。我们可以随心所欲地多次修改并重新编译b,而所有调用它的模块无需特别处理就会自动调用新版的b。 现在已经重新编译了b,那么如果我们修改并重新编译a会发生什么?来做个试验,把a改成下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-module(a).
-compile(export_all).

start(Tag) ->
spawn(fun() -> loop(Tag) end).

loop(Tag) ->
sleep(),
Val = b:x(),
io:format("Vsn2 (~p) b:x() = ~p~n", [Tag, Val]),
loop(Tag).

sleep() ->
receive
after 3000 -> true
end.

现在编译并启动a。

6> c(a). {ok,a} Vsn1 (two) b:x() = 2 Vsn1 (one) b:x() = 2 Vsn1 (two) b:x() = 2 7> a:start(three).

<0.84.0> Vsn1 (two) b:x() = 2 Vsn1 (one) b:x() = 2 Vsn2 (three) b:x() = 2 Vsn1 (two) b:x() = 2

有趣的事情发生了。启动新版的a后,我们看到了新版正在运行。但是,那些运行最初版a的现有进程仍然在正常地运行旧版的a。 现在可以试着再次修改b。

1
2
3
4
-module(b).
-export([x/0]).

x() -> 3.

我们将在shell里重新编译b,观察会发生什么。

8> c(b). {ok,b} Vsn1 (one) b:x() = 3 Vsn2 (three) b:x() = 3 Vsn1 (two) b:x() = 3

现在新旧版本的a都调用了b的最新版。 最后,再次修改a(这是第三次修改a了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-module(a).
-compile(export_all).

start(Tag) ->
spawn(fun() -> loop(Tag) end).

loop(Tag) ->
sleep(),
Val = b:x(),
io:format("Vsn3 (~p) b:x() = ~p~n", [Tag, Val]),
loop(Tag).

sleep() ->
receive
after 3000 -> true
end.

现在,当我们重新编译a并启动一个新版的a时,就会看到以下输出:

9> c(a). {ok,a} Vsn2 (three) b:x() = 3 Vsn2 (three) b:x() = 3 Vsn2 (three) b:x() = 3 Vsn2 (three) b:x() = 3 10> a:start(four).

<0.96.0> Vsn2 (three) b:x() = 3 Vsn3 (four) b:x() = 3 Vsn2 (three) b:x() = 3 Vsn3 (four) b:x() = 3 Vsn2 (three) b:x() = 3

这段输出里的字符串是由两个最新版本的a(第2版和第3版)生成的,而那些运行第1版a代码的进程已经消失了。 在任一时刻,Erlang允许一个模块的两个版本同时运行:当前版和旧版。重新编译某个模块时,任何运行旧版代码的进程都会被终止,当前版成为旧版,新编译的版本则成为当前版。可以把这想象成一个带有两个版本代码的移位寄存器。当添加新代码时,最老的版本就被清除了。一些进程可以运行旧版代码,与此同时,另一些则可以运行新版代码。

Erlang中catch和try...catch的区别

发表于 2017-05-01 | 更新于 2018-05-04 | 分类于 Erlang入门教程 |

在Erlang的错误处理中,catch并不是try…catch的缩写,try…catch和catch是不同的。下面我将通过一个例子来区别出他们的不同,为以后的使用做一个参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
%% exception_test.erl 代码文件
-module(exception_test).

-compile(export_all).

generate_exception(1) -> a;
generate_exception(2) -> throw(a);
generate_exception(3) -> error(a);
generate_exception(4) -> exit(a);
generate_exception(5) -> {'EXIT', a}.

test_use_catch() ->
[{I, catch generate_exception(I)} || I <- lists:seq(1, 5)].

test_user_try_catch() ->
[begin
try generate_exception(I) of
NormalRes ->
{I, normal, NormalRes}
catch
ErrorType : Error ->
{I, exception, ErrorType, Error}
end
end || I <- lists:seq(1, 5)].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
%% 执行exception_test:test_use_catch().函数的返回结果
[{1,a},
{2,a},
{3,
{'EXIT',{a,[{exception_test,generate_exception,1,
[{file,"exception_test.erl"},{line,7}]},
{exception_test,'-test_use_catch/0-lc$^0/1-0-',1,
[{file,"exception_test.erl"},{line,12}]},
{exception_test,'-test_use_catch/0-lc$^0/1-0-',1,
[{file,"exception_test.erl"},{line,12}]},
{erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
{shell,exprs,7,[{file,"shell.erl"},{line,686}]},
{shell,eval_exprs,7,[{file,"shell.erl"},{line,641}]},
{shell,eval_loop,3,[{file,"shell.erl"},{line,626}]}]}}},
{4,{'EXIT',a}},
{5,{'EXIT',a}}]
1
2
3
4
5
6
%% 执行exception_test:test_user_try_catch().函数的返回结果
[{1,normal,a},
{2,exception,throw,a},
{3,exception,error,a},
{4,exception,exit,a},
{5,normal,{'EXIT',a}}]

通过上面的列子我们可以看到,如果使用标准的try…catch来处理错误的话,调用者是可以正确的识别出错误,然后对错误进行相应的处理的。

但是如果用的是catch来处理错误的话,情况是不能乐观的,使用catch处理错误,exception(1)和exception(2)返回的结果是一样的,exception(4)和exception(5)返回的结果是一样的。catch在处理throw的时候只是简单的把throw的内容给返回,在处理exit的时候会返回一个tuple是带’EXIT’和exit里面的内容的结果,在处理error的时候会把堆栈给打印出来(这点比较人性化)。

所以大家在使用catch的时候要注意catch的返回值,正常的情况下还是推荐使用try…catch来处理错误,不然很容易就会掉到坑里面的。

吴恩达机器学习课程练习实现

发表于 2017-05-01 | 更新于 2018-05-04 | 分类于 代码 |

由于当前机器学习大火,让我对机器学习产生浓厚的兴趣,所以我就上网查了下机器学习的入门教程,大多数的人还是比较推荐吴恩达老师的机器学习课程的。所以我就在Coursera上面学习了吴恩达的机器学习课程,现在已经顺利的毕业了。 在Coursera上面每学完一小节课程都会有相应的练习,系统会自动对你提交的练习进行打分,要达到指定的分数才能顺利通过,这点Coursera的体验还是做的比较好的,但是由于国内网络的原因,在Coursera上面看视频,如果不使用科学上网的话,有时候是看不了的。 我做练习用的是Octave,用Octave提交代码的时候会报错,需要在练习中进行以下的代码替换:

lib/submitWithConfiguration.m 文件66行

1
responseBody = urlread(submissionUrl, 'post', params);

替换成:

1
[code, responseBody] = system(sprintf('echo jsonBody=%s | curl -k -X POST -d @- %s', body, submissionUrl));

最后附上我的练习代码,有需要的可以自取,如果对你有帮助的话,记得给我星星哈~ 代码地址:https://github.com/lintingbin2009/machine-learning-ex

剑指offer——C语言实现

发表于 2017-05-01 | 更新于 2018-05-04 | 分类于 代码 |

之前实习的时候为了能够比较顺利的找到一个实习,特意练习了下代码能力,期间看了挺多的算法书籍,然后把剑指offer书本上的那些练习大部分都自己用C语言实现了一遍。 这些练习的代码都已经传到了Github上面了,地址是: https://github.com/lintingbin2009/C-language/tree/master/%E5%89%91%E6%8C%87offer 有需要的可以自取,如果觉得对你有帮助的话,记得给我个星星哈~

Erlang数据类型

发表于 2017-04-30 | 更新于 2018-09-09 | 分类于 Erlang入门教程 |

Erlang提供的数据类型,包括以下几种:

基本类型

  • 数字(Number) 数字类型又包含整数(integers)和浮点数(floats)类型,其中整数是精确的而且是支持大数的,小数是满足IEEE754规则的64位浮点数。Erlang支持ASCII或者Unicode转换成整数值,同时支持整数不同进制的表示。(‘%’后的内容为注释)

    1> $a.  %% ASCII表中的a是97 97 2> $哈. 21704 3> $\n. 10 4> 2#100.  %% 用100表示的二进制是4 4 5> 4#100. 16 6> 16#100. 256

  • 原子(Atom) 原子可以理解为一个不可变的常量,必须以小写字母开头,如果要以大写、下划线或者其他的特殊字符开头,必须加上单引号。原子在Erlang里面是存在一张表上面的,原子的个数有上限,大概是在一百万个左右。

    test ‘Myhome’ ‘_hero’

  • 位串和二进制(Bit Strings and Binaries) 在大多数情况下,二进制型里的位数都会是8的整数倍,因此对应一个字节串。如果位数不是8的整数倍,就称这段数据为位串(bitstring)。所以当我们说位串时,是在强调数据里的位数不是8的整数倍。位语法是一种表示法,用于从二进制数据里提取或加入单独的位或者位串。当你编写底层代码,以位为单位打包和解包二进制数据时,就会发现位语法是极其有用的。

    1> <<257,1,2,3,5>>.   %%二进制型的元素如果大于8位的会自动截断,257截断成1 <<1,1,2,3,5>> 2> <<0:7,1:2>>.   %%二进制型位数如果不是8的整数倍就会产生位串,这边多了1位1 <<0,1:1>> 3> <<0:3,0:4,1:2>>. <<0,1:1>>

  • 引用(Reference) 可以通过make_ref/0函数来创建一个引用,引用在Erlang程序运行时调用make_ref函数产生的是全局唯一的。比如timer模块在创建一个定时任务的时候通常会返回一个引用,可以通过这个引用来取消定时任务。
  • 函数(Fun) 函数在Erlang里面也算是一种数据类型,通过给变量绑定函数,可以通过变量名来执行函数。

    1> Fun = fun(X) -> X * X end.

    #Fun<erl_eval.6.50752066> 2> Fun(9). 81
  • 端口标识符(Port Identifier) 端口用于与外界通信,由通过函数open_port/2来创建。消息可以通过端口进行收发,但是这些消息必须遵守所谓“端口协议”(port protocol)的规则。
  • 进程标识符(Pid) 当创建一个进程的时候会产生一个进程标识符,可以通过这个进程标识符和进程进行通讯。

    1> Process1 = spawn(fun() -> receive X -> io:format(“recv ~p, bye~n”, [X]) end end).

    <0.34.0>  %% 创建一个进程等待接收消息 2> Process1 ! my_test.  %% 给进程发消息 recv my_test, bye my_test

复合类型

为了方便定义以下的这些复合类型,我把上述的所有基本类型都称为Term。

  • 元组(Tuple) 元组类似于C语言里面的结构体(Struct),是由固定数量的元素组成的复合数据类型,可以定义成如下结构:

    {Term1, Term2, …, TermN}

    可以通过模式匹配或者element/2函数来提取元组里面元素的值,通过setelement/3来设置元组里面元素的值,size可以取元组里面元素的个数。

    1> P = {adam,24,{july,29}}. {adam,24,{july,29}} 2> element(1,P). adam 3> element(3,P). {july,29} 4> P2 = setelement(2,P,25). {adam,25,{july,29}} 5> size(P). 3 6> {adam, Old, {Month, Day}} = P. {adam,24,{july,29}} 7> Old. 24

  • 映射组(Map) 映射组是一个由多个Key-Vaule结构组成的符合数据类型,可以定义为如下结构:

    #{Key1=>Value1, Key2=>Value2, …, KeyN=>ValueN} 其中Key、Value都是Term

    可以通过maps模块提供的一些函数对映射组进行操作

    1> M1 = #{name=>adam,age=>24,date=>{july,29}}.

    #{age => 24,date => {july,29},name => adam} 2> maps:get(name,M1). adam 3> maps:get(date,M1). {july,29} 4> M2 = maps:update(age,25,M1). #{age => 25,date => {july,29},name => adam} 5> map_size(M). 3 6> map_size(#{}). 0

  • 列表(List) 列表类似于其他语言里面的数组,是由可变数量的元素组成的复合数据结构,可以定义成如下结构:

    [Term1, Term2, …, TermN]

    在Erlang里面,列表由一个头和一个尾组成,空列表也是一个列表。所以列表也可以有一个递归的定义

    List = [Term| List] | [] [] 是一个列表, 因此 [c|[]] 是一个列表, 因此 [b|[c|[]]] 是一个列表, 因此
    [a|[b|[c|[]]]] 是一个列表, 或者简写为 [a,b,c]

    lists模块可以提供大量函数对列表进行操作:

    1> L = [3,3,4,2,1,2,34]. [3,3,4,2,1,2,34] 2> length(L). 7 3> lists:sort(L). [1,2,2,3,3,4,34] 4> lists:reverse(L). [34,2,1,2,4,3,3]

其他类型(不算数据类型)

  • 字符串(String) 字符串用一对双引号括起来,但不算是Erlang中的数据类型。字符串仅仅是列表的一个缩写,比如:字符串”hello”是列表[$h,$e,$l,$l,$o]的一个缩写。两个相邻的字符串在编译的时候连接成一个字符串,不会造成任何运行时开销。

    1> “hello” “ “ “world”. “hello world”

  • 记录(Record) 记录其实就是元组的另一种形式。通过使用记录,可以给元组里的各个元素关联一个名称。对记录的处理是在编译的时候完成的,在运行时是不会有记录的,可以把记录理解成是元组的一种语法糖。

    1
    2
    3
    4
    5
    -module(person).
    -export([new/2]).
    -record(person, {name, age}).
    new(Name, Age) ->
    #person{name=Name, age=Age}.

    1> person:new(ernie, 44). {person,ernie,44}

  • 布尔类型(Boolean) 在Erlang中没有Boolean类型。而是用原子true和false来表示布尔值。

    1> 2 =< 3. true 2> true or false. true

类型转换

Erlang提供了一些内置的类型转换函数,可以方便地进行类型转换,下面是一些类型转换的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
1> atom_to_list(hello).
"hello"
2> list_to_atom("hello").
hello
3> binary_to_list(<<"hello">>).
"hello"
4> binary_to_list(<<104,101,108,108,111>>).
"hello"
5> list_to_binary("hello").
<<104,101,108,108,111>>
6> float_to_list(7.0).
"7.00000000000000000000e+00"
7> list_to_float("7.000e+00").
7.0
8> integer_to_list(77).
"77"
9> list_to_integer("77").
77
10> tuple_to_list({a,b,c}).
[a,b,c]
11> list_to_tuple([a,b,c]).
{a,b,c}
12> term_to_binary({a,b,c}).
<<131,104,3,100,0,1,97,100,0,1,98,100,0,1,99>>
13> binary_to_term(<<131,104,3,100,0,1,97,100,0,1,98,100,0,1,99>>).
{a,b,c}
14> binary_to_integer(<<"77">>).
77
15> integer_to_binary(77).
<<"77">>
16> float_to_binary(7.0).
<<"7.00000000000000000000e+00">>
17> binary_to_float(<<"7.000e+00>>").
7.0

为什么使用Erlang?

发表于 2017-04-29 | 更新于 2018-05-04 | 分类于 Erlang入门教程 |

主要特性

如果问我觉得Erlang最重要的特性是什么的话,我觉得应该是并发。

并发能够带来的好处是不言而喻的,比如:

  • 性能 现在的计算机由于主频的限制,都在往多核的方式发展,有些比较高端的机器甚至有几十个核心。如果编写的程序都是顺序运行的话将会严重浪费多核计算机的计算能力。Erlang本身是面向并发编程的,如果把之前在单核机器上面跑的Erlang程序放到多核机器上面跑的话,性能将会极大的提高。
  • 扩展性 如果在一台机器上面运行Erlang程序还不能满足性能的要求的话,可以简单的升级机器的CPU核心个数,甚至可以经过简单的改造把不同的进程分配到不同的机器上面运行,通过水平扩展方式来满足高并发的业务需求。
  • 容错性 Erlang内部实现的进程是相互独立的,一个进程的崩溃并不会影响到另外一个进程的运行,同时Erlang内部还OTP框架来保证系统的容错性。
  • 清晰性 Erlang世界观和现实的世界是一样的,在大多数的编程语言里面事情都是顺序发生的,但是在Erlang的世界里面所有的事件都是并发的,在编写程序的时候能够比较清晰的把现实世界事件的并行发生的的特性映射到Erlang的并发编程上面。

简介

快速介绍下Erlang比较与众不认同的特性:

  • Erlang Shell 在编写Erlang程序的过程中会有很多时间花费在Erlang Shell里面,Erlang Shell类似于Linux的Bash,开发者能在Erlang Shell里面运行表达式,通过这种交互方式,开发者能够在Erlang Shell里面调试正在运行的Erlang程序(包括远程的Erlang程序)。
  • = 操作符 在一般的编程语言里面,=表示赋值操作,一个变量能够被多次赋值。但是在Erlang里面变量是不可变的,一旦通过=绑定之后,该变量的值就不能发生改变了,重复绑定会导致异常。
  • 变量和原子 所有Erlang的变量都是以大写字母开头的,比如:One、This和My_baby这些都是变量。以小写字母开头的则是符号常量(被称为原子:atom),比如:person、one和hello_world。
  • 进程 Erlang的进程是Erlang虚拟机内部自己实现的进程,非常轻量级,刚开始创建的时候每个进程的大小也就2KB左右,1GB的内存就可以创建50万个进程。同时进程间没有共享内存,进程间的通信通过消息转发实现。

总结

Erlang的特性决定了它是一门比较另类的语言,相信第一次见到它的人会觉得很吃惊,世界上居然会有这样的一门语言。但正是由于这些看似奇怪的特性,让Erlang能够在当今多核的时代充分的发挥它的能力。

使用HEXO在Github上搭建个人博客

发表于 2017-04-29 | 更新于 2018-05-04 | 分类于 教程 |

在平时的工作中经常会遇到一些问题,在解决问题的时候如果能够及时记录下来是最好不过的,所以一直想维护一个自己的博客。虽然国内有各种技术博客(比如:CSDN,博客园)之类的第三方博客平台,但是作为一个程序员,不搭建一个自己的博客感觉不够酷。所以我就选择使用HEXO在Github上面搭建自己的个人博客。

下面的安装教程都是在Window x64的环境下进行的

安装步骤

申请Github账户

由于博客是要搭建在Github上面的,所有必须要有一个Github账号来上传代码,这样才能最终显示自己的博客内容。在建立完Github账号后,需要创建一个Repositories,这个Repositories的名字的格式是:your_user_name.github.io这样的。

安装Git软件

有了Github账号后还需要有软件能把本地的代码上传到Github上面,所就安装Git软件,安装Git也非常简单,直接下一步就行了。

安装NodeJs

由于Hexo是基于NodeJs的框架,所以使用Hexo前要先安装NodeJs,安装NodeJs也非常简单,只需要下载软件,点下一步就行了。现在新的版本的NodeJs,会同时安装npm(Node包管理软件),所以安装起来非常简单。

安装Hexo

把上面的软件都安装好了之后就可以开始安装Hexo了,打开window的终端,在终端中输入下面的命令开始安装Hexo

1
npm install -g hexo

使用步骤

初始化

创建一个文件夹,如:MyBlog之类,然后进到MyBlog文件夹下执行以下初始化命令

1
hexo init

到了这一步之后,Hexo算初始化完成,可以正常的使用了。

生成静态页面

继续在MyBlog目录下执行如下命令,生成静态页面

1
hexo generate // 简写 hexo g

本地启动

启动本地服务,进行文章预览调试,命令:

1
hexo server   // 动态启动,有修改发生会自动检测,简写 hexo s

然后在浏览器输入 http://localhost:4000 就可以看到博客的页面,当然也在服务器启动的时候加上-p来指定自己想要的端口

部署步骤

安装 hexo-deployer-git

1
npm install hexo-deployer-git --save

配置部署环境

在MyBlog的目录下会有一个_config.yml的文件,该文件为Hexo项目的配置文件,打开该文件然后把deploy部分改成下列格式

1
2
3
4
deploy:
type: git
repository: https://github.com/lintingbin2009/lintingbin2009.github.io.git // lintingbin2009替换成你自己的名字
branch: master

开始部署

1
hexo deploy

部署完成之后就可以使用your_username.github.io来访问你的个人博客了, 之后的部署命令应该是

1
2
3
hexo clean
hexo generate
hexo deploy

总结

总的来说用Hexo在Github上搭建个人博客还是比较简单的,当然这边只是涉及到最简单的搭建,还没有涉及到主题的更换、评论系统,统计系统。更多关于Hexo的使用文档可以浏览Hexo的中文官网,里面有详细的使用教程和很多可选的精美主题。

1…45
Darcy

Darcy

欢迎来到我的个人站

50 日志
17 分类
40 标签
RSS
GitHub E-Mail
© 2019 Darcy
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Mist v6.4.2