Erlang 非同期で並列ダウンロードするプログラム

  • 11
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

勉強するために、非同期で並列HTTP通信して10のURLからダウンロードするプログラムを書いて、実行速度を比較検証します。最後にErlang有識者にコード見せたら、かなり厳しめの指摘を頂いたのでその点について最期に書いてます。

■ 同期してダウンロード実行
スクリーンショット 2016-06-02 17.14.13.png

■ 非同期でダウンロード実行
スクリーンショット 2016-06-02 17.14.34.png

ダウンロード機能のみ実装

HTTP通信してファイルに保存する

http_downloader_2
%%%-------------------------------------------------------------------
%%% @doc
%%% http通信
%%% 参考: http://erlang.org/doc/apps/inets/http_client.html
%%% @end
%%% Created : 26. 5 2016 15:54
%%%-------------------------------------------------------------------
-module(http_downloader_2).

%% API
-export([main/0]).

%% main
main() ->
    Url = "http://www.erlang.org",
    Body = http_request(Url),
    file_writer(Body, "/tmp/download/output.dat"),
    ok.

%% printer
print(Message) -> io:format("E: ~p~n",[Message]).

%% http 通信してBodyを返す
http_request(URL) ->
    inets:start(),
    {ok, {{Version, 200, ReasonPhrase}, Headers, Body}} = httpc:request(URL),
    Body.

%% fileに保存する
file_writer(Body, FilePath) ->
    file:write_file(FilePath, Body, [binary, write]).
実行
>>> erlc ./http_downloader_2.erl
>>> erl -noshell -s http_downloader_2 main -s init stop
>>> ls -lta /tmp/download/output.dat 
-rw-r--r--  1 ******  wheel  12740  5 27 16:55 /tmp/download/output.dat

プロセス1つで順番にダウンロードする(同期して実行)

プロセスを1つ生成(spawn)して順番にダウンロードするモデル

http_downloader_4
%%%-------------------------------------------------------------------
%%% @doc
%%% プロセス1つで順番にダウンロードする
%%%
%%% @end
%%% Created : 26. 5 2016 15:54
%%%-------------------------------------------------------------------
-module(http_downloader_4).

%% API
-export([main/0, download_process/0]).

%% main
main() ->
    Urls = ["http://erlang.org/doc/man/httpc.html",
     "http://erlang.org/doc/apps/inets/http_client.html",
     "http://erlang.org/doc/apps/inets/inets_services.html#id60923",
     "http://erlang.org/doc/apps/inets/release_notes.html",
     "http://erlang.2086793.n4.nabble.com/Question-re-io-format-and-string-handling-td2109722.html",
     "http://www.erlang.org/mailman/listinfo/erlang-questions",
     "http://stackoverflow.com/questions/7595128/how-to-optimize-the-receive-loop-for-thousands-of-messages-in-erlang",
     "http://stackoverflow.com/jobs",
     "http://stackoverflow.com/tags",
     "http://stackoverflow.com/questions/tagged/c%23"],

    %% downloader プロセスを生成する
    Pid2 = spawn(?MODULE, download_process, []),

    Receiver = fun(Url) ->
        %% プロセスにメッセージを投げる
        Pid2 ! {self(), Url},

        %% プロセスからの応答を待つ
        receive
            {Pid2, Msg} ->
                print("Download Finish:  " ++ Msg)
        end
    end,

    lists:foreach(Receiver, Urls),

    %% プロセスの停止
    Pid2 ! stop.

%% printer
print(Message) -> io:format("E: ~p~n",[Message]).

%% download用のプロセス
download_process() ->
    receive
        {From, Msg} ->
            %% download処理
            downloader(Msg),

            %% 応答を返す
            From ! {self(), Msg},
            download_process();
        stop ->
            true
    end.

%% http通信してfileに保存する
downloader(Url) ->
    Body = http_request(Url),
    Path = path_generator(Url),
    file_writer(Body, Path),
    ok.

%% URLからファイルパスを生成する。
path_generator(Url) ->
    FileName = name_generator(Url),
    Path = string:concat("/tmp/download/", FileName),
    Path.

%% URLからファイル名を生成する。
name_generator(Url) ->
    {ok, Result} = http_uri:parse(Url),
    {Scheme, UserInfo, Host, Port, Path, Query} = Result,
    %% "/" を削除して文字列に変換
    re:replace(Path, "/", "", [global, {return, list}]).

%% http 通信してBodyを返す
http_request(URL) ->
    inets:start(),
    {ok, {{Version, 200, ReasonPhrase}, Headers, Body}} = httpc:request(URL),
    Body.

%% fileに保存する
file_writer(Body, FilePath) ->
    file:write_file(FilePath, Body, [binary, write]).

実行
rm -rf /tmp/download/*
erlc ./http_downloader_4.erl
echo "+++++++++++++"
erl -noshell -s http_downloader_4 main -s init stop
echo "+++++++++++++"
ls -lta /tmp/download/

実行結果
E: "Download Finish:  http://erlang.org/doc/man/httpc.html"
E: "Download Finish:  http://erlang.org/doc/apps/inets/http_client.html"
E: "Download Finish:  http://erlang.org/doc/apps/inets/inets_services.html#id60923"
E: "Download Finish:  http://erlang.org/doc/apps/inets/release_notes.html"
E: "Download Finish:  http://erlang.2086793.n4.nabble.com/Question-re-io-format-and-string-handling-td2109722.html"
E: "Download Finish:  http://www.erlang.org/mailman/listinfo/erlang-questions"
E: "Download Finish:  http://stackoverflow.com/questions/7595128/how-to-optimize-the-receive-loop-for-thousands-of-messages-in-erlang"
E: "Download Finish:  http://stackoverflow.com/jobs"
E: "Download Finish:  http://stackoverflow.com/tags"
E: "Download Finish:  http://stackoverflow.com/questions/tagged/c%23"
+++++++++++++
total 1104
drwxr-xr-x  12 ****  wheel     408  5 27 18:00 .
-rw-r--r--   1 ****  wheel   95746  5 27 18:00 questionstaggedc%23
-rw-r--r--   1 ****  wheel   85691  5 27 18:00 jobs
-rw-r--r--   1 ****  wheel   91712  5 27 18:00 questions7595128how-to-optimize-the-receive-loop-for-thousands-of-messages-in-erlang
-rw-r--r--   1 ****  wheel   67743  5 27 18:00 tags
-rw-r--r--   1 ****  wheel    6266  5 27 18:00 mailmanlistinfoerlang-questions
-rw-r--r--   1 ****  wheel  117721  5 27 18:00 Question-re-io-format-and-string-handling-td2109722.html
-rw-r--r--   1 ****  wheel    9012  5 27 18:00 docappsinetshttp_client.html
-rw-r--r--   1 ****  wheel    6574  5 27 18:00 docappsinetsinets_services.html
-rw-r--r--   1 ****  wheel    7011  5 27 18:00 docappsinetsrelease_notes.html
-rw-r--r--   1 ****  wheel   57479  5 27 18:00 docmanhttpc.html
drwxrwxrwt  28 root   wheel     952  5 27 16:24 ..

複数プロセスで非同期ダウンロード

スクリーンショット 2016-06-02 17.14.34.png

dwn.erl
%%%-------------------------------------------------------------------
%%% @author hami
%%% @copyright (C) 2016, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 30. 5 2016 12:36
%%%-------------------------------------------------------------------
-module(dwn).
-author("hami").

%% API
-export([main/0, download/1]).

main() ->
  Urls = ["http://erlang.org/doc/man/httpc.html",
    "http://erlang.org/doc/apps/inets/http_client.html",
    "http://erlang.org/doc/apps/inets/inets_services.html#id60923",
    "http://erlang.org/doc/apps/inets/release_notes.html",
    "http://erlang.2086793.n4.nabble.com/Question-re-io-format-and-string-handling-td2109722.html",
    "http://www.erlang.org/mailman/listinfo/erlang-questions",
    "http://stackoverflow.com/questions/7595128/how-to-optimize-the-receive-loop-for-thousands-of-messages-in-erlang",
    "http://stackoverflow.com/jobs",
    "http://stackoverflow.com/tags",
    "http://stackoverflow.com/questions/tagged/c%23"],

  %% マルチプロセスでダウンロード開始
  downloader(self(), Urls),

  %% ダウンロード完了を受信する
  receiver_download_finish(length(Urls)),
  ok.

%% ダウンロードプロセスから受信する
receiver_download_finish(0) -> ok;
receiver_download_finish(N) when N > 0 ->
    receive
      {Msg} ->
        print(Msg),
        receiver_download_finish(N - 1);
      stop ->
        true
    end.

%% 非同期でダウンロードプロセスを立ち上げる
downloader(MainPid, Urls) ->
  Starter = fun(X) ->
    %% プロセスの生成
    Pid2 = spawn(?MODULE, download, [MainPid]),

    %% プロセスにメッセージを投げる
    Pid2 ! {X} end,

  lists:foreach(Starter, Urls),
  ok.

%% 立ち上げるプロセス
download(MainPid) ->
  receive
    {Url} ->
      %% ダウンロード実行
      download_and_write_file(Url),

      %% 完了した旨を通知する
      MainPid ! {Url};
    stop ->
      true
  end.

%% printer
print(Message) -> io:format("E: ~p~n",[Message]).


%% http通信してfileに保存する
download_and_write_file(Url) ->
  Body = http_request(Url),
  Path = path_generator(Url),
  file_writer(Body, Path),
  ok.

%% URLからファイルパスを生成する。
path_generator(Url) ->
  FileName = name_generator(Url),
  Path = string:concat("/tmp/download/", FileName),
  Path.

%% URLからファイル名を生成する。
name_generator(Url) ->
  {ok, Result} = http_uri:parse(Url),
  {Scheme, UserInfo, Host, Port, Path, Query} = Result,
  %% "/" を削除して文字列に変換
  re:replace(Path, "/", "", [global, {return, list}]).

%% http 通信してBodyを返す
http_request(URL) ->
  inets:start(),
  {ok, {{Version, 200, ReasonPhrase}, Headers, Body}} = httpc:request(URL),
  Body.

%% fileに保存する
file_writer(Body, FilePath) ->
  file:write_file(FilePath, Body, [binary, write]).

コンパイルと実行
# コンパイルと実行
echo "+++++++++++++"
erlc ./dwn.erl
echo "+++++++++++++"
time erl -noshell -s dwn main -s init stop
echo "+++++++++++++"

実行結果
+++++++++++++
E: "http://stackoverflow.com/questions/7595128/how-to-optimize-the-receive-loop-for-thousands-of-messages-in-erlang"
E: "http://stackoverflow.com/tags"
E: "http://stackoverflow.com/questions/tagged/c%23"
E: "http://stackoverflow.com/jobs"
E: "http://erlang.2086793.n4.nabble.com/Question-re-io-format-and-string-handling-td2109722.html"
E: "http://erlang.org/doc/apps/inets/http_client.html"
E: "http://erlang.org/doc/apps/inets/release_notes.html"
E: "http://erlang.org/doc/apps/inets/inets_services.html#id60923"
E: "http://erlang.org/doc/man/httpc.html"
E: "http://www.erlang.org/mailman/listinfo/erlang-questions"

real    0m3.462s
user    0m0.258s
sys 0m0.142s
+++++++++++++
```shell-session:実行結果

Erlang有識者にコード見せた結果

ロジックは正しいが、業務では次の3つの関数は基本的に利用せず、behaviorを利用するとアドバイスを頂きました。

■ 業務でErlang書くときは、次の3つの関数をほぼ利用しない
1. spawn 禁止
2. receive 禁止
3. Pid ! {} 禁止

■ 対策
適切にbehaviorを使って書き直そう
1. application
2. supervisor
3. gen_server
4. gen_fsm

次の学習

次はbehaviorを使って並行ダウンロードのプログラム書き直してみます