理查德·卡洛斯 ( Richard Kallos)
Wrek是我为同时执行任务依赖图而编写的Erlang库。 目的是运行一组预定义的任务,它们之间有部分排序。 在这篇文章中,我解释了为什么写wrek,可以用于什么以及如何使用它。
动机
Wrek是由两种截然不同的力量产生的。 首先,我对图论的业余爱好使我尝试在任何可能的地方查看图,从而为该库奠定了概念基础。 其次,我意识到我在Adgear上从事的项目将从这种库中受益,这使我最终开始编写Wrek。
概念性
该图是计算中的普遍数据结构。 当我在学校学习图算法时,我为它们的广泛应用而感到惊讶。 图在编译器和构建系统中起着至关重要的作用。 各种图形算法构成了我们如何通过Internet相互通信的基础。
我们的日常生活往往充满了清单。 我们有待办事项清单,购物清单,食谱,清单,说明等等。 有一天,我意识到其中一些清单在欺骗。 其中一些列表实际上是隐藏在列表中的图形。 这些列表是有向无环图的 拓扑顺序 。 最明显的两种情况是待办事项清单和配方。 您无法发送尚未写过的信,也可以煮一锅水煮意大利面。
一旦发现了偷偷摸摸的图,我就花了一些时间思考如何从像DAG一样的各种列表中秘密获得收益。 这些清单和DAG之间最清晰的类比是待办事项清单和配方。 这些非常相似的依赖图 。 这些依赖图与列表表示不同,它们显示可以同时执行的顶点(单个任务)。 有时候这很明显(例如:我可以等到一锅水烧开时切碎蔬菜!我可以在第一批饼干在烤箱中时准备第二张饼干烘烤片!),但是我希望将列表重新构造为依赖关系图的行为可能会暴露更多的并发机会。
Boil water -- Add pasta -- Cook pasta --.
\
Purée tomatoes --. \
\ \
Chop vegetables -- Combine in saucepan -- Simmer sauce -- Combine pasta and sauce -- Serve
/
Add spices --'
上图中顶点之间的边表示部分排序。 它们之间没有路径的顶点可以同时执行。 图中顶点之间的关系反映了厨房中的事实。 我们可以在煮酱的时候煮意大利面。 我们先切蔬菜还是番茄泥都没关系。 如果我们要做任何调味料,它们都需要做。
尽管我尽了最大的努力,但我仍然是厨房里的灾难。 作为改变这一不便事实的一种方法,我认为尝试将配方表示为有向图以及附加数据(如预计每个步骤需要多长时间)会很有趣。 这很长时间以来一直存在于我的“有一天的项目”列表中,只有当我开始考虑我在$ JOB从事的项目时,我才想起它。
实际的
我去年在Adgear上完成的项目之一是删除在我们每个已经极限的边缘服务器上进行的昂贵计算,然后将其替换为在单台机器上执行昂贵计算的系统,然后分发结果。 当没有人触摸时,该系统就可以工作,但是它的编程效果等同于细枝和胶带。 cron作业和Shell脚本。 该系统使用起来仍然很痛苦,并且出现了更多在CPU匮乏的计算机上进行昂贵计算的示例。 在这一点上,开始编写一个更强大的系统是有意义的。
这些昂贵的计算很好地分解为要执行的步骤列表。 获取此数据,对其进行转换,对其进行更多转换,将一些数据发送到该组服务器,将该其他数据发送到其他组服务器。 Shell脚本非常擅长编码这些管道,因此在实现时是一个可接受的选择。 过了一会儿,我突然意识到这些步骤清单并不是真正的清单。 它们是依赖图。 我试图通过在shell脚本中添加一些后台作业和waitpid来暴露潜在的并发性,但是我决定切换到Erlang并从OTP提供的所有功能中受益是更有意义的。
让我们来《 Wrek》
(对不起。我无法抗拒。)
Wrek接受像上面的意大利面条食谱这样的依赖图作为输入,并执行每个顶点。 只要能够执行动作,就可以同时执行动作的性质对于依赖关系图可以表示的任何问题都是通用的 。 依赖图的结构以及每个图的顶点所涉及的任务都是特定于用户的意愿的。 遵循与OTP相同的通用/特定划分; 此类问题的通用部分由wrek
和wrek_vert
模块解决。 用户以Erlang映射的形式提供特定部分,该映射描述了图中的每个顶点,以及一组实现wrek_vert
的回调模块 行为。
wrek_vert
行为由单个回调run/2
,其中第一个参数是要发送给回调函数的参数列表,第二个参数是可以提供其他顶点生成的信息的进程的ID。 此回调函数的预期结果是
{ok, Any :: any()}
要么 {error, Reason :: any()}
。 如果回调函数成功,则wrek将使用Any
并将其提供给其他wrek_vert
进程。 如果回调函数崩溃或返回错误,则将关闭整个图形。
制作Erlang意大利面
按照上面人为设计的示例,让我们开始使用wrek制作意大利面。 当然,我们的程序不会真正制作面食,但是它的输出应该使人蒙昧。
查看上图,每个步骤似乎可以完成以下三件事之一:
- 添加成分
- 在容器中混合成分
- 对容器中的成分进行处理
让我们继续为每个动作编写一些wrek_vert
。 如果您不想跟随文本编辑器,请在此处找到完整的代码。
-behaviour(wrek_vert).
-export([run/2]).
run([Ingredient, Quantity], _Pid) ->
io:format(“adding ~s. amount: ~s.~n”, [Ingredient, Quantity]),
{ok, #{added => [{Ingredient, Quantity}]}}.
这就是cook_add
的全部cook_add
。 它打印一条消息,然后生成一个添加了键的映射,该键的值是具有一对的属性列表。
-behaviour(wrek_vert).
-export([run/2]).
run([Verb, Noun], _Pid) ->
io:format("~ping ~p.~n", [Verb, Noun]),
{ok, #{}}.
cook_heat
也很短。 它也非常抽象。 它可以用于打印有关 Verb
任何Noun
,而不仅仅是烹饪食材!
我们的最终回调模块要更长一些,因为它比打印消息要执行更多的工作。
-behaviour(wrek_vert).
-export([run/2]).
run([Ingredients, Vessel], Pid) ->
Fun = fun(Step, Acc) ->
Stuff = wrek_vert:get(Pid, Step, added),
io:format("combining ~p with ~p in ~p.~n", [Stuff, Acc, Vessel]),
Stuff ++ Acc
end,
Stuff = lists:foldl(Fun, [], Ingredients),
io:format("~p now contains: ~p.~n", [Vessel, Stuff]),
{ok, #{added => Stuff}}.
Ingredients
应该是顶点名称的列表。 最后,我们使用父进程的Pid作为wrek_vert:get/3
的参数。 这使我们可以使用cook_add
生成的数据 回调模块。 合并所有内容后,我们将返回一个新的成分集合。
好的! 我们几乎已经完成了描述问题的特定部分的工作! 最后一步是根据这些回调模块和我们要传递给它们的参数来表示我们的依赖图。
make_pasta() ->
application:ensure_all_started(wrek),
Graph = #{
tomatoes => #{
module => cook_add,
args => ["pureed tomatoes", "1 can"],
deps => []
},
vegetables => #{
module => cook_add,
args => ["chopped vegetables", "lots"],
deps => []
},
spices => #{
module => cook_add,
args => ["spices", "to taste"],
deps => []
},
saucepan => #{
module => cook_combine,
args => [[tomatoes, vegetables, spices], saucepan],
deps => [tomatoes, vegetables, spices]
},
simmer_sauce => #{
module => cook_heat,
args => [simmer, sauce],
deps => [saucepan]
},
boil_water => #{
module => cook_heat,
args => [boil, water],
deps => []
},
add_pasta => #{
module => cook_add,
args => ["pasta", "1 handful"],
deps => [boil_water]
},
cook_pasta => #{
module => cook_heat,
args => [cook, pasta],
deps => [add_pasta]
},
mix_pasta_with_sauce => #{
module => cook_combine,
args => [[saucepan, add_pasta], saucepan],
deps => [simmer_sauce, cook_pasta]
}
},
wrek:start(Graph).
真是满眼! 我们在这里完成的工作是创建一个Erlang映射,其键代表原始依赖关系图中顶点的名称,其值是指定回调模块,传递给回调模块的参数以及顶点的任何依赖关系的映射可能有。 我对你们中那些注意到我们从不拉紧意大利面的人表示祝贺; 我承认在厨房里是一场灾难。 我保证我学到了教训。
好了,我们完成了编码! 让我们开始一个贝壳,做一些意大利面吧!
阅读有关codesync.global的本博客文章的其余部分。
加入CodeBEAM SF的AdGear开发人员Richard Kallos,他将在wrek上发表演讲 。
最初发布于 codesync.global 。
From: https://hackernoon.com/introducing-wrek-a-miniature-erlang-graph-engine-79196e7ea457