ZEAM開発ログ v.0.2.1 Node.js と同じ原理の軽量コールバックスレッドを Elixir に実装してみた (実装編)

（この記事は[「言語情報 Advent Calendar 2017」](https://qiita.com/advent-calendar/2017/lang_dev) の23日目で，[「ZEAM開発ログ v.0.2.0 Node.js と同じ原理の軽量コールバックスレッドを Elixir に実装してみた (背景編)」](https://qiita.com/zacky1972/items/404301b783d2e66ed0a5)の続きです）

[「ZEAM開発ログ 目次」はこちら](https://qiita.com/zacky1972/items/70593ab2b70d192813df)

# おしらせ

Elixirの研究に日夜励んでいるZACKYです。好評いただいた「ElixirでGPU駆動」の連載記事のまとめを，今度の[fukuoka.ex#11](https://fukuokaex.connpass.com/event/87241/)でプレゼンテーションします！ まだ若干の空席があります！

[![image.png](https://connpass-tokyo.s3.amazonaws.com/thumbs/cb/13/cb130a03343bc310b07244c308b940db.png)](https://fukuokaex.connpass.com/event/87241)

# さて本題〜はじめに

[「ZEAM開発ログ v.0.2.0 Node.js と同じ原理の軽量コールバックスレッドを Elixir に実装してみた (背景編)」](https://qiita.com/zacky1972/items/404301b783d2e66ed0a5)では次のようにまとめました。

* マルチタスクを実現する方式が進化し続けています。
* Unix では**マルチプロセス方式**により，メモリ管理と一体となった形でコンテキストスイッチをしていました。
* ウェブブラウザの登場とともに **マルチスレッド方式**が発案され，メモリ管理情報を切り替えずにコンテキストスイッチすることで効率化するようになりました。
* Node.js では，**コールバック方式**により，スタックメモリを確保せずに接続要求を処理する方式が発案されました。
* 私たちは Elixir に**軽量コールバックスレッド**を実装し，メモリ消費を抑えて Phoenix の同時セッション最大数とレイテンシを格段に改善する方式を提案します。

ではいよいよ Elixir での軽量コールバックスレッドをどう実装するか，コードを紹介したいと思います！

# まずは実行例の紹介

[GitHub はこちら(https://github.com/zeam-vm/zeam_callback)です。](https://github.com/zeam-vm/zeam_callback)

使い方はこんな感じです。

```elixir
iex(1)> pid = ZeamCallback.Receptor.new
#PID<0.111.0>
iex(2)> send(pid, {:spawn, fn(tid) -> IO.puts "foo #{tid}" end}); send(pid, {:spawn, fn(tid) -> IO.puts "bar #{tid}" end})
foo 0
bar 1
```

実行すると，`foo 0` と `bar 1` が表示されるのですが，これらはコールバックされています。番号は軽量コールバックスレッドのIDです。

# 使い方

* `Receptor` は，軽量コールバックスレッドへのリクエストを受け付けます。現状では次のリクエストを受け付けます。
  * `:spawn`: 軽量コールバックスレッドを起動します。引数として，引数を1つ持つコールバックする関数を与えます。あとで `Worker`がこの関数を呼び出します。

近い将来，軽量コールバックスレッド同士のメッセージ通信を実装する予定です。現在，送る方の仕組み `:send` リクエストはだいたい実装できました。現在，受け取る仕組みについて，試行錯誤しているところです。次のような感じでコールバックを用いてメッセージを受け取る方式がわかりやすいかなと思います。

```elixir
send(pid, {:spawn, fn(tid) ->
  env = %{:tid => tid}
  ZeamCallback.receive(env, fn(env, message) ->
    IO.puts "Thread #{env[:tid]} receives a message: #{message}"
  end)
end)
```

# 実装原理

軽量コールバックスレッドの実装にあたり，`Receptor` と `Worker` の2つのスレッドを用意しています。

`Receptor` は軽量コールバックスレッドへのリクエストを次々 `Worker` に投げます。

`Worker` の処理としては，最初に `receive` を実行してメッセージを処理します。もし何もメッセージキューにたまっていなかったら実行キュー(`env[:queue]`)から1つ取り出して，引数には軽量コールバックスレッドのIDを入れて，コールバックします。実行キューに何もなかったら10ms寝ます。

`Worker` は `Receptor` のプロセスIDと環境変数 `env` を状態として持つプロセスです。環境変数 `env` には，現状では `:queue` と `:threads` と `:next_tid` の3つの情報を記録しています。

* `:queue` は実行キュー，すなわち次以降に実行するコールバック関数の待ち行列です。
* `:threads`は現在の軽量コールバックスレッドIDとコールバック関数の対応関係を保持しています。
* `:next_tid` は次に軽量コールバックスレッドを新規生成した時のIDが記録されています。

`Worker` の持つ環境変数は，適宜 `Receptor` にも `:queue` リクエストで送られます。`Receptor` は環境変数をバックアップとして保持します。もし `Worker` が何らかの理由で無反応になってしまったときには `Worker` を再起動する機能を実装する予定です。

# まとめ

* 軽量コールバックスレッドを実装しました。[コードは https://github.com/zeam-vm/zeam_callback に公開しています。](https://github.com/zeam-vm/zeam_callback)
* 現状では軽量コールバックスレッドの起動 `:spawn` を実装しています。
* 近い将来，軽量コールバックスレッド同士のメッセージ通信を実装する予定です。
* 軽量コールバックスレッドの実装にあたり，`Receptor`と`Worker`という2つのスレッドを役割分担させました。

次回は軽量コールバックスレッドのメモリ消費量について評価します。お楽しみに！

明日は @twinbee さんの[「ElixirのiexでとりこぼしたPIDをひろう」](https://qiita.com/twinbee/items/5ef58c6f4c1200173197)です。こちらもお楽しみに！

# p.s.「いいね」よろしくお願いします

よろしければ，ページ左上の ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/4d515047-cc48-382e-c2b1-3ad0cc50dbbf.png) や ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/a4e3da58-70a3-4197-95a2-6a6906650d01.png) のクリックをお願いしますー:bow:
ここの数字が増えると，書き手としては「ウケている」という感覚が得られ，連載を更に進化させていくモチベーションになりますので，もっとElixirネタを見たいというあなた，私たちと一緒に盛り上げてください！:tada: