少数派报告----Edward's Webblog

Some raw thought.

Download as .zip Download as .tar.gz View on GitHub

Using Ai In Development

AI 在当前软件开发中的应用探讨

随着ChatGPT的横空出世,这两年人工智能成了热门话题,在不同的领域人们都在讨论如何将AI融入具体的应用,同时,人们也开始担忧自己的工作是否会被AI所取代。在编程领域,AI的出现并不算新闻,开发者们从多年以前就不断研究和探索如何更简单地让计算机生成代码,从各种DSL到UML代码生成。但这个过程始终需要更加专业的开发者来完成,也对操作者的计算机基础要求较高。然而,随着大语言模型的应用,AI生成代码变得真正智能起来。不仅是ChatGPT 4.0的组件,GitHub copilot、jetbrains 的AI assistant也迅速被集成到各种开发工具中。虽然生成的代码在安全性等细节方面仍有待提高,但在功能性方面已经变得完全可用。

作为开发者,我们并不需要过度担心是否会被取代,各种危言耸听的言论经过媒体的发酵往往搞得人心惶惶。在我看来,目前的情况正是我们长久追求的,我们不仅不应该畏惧AI,反而应该拥抱这一变化,谁能够更快地适应和掌握这一变化,谁才更容易在未来脱颖而出。

我经常遇到一些同事,工作的时候常常抱怨自己做的事情是重复劳动,没有创造力,想要换工作,但换了工作之后不久又开始同样的抱怨。当然这里面有企业的问题,但另一方面也有个人的问题。我们不妨考虑一下,是不是身边的同事总有那么几个人即便是在同一个项目相同的职位也能把事情做得更好,我们咒骂写下旧系统代码的人留下一堆垃圾的时候这几个人已然悄悄地重构整理了代码,写出了更适合当前阶段的系统和模块功能。当我们还在网络上看着漫画说当一个系统能运行就不要动它时候,这些人已经把一座危楼改建成了漂亮的别墅。同样的,当我们担忧AI的到来是否会替代我们的时候,他们已经把AI融入自己的日常工作之中了。可是当你去使用时候又会觉得怎么这么不顺手?我们抱怨工作乏味,然而,人工智能帮助我们来完成乏味的工作时我们又觉得不好用。实际上,有时候并不是工作乏味,而是我们自己不思考。我们从开始面对工作就没有认真思考怎么做效率更高,怎样能让自己更轻松。同样地,引入了AI之后,我们依然没有思考如何给它下指令来更好地完成工作。

前些天看到吴恩达博士的一段演讲,演示了如何使用多角色agent使得GPT更加好用,通过把工作内容细分成不同的阶段,并分配给不同的AI角色,使多个agent互相协同,与ChatGPT互相问答,上下文不断完善,从而获得我们需要的结果。根据统计数据,这样使用的效果比我们过去的方式效率提高了近两倍。

所以说,单单是AI本身无法代替我们来工作,这一点完全不必担忧,真正使得AI完成工作的依然是人。对于我们来说,现在是反思的时候了。我们要考虑的不是如何去堆砌一堆低效的代码,而是怎样给AI下达指令来获得更加高效、稳定、安全的代码;怎样借助于AI大模型突破我们既往的知识壁垒,从而使不同系统、语言更好地协同工作,创造出以往的个人和团队无法完成的产品。

go 1.16 embed标签初探

go1.6版本昨天发布了,与1.15版本相隔了半年。这次的更新引入了新的embed包,通过//go:embed 指令可以在编译时内嵌文件。用法如下:

package main

import _ "embed"

//go:embed hello.txt
var s string

func main() {
	print(s)
}

在同目录下创建hello.txt文件,

​ hello world, it’s the first workday.

编译执行,可以看到以下输出:

官方文档给出的说明是引入“embed”包的go源文件可以使用//go:embed指令初始化一个string类型变量、[]byte, 或者从包所在目录或子目录读取文件内容的FS(一个只读的文件集合)。其中变量的类型必须是字符串、字节切片或者FS(或FS的别名)。

看来文件内容不仅可以是字符串,还可以是[]byte的切片。我们看以下例子:

package main

import _ "embed"

//go:embed hello.txt
var b []byte

func main() {
	print(string(b))
}

hello.txt文件内容不变。执行go run myEmbedSlice.go结果如下:

可以看到输出了相同的结果。

接下来,用读取文件到FS的方式代码如下:

package main

import "embed"

//go:embed hello.txt
var f embed.FS

func main() {
	data, _ := f.ReadFile("hello.txt")
	print(string(data))
}

hello.txt内容依然不变,看看运行结果:

在变量声明上方的go://embed指令指定了要内嵌的文件,文件名可以支持模式匹配,例如:

go://embed *.txt

embed指令必须在紧邻变量声明的上一行,在指令和变量声明之间仅允许空行或注释。

当然,内嵌的文件类型也不限于文本,例如:

  • .gob

  • *.tmpl

    关于内嵌的详细内容可以参见官方文档和Carl Johnson的文章: How to Use Go embed

杂谈现代高级编程语言[转]

之前看到过这篇文章,不记得是谁的了,当时存了PDF放在机器上,正好刚才看到了,就贴出来吧,好象是陈硕的文章。如有其它问题请右键告知,如有法律问题,亦请告知,我会及时删除。以下是原文:

杂谈现代高级编程语言

几个月之前,Slashdot 转载了Robert Harper教授的一篇博客,说卡内基梅隆大学计算机系把”面向对 象编程“从大一新生的必修课中删掉了,其原因是 Object-oriented programming … is both anti-modular and anti-parallel by its very nature. 这两个原因(anti-modular和anti-parallel)都是很重的指责了;尤其是anti-modular,因为OO的基 本思想通常被理解成“封装”,从而实现模块化。 我是在1995年第一次听说“面向对象”(Object Oriented)这个说法。当时在学习正在成长过程中的 C++,用的是Borland C++ 1.0。从那时开始的很多年里,”类“(class)、“对象”(object)和 “方法”(methods),以及在这些概念之上构建的”继承“(inheritance)和”多态 “(polymorphism)都是我理解中OO最核心的思想。我猜大多数程序员在这方面的认识都和我差不 多。 但是“封装”真的是OO的本质嘛?直到最近为了给iPhone写个玩具程序而学习Objective-C(一种非 常古老和原始的面向对象编程语言)的时候,才注意到早在1998年,OO之父Alan Kay就曾经在一篇 邮件中说,他很后悔发明了“object”这个词,从而误导大家,把注意力都集中到“封装”,而忽视了 OO的本质——messaging(消息传递)。Alan Kay的原话是: The big idea is “messaging” … . The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be. Objective-C的设计是非常强调“消息传递”(messaging)的——对一个object的method的调用, 被称为“给这个object发了一个消息”。为了突出调用method时指定的参数(parameters)实际上 是消息中的一些内容,Objective-C不惜把method的定义方式都做了相对于C的很大的修改,从而把 参数嵌入在method的名字里。比如在一个叫做myWebView对象中搜索一段文字,要求不区分大小写, 从前往后搜索,用Objective-C来描述是:

[myWebView searchFor:myString
direction:YES
caseSensitive:NO
wrap:YES]

而用C++或者Java来描述,则是

myWebView.searchFor(myString, YES, NO, YES)

乍看上去,C++ 或者Java的方式更简短,但是Objective-C的方式更强调“发消息”。实际上,上面 Objective-C语句会被翻译成如下C函数调用:

objc_msgSend(myWebView,
searchFor:direction:caseSensitive:wrap:,
myString, YES, NO, YES)

从强调messaging的角度看,Objective-C确实比C++和Java更符合Alan Kay对OO思想的描述。 OO中的messaging思想不仅体现在Objective-C语言以及在其上构建的NextSTEP/Cocoa GUI编程套 件上。在Cocoa因为Mac OS X和iPhone流行起来之前,很多人都接触过Qt(一种基于C++语言的 GUI开发套件)。Qt对messaing的支持比Objective-C/Cocoa更彻底——每个object可以发出若干 signal,每个signal可以触发这个object自己的或者其他objects的若干个slot。 有意思的是,为了支持messaging,Qt对C++语言做了扩展,而Objective-C对C语言做了扩展。这 两套扩展都利用了起源于C语言的“宏”机制(macro)。类似的做法也可以用于Java,前提是我们在 调用Java编译器之前,先调用一下cpp宏展开程序来预处理一下我们的Java程序。这事儿可以留待 Java爱好者们来搞? 不管是Objective-C还是Qt,都会“尽力”去检查一个object是否支持一个method(或者叫 message),但是并不禁止程序员向一个object发送一个它不认识的message(或者调用一个object 没有的method)。说“尽力”检查,是因为两者都不能保证检查的完备性。这是因为Objective-C和 Qt都支持多态;具体的说,接受message的object可能是表示为一个指针(指向object),所以直到 运行时候,当一个message抵达某个object的时候,系统才能(通过查这个object对应的message list)知道这个object是否认识这个message。 这种灵活性在Google新推出的Go语言中也同样实现了,而且做的很极致——Go语言中没有class的 概念;换句话说,不需要是class类型的object才能有对应的方法(methods)——Go允许给几乎任 何类型附上methods。而且程序中可以很方便的检测一个object是否支持(一组)methods,比如:

type Stringer interface {
String() string
}
s, ok := v.(Stringer) // Test whether object v implements "String()"

【和Go的这种灵活性类似的,Objective-C允许给已经定义了的class增加一些methods,而不需要 derive subclass;Objective-C的这种机制被称为category。和Go不同的是,一个category是对某个 class的一个扩展,而Go语言里完全没有class了(但是有interface的概念)。】 说到这里,我觉得差不多可以反过来理解Robert Harper教授对OO的评价了——其实Robert不是在 藐视OO,而是在指责很多imperative OO languages(我理解包括Java和未经Qt扩展的C++;详见 后述),认为这些语言没有完成实现OO中object messaging的核心思想,从而不算实现了“模块化 “(modulization)的思想。 上述都是关于程序的模块化。实际上,模块化的另一个主要方面是对“数据”(data)的模块化。从图灵 机和lambda-calculus开始,计算机科学家们就注意到程序和数据是统一的;比如在冯诺依曼的“二进 制存储电子计算机”模型里,程序和数据都是bit stream。即时我们在讨论高级编程语言的时候,程序 和数据也不应该被分开。因为现代数据操作和模块化的基础是并行程序(parallelism),而有效实现并 行的基础是程序的first-class表达,也就是把程序作为一种基本数据类型。 鉴于这篇帖子已经很长了,这段话就作为下一篇帖子的提纲吧。下一篇帖子里,我们来说说 XML、JSON、MessagePack、Protocol Buffers这些persistent data structure,以及用源于古老的 functional programming paradigm的Go语言和MapReduce实现的并行数据操作。

面向对象编程方法的反思

“面向对象”是每个计算机系学生一定会接触到的概念,也是每个程序员一定会接触到的概念,连我学习测控专业的表弟都问我什么叫“面向对象”。对于这一概念今日的红火,恐怕连艾伦·C·凯创造它时也未曾想到。

面向对象思想提出“一切皆是对象”的说法,并以此应用于计算机程序设计中,提出了面向对象的三个基本要素:封装、继承和多态。 所谓封装,是指把事物抽象为类,暴露对外接口,隐藏内部实现。 所谓继承,是指利用现有类的功能,在无需重写原有类的情况下进行扩展。 所谓多态,是指将父类设置成为和起派生类对象相等的技术。简单说即允许将派生类类型的指针赋给父类类型的指针。

事实上,我们在最初接受此种编程范式时通常是默认了其正确性。之后便尽可能在实际中应用它,却鲜少提出过反对或是怀疑。当我们默认了其正确性之后,无论由此引起何种问题,我们都只会认为其不够完善或是自己使用不当,绝不会去质疑方法本身,因此,在面向对象遇到问题之后,便产生了例如设计模式这类理论和方法来掩盖其先天不足。

对于面向对象的问题,我发现了以下几点:

首先,面向对象方法使我们编写了大量本不必要的代码。例如,当我们要写一个swap函数交换两个变量的值,如果用C语言代码可以这样写:

void swap(int *px, int *py); // 函数体此处略过,这个应该是最基础的内容,想不起来可以翻阅K&R的《C程序设计语言》

而使用面向对象语言比如java则会是这样:

class ClassA{
    private int value;
    public ClassA(int i) {this.value=i;}
    public void setValue(int value){this.value=value;}
    public int getValue(){return this.value;}
    public static void swap(ClassA a, ClassA b){
        ClassA c=new ClassA(a.getValue());
        a.setValue(b.getValue());
        b.setValue(c.getValue());
    }
}

很明显,c代码要比java简洁得多。本来一个函数可以解决的问题,OO却告诉我们需要一个类,于是就多出了许多代码。我认为编程时一条重要的原则就即:no more, no less. 上述java代码很难使我们认为它是美的。

其次,面向对象方法在一些时候(其实说是很多时候也不为过)并不符合数学的思维方式,而至少当前我们使用的计算机还是基于数学的理论基础创造的。同样还是上述例子,当我们在数学中解决交换数值问题是该是如何思维的?当然是把要交换的内容作为参数传 入函数。正如Joe Armstrong所说,数学的思维方式中数据和函数时分开的。数学家们不会也不许要将这两者粘合在一起构成一个对象或是其他什么东西。当然,按照Martin Vilcans反驳Joe的文章《No, that’s not why oo sucks》中指出Joe并没有提出合理的解释来证明为何数据和函数应当分开。那么请思考以下情况:假设我们现在有两个类: Class A 和 Class B, 我们需要在A的实例中操作B实例中的某个数据,按照面向对象的设计,我们可以有这两种方式:

  1. 在A中创建B的实例,并访问B中响应函数来操作该数据
  2. 创建全局的B实例并在A中通过访问B实例public函数来操作数据 无论上述哪种方式,不仅增加了耦合,同时使代码失去美感,而如果我们分离了数据和函数,则避免了这些问题。

第三,在第三方库的使用方面,基于面向对象设计的通常时类库,而基于面向过程或函数式则通常为函数库。以函数方式实现的优势在于我们无需在自己的代码中添加多余的东西,对库的使用恰到好处,以类似黑盒的方式,传入参数,得到需要的返回值。

所以,面向对象的设计思想本身即存在着不少问题,它造成了耦合,却又创造出来解耦,这本身就是一件很无聊的事情,而使用者却常常对这些问题视而不见。而面向对象本身也就是把简单问题复杂化的典型。

这里所说的几点主要是提醒大家思考,我们不该一味接受而失去了独立思考的能力。这些是我的想法,还比较笼统,以后会进一步梳理。

参考 Why OO sucks —— Joe Armstrong No, that’s not why OO sucks —— Martin Vilcans

理解Erlang OTP gen_event

目录
A. gen_event作用
B. gen_event 与 gen_server 及 supervisor之间的关系
C. gen_event 细节描述
	1. 各函数作用、参数、返回值,哪几个函数是必需的
	2. 每个函数是使用方法
D. 一个具体的例子

理解Erlang OTP superivisor 行为模式

本文介绍OTP行为模式中的supervisor行为,即监督模式,此行为模式主要用于监督和管理服务器,在服务器崩溃后重启进程。 本文介绍监督者模式的原理和结构,具体示例代码请参考官方文档和《Erlang/OTP并发实战》一书。


进程树结构


监督者调用结构

module:start_link/0 —> supervisor:start_link/3 —> module:init/1.

其中 supervisor:start_link/3为如下形式:

supervisor:start_link({local, Name}, Module, Args);

supervisor:start_link({global, Name}, Module, Args).

该函数创建一个新的监督者进程。 其中参数如下:

{local, Name} 和 {global, Name} 与 gen_server行为模式中意义相同。
Module %% 为监督者模块名
Args %% 传入参数

新的监督者进程将调用监督者模块中的module:init([])函数以期返回{ok, StartSpec}.

init形式如下:

init(...) ->
	{ok, { {RestartStrategy, MaxR, MaxT}, [ChildSpec, ...] } }.

例如:

init(_Args) ->
	{ok, {one_for_one, 1, 60}, [{ch3, {ch3, start_link, []},
	permanent, brutal_kill, worker, [ch3]}]}.

参数说明:

MaxR %% 在MaxT时间内最大重启次数
MaxT %% 限定时间,单位为秒

最大重启频率:

如果在过去MaxT秒内重启次数超过了MaxR,监督者将会终止所有子进程然后终止自身进程。 当监督者进程终止时,下一优先级的监督者可能做如下操作:

  • 重启监督者
  • 终止监督者
  • 终止自身进程

重启策略:

在init/1 中返回值 RestartStrategy 表示监督者重启策略,其可能取值如下:

  • one_for_one %% 当一个进程终止时,只有它自身会被重启
  • one_for_all %% 当一个子进程终止时,所有子进程(包括该子进程本身)都将会被重启
  • rest_for_one %% 当一个子进程终止时,所有“剩余的”子进程和它本身将本重启

子进程规范:

子进程规范是一个元组,用来描述受监督的进程。该元组结构如下:

{Id, StartFunc, Restart, Shutdown, Type, Modules}

其中参数:

Id %% 监督者在系统内部识别子进程规范的名称,一般使用模块名
StartFunc %% 用于启动进程的三元组 {M, F, A}
Restart %% 指定子进程何时被重启
	取值为:
	- permanet %% 始终重启
	- temporary %% 永不重启
	- transient %% 非正常终止时重启
Shutdown %% 指明如何终止进程
	取值为:
	- brutal_kill %% 调用 exit(Child, kill)立即终止子进程
	- 整型数值 %% 监督者告知子进程调用 exit(Child, shutdown)来终止,
	  之后等待退出信号返回,如果在指定时间内未收到退出信号,则调用exit(Child, kill)
	- infinity %% 通常表示子进程也是一个监督进程的情况,监督者将给子进程留出足够的时间来终止
Type %% 表示子进程是监督者(supervisior)还是工作者(worker)
Modules %% 所依赖模块名称的列表

添加子进程:

我们可以通过调用以下函数来实现向一个静态监督树中田间动态子进程:

supervisor:start_child(Sup, ChildSpec).

其中: sup %% 进程pid或监督者名称 ChildSpec %% 子进程规范

注意:当监督者进程消亡并重启后,所有动态添加的子进程将会丢失。

终止子进程:

调用以下函数可以终止任何动态或静态的子进程:

supervisor:terminate_child(Sup, Id).

终止后的子进程将会有以下函数删除:

supervisor:delete_child(Sup, Id).

其中:

Id %% 指在子进程规范中定义的Id

同样,删除的动态添加的子进程将在监督者重启后丢失。


Simple_One_For_One 监督者: 当监督者的子进程重启策略设置为 simple_one_for_one时,所有子进程都将是动态添加的。此时,当监督者启动时不会启动任何子进程,所有子进程都将由以下函数动态添加:

supervisor:start_child(Sup, List).

应用的终止:

监督者是监督树的一部分,所以它将会自动被其监督者终止。 关闭时,监督者将会按照启动子进程的逆序根据关闭规范终止所有子进程,最后终止自身进程。



理解Erlang OTP gen_server行为模式

OTP的gen_server行为模式,在《Erlang程序设计》和《Erlang/OTP并发编程实战》两本书里都主要介绍如何使用,至于其中原理提到的并不多,以下内容主要介绍工作原理,主要来自官方的文档。

######总览######

结构:

  • 通用服务器模块 gen_server

  • 回调模块 module(例如:my_bank.erl)


调用关系:

通用服务器模块						回调模块

gen_server:start_link            ------------>             Module:init/1

gen_server:call
gen_server:multi_call            ------------>             Module:handle_call/3

gen_server:cast
gen_server_abcast                ------------>             Module:handle_cast/2

--                               ------------>             Module:handle_info/2
--                               ------------>             Module:terminate/2
--                               ------------>             Module:code_change/3

当回调函数调用失败或返回错误值时,gen_server会中止。 gen_server不会自动跟踪退出信号。


######gen_server函数详解######

下面看看各函数的参数和返回值:

start_link(Module, Args, Options) -> Result
start_link(ServerName, Module, Args, Options) -> Result

参数类型:

ServerName = {local, Name}  %% 在本地将gen_server注册为Name
| {global, GlobalName}     %% 注册全局名称GlobalName
| {via, Module, ViaName}    %% 使用模块所代表的注册入口进行注册

其中:

Module 为原子(atom), Args为数据项(term), Options是由选项构成的列表[Option],
可取的值为:
{debug, Dbgs}         %% 调试
| {timeout, time}     %% 设置超时
| {spawn_opt, Sopts}  %% 将Sopts选项列表传递给spawn_opt,创建一个新的gen_server进程

参数 Dbgs 为一个列表[Dbg], 可能的取值如下:
Dbg = trace | log | statistics | {Log_to_file, FileName} | {install, {Func, Function}}

Sopts = [term()]

start_link可能的返回值如下:
{ok, Pid} | ignore | {error, Error}
其中Pid为进程id(pid())
失败时,Error的取值为
{already_started, Pid}  %% 进程已启动
| term()

前面已经说过,gen_server:start_link会调用回调模块的init/1函数,该函数在出错时会返回{error, Reason}.

另一种启动方式是

start(Module, Args, Options) -> Result
start(ServerName, Module, Args, Options) -> Result

这两种方式的区别在于: start_link创建的进程将作为监控树的一部分;而start创建的进程则作为一个独立的gen_server进程,它没有相应的监督者。 深入内容请参阅监督者行为模式的相关内容。

call(ServerRef, Request) -> Reply
call(ServerRef, Request, Timeout) -> Reply

call函数会通过向gen_server的ServerRef发送请求进行同步调用,也就是说发送消息后该函数会等待直到收到回应或超时。gen_server模块会调用回调函数 Module:handle_call/3 来处理请求。

ServerRef可以取以下值:

- Name, 当gen_server在本地注册时
- {Name, Node}, 当gen_server全局注册时
- {via, Module, ViaName}, 当gen_server通过可选的进程注册入口注册时

Request可以是任意Erlang数据项,它会被作为参数传递给 Module:handle_call/3.

参数Timeout代表超时的时间,以毫秒为单位,取任意大于0的整数,或者infinity表示永不超时.

返回值 Reply 由回调函数 handle_call/3定义.
cast(ServerRef, Request) -> ok

cast函数与call类似,其区别是call进行同步调用,而cast则是异步调用,即cast将立即返回,不论目标节点或通用服务器是否存在。其参数的类型也和call相同。

reply(Client, Reply) -> Result.

当回应无法在Module:handle_call/3中定义时,reply会向调用call或multi_call函数的客户端明确发送一个返回消息。

Client必须是想回调函数提供的From参数
Reply = term()   %% Reply是Erlang数据项
Result = term()  %% Result是Erlang数据项
enter_loop(Module, Options, State)
enter_loop(Module, Options, State, ServerName)
enter_loop(Module, Options, State, Timeout)

enter_loop函数在通用服务器中调用已存在的进程,此函数不返回任何值。调用的进程将会进入通用服务器的接收消息循环中并成为一个通用服务器。其中参数与前面介绍函数中同名参数类型相同,此函数通常在比gen_server行为模式所提供的初始化更加复杂的情况下使用。


######回调函数######

Modult:init(Args) -> Result

gen_server:start或gen_server:start_link函数将会调用此函数进行初始化操作。其参数类型如下

Args = term()  %% 启动函数所需的参数
Result = {ok, State} | {ok, State, Timeout} | {ok, State, hibernate} | {stop, Reason} | ignore
State = term()
Timeout = int() >= 0 | infinity
Reason = term()

其中hibernate参数表示进程将会进入休眠状态等待下一条消息到来(调用 proc_lib:hibernate/3)。 如果初始化失败,将会返回{ok, Reason}。

注: init/1 函数返回值{ok, State}中的State即进程状态实际上是一个LoopData,可以为任何值,
包括记录(-reocrd), ets表, 字典(dict)等, 该值将在各函数之间作为参数传递表示系统状态。
Module:handle_call(Request, From, State) -> Result

gen_server:call或gen_server:multi_call将会调用此函数来处理请求。其中参数类型:

Request = term()
From = {pid(), Tag}
State = term()
Result = {reply, Reply, NewState} | {reply, Reply, NewState, Timeout} 
| {reply, Reply, NewState, hibernate} | {noreply, NewState, Timeout} 
| {noreply, NewState, hibernate} | {stop, Reason, Reply, NewState} 
| {stop, Reason, NewState}
Reply = term()
NewState = term()
Timeout = int() >= 0 | infinity
Reason = term()

当此函数返回{reply, …}时,响应Reply将会发送给From参数所指的进程;返回{noreply, …}时,gen_server将会以新状态NewState继续执行,如果需要返回响应,则必须显式调用gen_server:reply/2。 返回{stop, Reason, Reply, NewState}时,函数会把Reply返回给From的进程,{stop, Reason, NewState}则需要明确调用gen_server:reply/2返回。然后gen_server会调用Module:terminate(Reason, NewState}。

Module:handle_cast(Request, State) -> Result.

如前所述,gen_server:cast/2 或 gen_server:abcast/2, 3将会调用此函数来处理发来的请求。其参数与handle_call中相似。

Module:handle_info(Info, State) -> Result.

gen_server收到同步和异步以外的消息或系统消息以及超时的情况下会调用此函数,其参数即返回如下:

Info = timeout | term()
State = term()
Result = {noreply, NewState} | {noreply, NewState, Timeout} 
| {noreply, NewState, hibernate} | {stop, Reason, NewState}
NewState = term()
Timeout = int() >= 0 | infinity
Reason = normal | term()
Module:terminate(Reason, State)

gen_server将要终止时会调用此函数,它的工作与Module:init/1相反,同时进行其他必要的清理工作。其参数:

Reason = normal  %% 正常终止
| shutdown %% 当设置了跟踪退出信号 或 监督者子进程定义了特定的整数超时值时
| {shutdown, term()}
| term()
State = term()

任何 normal, shutdown, {shutdown, Term} 以外原因出现时,gen_server都会假定出错并用error_logger:format/2报告错误。

Module:code_change(OldVsn, State, Extra) -> {ok, NewState} | {error, Reason}

当版本更新时会使用此函数进行模块热替换,其中参数比较简单: OldVsn = Vsn | {down, Vsn} Vsn = term() State = NewState = term() Extra = term() Reason = term()

成功时此函数会返回更新后的内部状态,如果由于某种原因出错,升级将会失败并回滚到之前状态。

Module:format_status(Opt, [PDict, State]) -> Statue.

此函数是可选的,当请求服务器状态或异常终止并记录错误日志是gen_server进程会调用它。其参数如下:

Opt = normal | terminate
PDict = [{Key, Value}]
State = term()
Status = term()

PDict是gen_server进程字典的当前值,State是gen_server的内部状态。


######小结######

综上所述,gen_server行为模式的基本流程如下: 回调模块的start调用gen_server:start_link(或gen_server:start)来启动服务器,并进入主循环,在回调模块的接口函数(处理具体的业务逻辑)中调用gen_server的call或cast来处理请求,在gen_server的call和cast中进一步调用回调模块中的handle_call和handle_cast来执行具体的业务逻辑处理,回调模块的terminate来终止进程。

其中通用服务器模块和回调模块的接口函数在上面做了简单说明,具体各个函数内部细节如何实现以及调用了哪些相关库函数请查阅官方文档。