笔记系列

Erlang环境和顺序编程
Erlang并发编程
Erlang分布式编程
Yaws
Erlang/OTP

日期              变更说明

2014-11-02 A outline

2014-11-03 A 1

2014-11-08 A 2 3

2014-11-17 A 4

Agenda

0 范围

Erlang的现实世界建模方式

Erlang进程创建

Erlang进程设计模式

Erlang进程错误处理

1 Erlang Concurrency Modeling Philosophy

Armstrong在[2]中跳出程序语言在处理并发应用场景时洋洋洒洒的急迫性、跃跃欲试的一站式解决方案阐述桎梏,创造性的提出“暂时忘却编程,思考一下现实世界里发生着什么”问题。

解决问题一直是CS科学家、IT工程师标榜的影响现实世界的方式,自计算机科学被质疑是否应该是一门学科/科学开始。图灵机、冯▪诺依曼机器,还有我不理解的量子机器等,真的是仿生学的产物或是对现实世界的模拟吗,但她们确确实实的成功了,并影响着领域内参与者的思维。用natural 还是artificial,似乎是taste的问题。不扯了!

将Armstrong的阐述摘录如下:

我们理解并发。

世界是并行的。

Erlang程序反映了我们思考和交流的方式。

人类表现为独立的个体,通过发送消息进行交流。

如果有人死亡,其他人会注意到。

Erlang concurrency constructor overview

Constructor

Description

self()

返回所在执行进程的标识符

Pid = spawn(Mod, Func, Args)

创建进程,该进程执行Mod:Func(Args)

Pid ! Message

向进程发送消息

receiveend

接收消息

register(AnAtom, Pid)

注册进程

unregister(AnAtom)

移除进程注册名

whereis(AnAtom) -> Pid | undefined

检测进程注册名

registered() -> [AnAtom::atom()]

系统中所有注册进程的列表

link(Pid)

调用进程与Pid之间设置双向链接

unlink(Pid)

移除链接

spawn_link(Mod, Func, Args)

原子操作:生成进程并设置双向链接

spawn_monitor(Mod, Func, Args)

元组操作:生成进程并设置调用进程与新进程间的监控

process_flag(trap_exit, Flag)

将调用进程设置为系统进程

Ref = erlang:monitor(process, Pid)

生成对Pid的单向监控

erlang:demonitor(Reference)

移除监控

erlang:demonitor(Reference, [flush])

移除监控,并清空监控进程邮箱中相关信息

exit(Reason)

调用进程以原因Reason终止

exit(Pid, Reason)

向进程Pid发送退出信号

2 创建进程

►Sample: echo server[1]

-module(echo).

%% API
-export([start/0, loop/0]).

%%%===================================================================
%%% API
%%%===================================================================

start() ->
    %Pid = spawn(echo, loop, []),
    %Pid ! {self(), hello},
    register(echo, spawn(echo, loop, [])),%◄注册新生成的进程
    echo ! {self(), hello},%◄向进程发送消息,消息不限于tuple,可以是任一term
    receive      %◄当前进程阻塞式接收消息
         %{Pid, Message} ->
         %    io:format("~w~n", [Message])
         {_Pid, Message} ->
             io:format("~w~n", [Message])
    %end,
    %Pid ! stop.
    end.

loop() ->
    receive
         {From, Message} ->
             From ! {self(), Message},
             loop();%◄尾递归
         stop ->
             true
    end.

%%%===================================================================
%%% Internal functions
%%%===================================================================

终端进程创建并注册echo进程后阻塞式接收消息;echo进程模式匹配接收消息,loop子句1中使用尾递归方式保持活跃。

没有找到其他合适的进程创建、执行交互的建模表示方式,暂以OO时序图表示:

执行

注意需要将创建进程用到的函数loop导出,否则会抛出如下错误:

原因嘛,自然是erts需要这些信息(MFA)来生成新进程。

一些有用的终端命令

Command

Description

pid(x,y,z)

在终端中生成进程<x,y,z>的标识符

flush/0

检索和显示所有发送到终端进程信箱中的消息,同时删除信箱中的消息

i()

输出当前运行时系统(ert)正在执行的进程,包括进程标识符、生成进程的函数

processes()

输出所有系统中运行的进程pid列表

regs()

输出所有已注册的进程

Erlang进程构造

Erlang的并发是基于进程的,这里进程隶属于Erlang语言,而不是底层操作系统中的进程。Erlang的进程是一些独立的小型虚拟机,可以执行Erlang函数[2]。

Erlang进程约束/特性

创建和销毁进程是非常快速的;

在进程间发送消息是非常快速的;

进程在所有操作系统中具有相同的行为方式;

可以拥有大量进程;

进程不共享任何内存,是完全独立的;

进程唯一的交互方式是消息传递。

基本函数

(1) spawn(Module, Function, Args) -> Pid

创建新的进程(子进程),新进程执行apply(Module, Function, Args)。子进程与父进程一起运行,Function(Args)必须导出。Spawn返回子进程的进程标识符Pid,可以用Pid给该进程发送消息。

新进程创建后,会使用最新的代码定义模块(有关Erlang软件升级/动态代码升级的概念,以后再阐述)。

(2) spawn(Function) -> Pid

创建新进程执行Function()。新进程总是使用被执行fun的当前值,fun不需要从模块中导出。

(3) Pid ! Message

向进程Pid异步的发送消息Message。该函数的返回结果为Message。

Pid1 ! Pid2 ! … !PidN ! Message的含义是Pid1! (Pid2 ! (… !(PidN ! Message)))。

以这种方式发送消息永远不会失败,但发送消息到不存在的进程会导致badarg错误,调用进程会结束执行。

(4) receive … end

(4.1)基本接收

语法

receive

Pattern1 [when Guard1] -> Expressions1;

Pattern2 [when Guard2] -> Expressions2;

end

语义

消息达到进程信箱后,依次尝试匹配Patterni [when Guardi],如果匹配则执行Expressionsi;否则消息驻留于进程信箱中以供以后处理,进程会开始等待下一条消息。(每个进程有一个随其创建而同步创建的信箱。)

(4.2)带超时的接收

语法

receive

Pattern1 [when Guard1] -> Expressions1;

Pattern2 [when Guard2] -> Expressions2;

after Time ->

Expressions

end

语义

进程进入接收表达式Time毫秒后,仍没有收到匹配的消息,进程停止等待消息,开始执行Expressions。Time为infinity时,回退为不带超时的接收。

(5)(un)register

register(atom(), Pid)

用原子作为Pid的注册名称,系统中任何进程可以通过该注册名与进程通信。

unregister(atom())

移除注册名称,进程终止后自动取消注册。

(6) lookup

whereis(atom()) -> Pid | undefined

检查名称是否被注册。

registered() -> [atom()]

返回系统中所有注册进程的名称列表。

进程的行为模式

父进程用一系列参数值作为子进程的初始状态,创建子进程。子进程携带状态数据,执行一个REPL(read evaluate print loop)函数保持活跃(例:echo中的loop)。在REPL函数中,模式匹配进程信箱中的消息,如果匹配,则做相应的内部处理、更新状态数据或结束进程活动;如果不匹配,则消息驻留于信箱中,或者执行错误处理函数来解决错误的消息发送问题。一般的,stop消息是通知子进程停止的信号。

这里堆砌Erlang进程创建构造的解释(来源:ERTS Reference Manual, Module erlang),目的是指出在基本函数/构造基础上Erlang的goose swimming,包括错误处理、一些特殊的消息传送方式;另一方面是提醒自己,在喊出”Huston, we have a problem!”之前,可以在文档中自己找到一些答案。

(1) spawn(Module, Function, Args)

Module = module()

Function = atom()

Args = [term()]

返回应用apply(Module, Function, Args)启动的新进程的pid。新创建的进程放入系统调度队列以待执行。

如果Module:Function/Arity不存在,新进程执行error_handler:undefined_function(Module, Function, Args)。这个错误处理器可以重新定义(见process_flag/2)。如果error_handler未定义,或用户重定义的error_handler未定义,则产生原因为undef的失败。

(2) register(RegName, PidOrPort) -> true

RegName = atom()

PidOrPort() = port() | pid()

将名称RegName与pid或端口标识符关联。RegName必须是atom,在发送操作符(!)中替代pid或端口标识符。

如果PidOrPort不存在、RegName已被使用、进程或端口已被注册、RegName是undefined时产生badarg失败。

Erlang除发送消息操作符(!)外,还支持一些特殊的消息发送方式,如下(完整的方式可详阅文档)

(3) erlang:send(Dest, Msg) -> Msg

Dest = dst()

Msg = term()

dst() = pid() | port() | (RegName::atom()) | {RegName::atom(), Node::node()}

与Dest ! Msg一致,发送消息Msg,返回值为Msg。

Dest可以是远程或本地pid、本地端口、本地注册名称、或另一个节点上的注册名称{RegName, Node}。

(4) erlang:send(Dest, Msg, Options) -> Res

Dest = dst()

Msg = term()

Options = [nosuspend | noconnect]

Res = ok | nosuspend | noconnect

dst() = pid() | port() | (RegName::atom()) | {RegName::atom, Node::node()}

发送消息,返回ok或不发送消息返回下面的选项值,除此之外与erlang:send/2相同。

选项的含义

nosuspend 如果发送者需要暂停来发送,直接返回nosuspend

noconnect 如果发送前目标节点需要自动连接,直接返回noconnect

(5) erlang:send_after(Time, Dest, Msg) -> TimerRef

Time = integer() >= 0 (0<= Time <= 4294967295)

Dest = pid() | atom()

Msg = term()

TimerRef = reference()

启动定时器,在Time毫秒后发送消息。

如果Dest是pid,pid必须是本地进程,进程pid不活跃或退出时,该定时器自动取消(该特性在erts 5.4.11中引入)。

如果Dest是atom,则应该是已注册进程的名称,该名称在传送消息时查找,如果名称未指向某个进程则产生错误。定时器不会自动取消。

参数不符合上述要求时产生badarg失败。

3 进程错误处理

Erlang关于构建容错式软件的基本思路是[2]:

(1) 让其他进程修复错误

应用中的进程相互监控的活跃状况,如果某个进程挂了,其他进程能够知道这一点,并采取相应的措施;

(2) 任其崩溃

将应用程序代码分为两部分:一部分负责解决问题(即关注于应用逻辑)、另一部分负责前一部分出现问题时纠正他们。纠正错误的代码一般是通用的、独立于应用的。

Erlang进程错误处理术语

(1) 系统进程

两类进程:普通进程、系统进程。spawn生成的进程是普通进程,普通进程调用BIF process_flag(trap_exit, true)变为系统进程(捕获退出信号)。

系统进程接收到退出信号时,会将信号转换为普通的{‘EXIT’, From, Reason}消息;而普通进程接收到错误信号时,如果退出信号原因不为normal会结束执行,并将退出信号传播到它链接的所有进程。

(2) 链接(link)

进程间相互链接,如果一个挂了,会向另一个发送错误信号。

(3) 监视(monitor)

单向的链接,如果被监视进程挂了,会向监视进程发送’DOWN’消息。

(4) 错误信号

进程间协作的方式是传递消息或错误信号。消息是通过send(!)发送的,错误信号是进程崩溃或正常终止时自动发送的。

基本错误处理函数

(1) link(Pid)

调用进程与Pid设置双向链接。

如果Pid不存在,抛出noproc退出异常。

多次调用没有副作用。

(2) monitor(process, PidOrRegName) -> Reference

生成对进程Pid/RegName的单向监控。

如果调用是,PidOrRegName已经终止或不存在,则会向监视进程发送退出原因是noproc的’DOWN’消息。

多次调用生成不同的Reference。

‘DOWN’消息的格式[Module erlang doc]

{‘DOWN’, Reference, process, Object, Info}

Object: 被监视对象的引用,Info:进程退出原因、noproc(不存在的进程)或noconnection(无法连接节点)。

(3) process_flag(trap_exit, Flag)

将当前进程的退出信号转换为退出消息。

(4) spawn_link spawn_monitor

创建进程和建立链接/监视的原子操作。

(5) unlink demonitor

解除链接/监视关系。

(6) exit

如果不在catch/try中调用exit(Reason),当前进程会结束执行,并向其链接的所有进程传播退出信号{‘EXIT’, From, Reason}。Reason可以是任意term。

exit(OtherPid, Reason)向OtherPid进程/端口发送原因为Reason的退出新信号,不同的Reason时OtherPid的行为:

(a) Reason不是normal或kill

如果OtherPid不捕获退出信号,将以Reason退出;否则,退出信号转换为消息{‘EXIT’, From, Reason}加入OtherPid的信箱(消息队列),From是发送退出信号的进程;

(b) Reasone是normal

OtherPid不会退出,如果捕获退出信号,退出信号转换为消息{‘EXIT’, From, normal}并将如OtherPid的信箱;

(c) Reason是kill

OtherPid以killed原因无条件结束执行。

4 进程设计模式

文[3]中指出,在出现OTP之后,使用原生的进程和消息传递编写Erlang应用代码,反而变为一个高级话题。当然,借助框架、基础设施来解决常见的应用开发问题、甚至捎带保证了产品级代码的高可靠性和性能等要求,但是必要的时候,make hands dirty才是必杀技。(想起了Doug Lee大叔4章的 Java并发编程:设计原则和模式,我所认识的Java程序员几乎没有读过这本书,SSH就像煎饼包一切一样似乎可以解决所有问题,真的吗?)

[1]中列出了四种最常见的进程设计模式:

(1) 客户端/服务器

通常应用于资源分配的进程。服务端代码和客户端代码位于同一module中,通信格式契约在接收消息模式匹配中体现。

(2) 有限状态机(finite state machine ,FSM)

需要维护一个活动的状态,状态的变迁通过事件(消息)触发。

(3) 事件句柄(event handler)

接收特定类型的消息,再做相应的处理。概念上与OO设计模式中Publisher/Observer基本一致。

(4) 监控进程(supervisor)

建立和维护应用中进程结构树,监控进程负责启动工作进程并监控。

监控进程的通用行为是启动工作进程,监控他们,在必要时重新启动工作进程;

监控进程的特定行为是何时、如何启动和重新启动工作进程。

通用行为和特定行为的区分与OO设计模式中template method方法的思想不谋而合。

►(1)Sample: frequency[1]

-module(frequency).

%% API
-export([start/0, stop/0, allocate/0, deallocate/1]).
-export([init/0]).

%%%===================================================================
%%% API
%%%===================================================================
% server side function
start() ->
    register(frequency, spawn(frequency, init, [])).

init() ->
    Frequencies = {[10,11,12,13,14,15],[]},
    loop(Frequencies).

% client side function
stop() ->
    call(stop).
allocate() ->
    call(allocate).
deallocate(Freq) ->
    call({deallocate, Freq}).

call(Message) ->
    frequency ! {request, self(), Message},
    receive
    {reply, Reply} -> Reply
    end.

loop(Frequencies) ->
    receive
    {request, Pid, allocate} ->
        {NewFrequencies, Reply} = allocate(Frequencies, Pid),
        reply(Pid, Reply),
        loop(NewFrequencies);
    {request, Pid, {deallocate, Freq}} ->
        NewFrequencies = deallocate(Frequencies, Freq),
        reply(Pid, ok),
        loop(NewFrequencies);
    {request, Pid, stop} ->
        reply(Pid, ok)
    end.

reply(Pid, Reply) ->
    Pid ! {reply, Reply}.

%%%===================================================================
%%% Internal functions
%%%===================================================================
% the helper function used to allocate and deallocate frequencies
allocate({[], Allocated}, _Pid) ->
    {{[], Allocated}, {error, no_frequency}};
allocate({[Freq|Free], Allocated}, Pid) ->
    {{Free, [{Freq, Pid} | Allocated]}, {ok, Freq}}.
deallocate({Free, Allocated}, Freq) ->
    NewAllocated = lists:keydelete(Freq, 1, Allocated),
    {[Freq|Free], NewAllocated}.

执行

描述

服务端持有频率(frequency)资源,客户端申请分配频率,并在不使用时很合作的释放频率。

有一点很奇怪,客户端可以关闭服务端。这里出于演示的目的,只有从终端(客户端)中关闭Server。

消息封装,客户端代码没有必要暴露任何内部通信协议细节,客户端可见的是工作的简单的API。

lists:keydelete(Module lists)

keydelete(Key, N, TupleList1) -> TupleList2

Types:

Key = term()

N = integer() >= 1

1..tuple_size(Tuple)

TupleList1 = TupleList2 = [Tuple]

Tuple = tuple()

Returns a copy of TupleList1 where the first occurrence of a tuple whose Nth element compares equal to Key is deleted, if there is such a tuple.

如果元组列表TupleList1中存在这样的元组,第N个元素等于Key,返回删除该元组后的TupleList1。

最后,启动两个终端产生两个运行时系统,证据如下:

►(2)Sample: phone[1]

Left blank

[1]中列举的固定电话示例在当前来看是一个很大很大的坑,经过一番折腾后,发觉需要纳入考虑的活动体包括:两个人/terminal、两个电话、电话线路broker,而这些放在分布式编程中更为合适:

故,这里仅提示一下,将每个状态表示为函数,函数体中receive状态迁移消息并做相应处理,状态迁移以尾部调用另一个状态函数完成。

I promise, I shall return! J

►(3)Sample: event_manger[1]

event_manger.erl

-module(event_manager).

%% API
-export([start/2, stop/1]).
-export([add_handler/3, delete_handler/2, get_data/2, send_event/2]).
-export([init/1]).
%%%===================================================================
%%% API
%%%===================================================================

start(Name, HandlerList) ->
    register(Name, spawn(event_manager, init, [HandlerList])),
    ok.

init(HandlerList) ->
    loop(initialize(HandlerList)).

stop(Name) ->
    Name ! {stop, self()},
    receive {reply, Reply} -> Reply end.

add_handler(Name, Handler, InitData) ->
    call(Name, {add_handler, Handler, InitData}).
delete_handler(Name, Handler) ->
    call(Name, {delete_handler, Handler}).
get_data(Name, Handler) ->
    call(Name, {get_data, Handler}).
send_event(Name, Event) ->
    call(Name, {send_event, Event}).

%%%===================================================================
%%% Internal functions
%%%===================================================================
loop(State) ->
    receive
    {request, From, Msg} ->
        {Reply, NewState} = handle_msg(Msg, State),
        reply(From, Reply),
        loop(NewState);
    {stop, From} ->
        reply(From, terminate(State))
    end.

initialize([]) -> [];
initialize([{Handler, InitData} | Rest]) ->
    [{Handler, Handler:init(InitData)} | initialize(Rest)].

terminate([]) -> [];
terminate([{Handler, Data} | Rest]) ->
    [{Handler, Handler:terminate(Data)} | terminate(Rest)].

handle_msg({add_handler, Handler, InitData}, LoopData) ->
    {ok, [{Handler, Handler:init(InitData)} | LoopData]};
handle_msg({delete_handler, Handler}, LoopData) ->
    case lists:keysearch(Handler, 1, LoopData) of
    false ->
        {{error, instance}, LoopData};
    {value, {Handler, Data}} ->
        Reply = {data, Handler:terminate(Data)},
        NewLoopData = lists:keydelete(Handler, 1, LoopData),
        {Reply, NewLoopData}
    end;
handle_msg({get_data, Handler}, LoopData) ->
    case lists:keysearch(Handler, 1, LoopData) of
    false ->
        {{error, instance}, LoopData};
    {value, {Handler, Data}} ->
        {{data, Data}, LoopData}
    end;
handle_msg({send_event, Event}, LoopData) ->
    {ok, event(Event, LoopData)}.

event(_Event, []) -> [];
event(Event, [{Handler, Data} | Rest]) ->
    [{Handler, Handler:handle_event(Event, Data)} | event(Event, Rest)].

call(Name, Msg) ->
    Name ! {request, self(), Msg},
    receive {reply, Reply} -> Reply end.

reply(To, Msg) ->
    To ! {reply, Msg}.

io_handler.erl

-module(io_handler).

%% API - callbacks
-export([init/1, terminate/1, handle_event/2]).

%%%===================================================================
%%% API
%%%===================================================================

init(Count) ->
    Count.

terminate(Count) ->
    {count, Count}.

handle_event({raise_alarm, Id, Alarm}, Count) ->
    print(alarm, Id, Alarm, Count),
    Count+1;
handle_event({clear_alarm, Id, Alarm}, Count) ->
    print(clear, Id, Alarm, Count),
    Count+1;
handle_event(_Event, Count) ->
    Count.

%%%===================================================================
%%% Internal functions
%%%===================================================================
print(Type, Id, Alarm, Count) ->
    Date = fmt(date()),
    Time = fmt(time()),
    io:format("#~w,~s,~s,~w,~w,~p~n",
         [Count, Date, Time, Type, Id, Alarm]).

fmt({AInt, BInt, CInt}) ->
    AStr = pad(integer_to_list(AInt)),
    BStr = pad(integer_to_list(BInt)),
    CStr = pad(integer_to_list(CInt)),
    [AStr, $:, BStr, $:, CStr].

pad([M1]) -> [$0, M1];
pad(Other) -> Other.

log_handler.erl

-module(log_handler).

%% API - callbacks
-export([init/1, terminate/1, handle_event/2]).

%%%===================================================================
%%% API
%%%===================================================================

init(File) ->
    {ok, Fd} = file:open(File, write),
    Fd.

terminate(Fd) ->
    file:close(Fd).

handle_event({Action, Id, Event}, Fd) ->
    {MegaSec, Sec, MicroSec} = now(),
    io:format(Fd, "~w,~w,~w,~w,~w, ~p~n",
             [MegaSec, Sec, MicroSec, Action, Id, Event]),
    Fd;
handle_event(_, Fd) ->
    Fd.

%%%===================================================================
%%% Internal functions
%%%===================================================================

运行示例

按照Publisher/Observer设计模式,publish的事件(send_event)被所有Observer(io_handler, log_handler)接收并处理。

►(4)Sample: my_supervisor[1]

my_supervisor.erl 监控者/进程

-module(my_supervisor).

%% API
-export([start_link/2, stop/1]).
-export([init/1]).

%%%===================================================================
%%% API
%%%===================================================================

start_link(Name, ChildSpecList) ->
    register(Name, spawn_link(my_supervisor, init, [ChildSpecList])),
    ok.

init(ChildSpecList) ->
    process_flag(trap_exit, true),
    loop(start_children(ChildSpecList)).

stop(Name) ->
    Name ! {stop, self()},
    receive {reply, Reply} -> Reply end.

%%%===================================================================
%%% Internal functions
%%%===================================================================
start_children([]) -> [];
start_children([{M, F, A} | ChildSpecList]) ->
    case (catch apply(M, F, A)) of
    {ok, Pid} ->
        [{Pid, {M, F, A}} | start_children(ChildSpecList)];
    _ ->
        start_children(ChildSpecList)
    end.

loop(ChildList) ->
    receive
    {'EXIT', Pid, _Reason} ->
        NewChildList = restart_child(Pid, ChildList),
        loop(NewChildList);
    {stop, From} ->
        From ! {reply, terminate(ChildList)}
    end.

restart_child(Pid, ChildList) ->
    {value, {Pid, {M, F, A}}} = lists:keysearch(Pid, 1, ChildList),
    {ok, NewPid} = apply(M, F, A),
    [{NewPid, {M, F, A}} | lists:keydelete(Pid, 1, ChildList)].

terminate([{Pid, _} | ChildList]) ->
    exit(Pid, kill),
    terminate(ChildList);
terminate(_ChildList) ->
    ok.

add_two.erl 工作者

-module(add_two).

%% API
-export([start/0, request/1, loop/0]).

%%%===================================================================
%%% API
%%%===================================================================

start() ->
    process_flag(trap_exit, true),
    Pid = spawn_link(add_two, loop, []),
    register(add_two, Pid),
    {ok, Pid}.

request(Int) ->
    add_two ! {request, self(), Int},
    receive
    {result, Result} ->  Result;
    {'EXIT', _Pid, Reason} -> {error, Reason}
    after 1000 -> timeout
    end.

loop() ->
    receive
    {request, Pid, Msg} ->
        Pid ! {result, Msg + 2}
    end,
    loop().

%%%===================================================================
%%% Internal functions
%%%===================================================================

工作者运行示例

监控模式运行示例

局限性

监控进程应该托管工作进程的创建。

Now, let us face the crucial distributed world!

参考文献

[1] Cesarini F., Thompson S.著,慕尼黑Isar工作组 杨剑译.

Erlang编程指南.

北京: 机械工业出版社.2011.

[2] Armstrong J.著,牛化成 译.

Erlang程序设计(2).(Programming Erlang, Second Edition – Software for a Concurrent World).

北京: 人民邮电出版社.2014.

[3] Logan M., Merritt E., Carlsson R.著,连城 译.

Erlang/OTP并发编程实战.(Erlang and OTP in Action).

北京: 人民邮电出版社.2012.

最新文章

  1. Java多线程10:ThreadLocal的作用及使用
  2. MySql5.7.12设置log-bin
  3. NGUI之自适应屏幕
  4. Gradle学习系列之八——构建多个Project
  5. 第二篇、Maven快速上手
  6. 机器学习实验报告:利用3层神经网络对CIFAR-10图像数据库进行分类
  7. 自己做站点(二) 20块钱搞定一个企业站:域名&amp;amp;空间申请
  8. Struts2框架的基本使用(二)
  9. video+ audio
  10. 关于 char 、 wchar_t 、 TCHAR 、 _T() ||| 宏 _T 、 TEXT 、 _TEXT 、 L
  11. Node.js的下载、安装、配置、Hello World、文档阅读
  12. APIO dispatching
  13. IntelliJ IDEA 2017版 spring-boot2.0.4+mybatis反向工程;mybatis+springboot逆向工程
  14. CSS 基础 例子 背景色 &amp; 背景图片
  15. ios逆向工程-内部钩子(Method Swizzling)
  16. MongoDB 刷新几次就报错
  17. aoj0121
  18. ajax设置自定义头
  19. React脚手架
  20. HAproxy 源码包安装

热门文章

  1. CentOS7网卡的命名规则
  2. 14.KVM安装之脚本和镜像目录树准备
  3. 程序设计入门——C语言 第2周编程练习 1时间换算(5分)
  4. 关于 RxJava 技术介绍
  5. nodejs的第二天学习笔记
  6. Excel 2013中单元格添加下拉列表的方法
  7. Laravel Container分析
  8. windows添加虚拟网卡
  9. iis 301重定向
  10. Objective-C( protocol协议)