Engineering from Scratch

エンジニア目指してます

2022/07/03

You Might Not Need an Effect

Resetting all state when a prop changes

悪い例

import { useEffect, useState } from "react";

const App = () => {
  const [userId, setUserId] = useState(0);

  return (
    <>
      <button onClick={() => setUserId((userId) => userId + 1)}>+1</button>
      <Comment userId={userId} />
    </>
  );
};

type CommentProps = {
  userId: number,
};

const Comment = (props: CommentProps) => {
  const { userId } = props;
  const [comment, setComment] = useState("");

  useEffect(() => setComment(""), [userId]);

  return (
    <>
      <div>{userId}</div>
      <input value={comment} onChange={(e) => setComment(e.target.value)} />
      <div>{comment}</div>
    </>
  );
};

export default App;

userIdごとにコメントを出し分けるCommentコンポーネントが存在する。userIdが変化した時に,commentstate を初期化するために,Effect を使用している。

userIdstate が更新された時に,Commentコンポーネントが古い値のまま再レンダリングされ,その後 useEffect により,もう一度レンダリング処理が走るため,非効率。

良い例

import { useState } from "react";

const App = () => {
  const [userId, setUserId] = useState(0);

  return (
    <>
      <button onClick={() => setUserId((userId) => userId + 1)}>+1</button>
      <Comment userId={userId} key={userId} />
    </>
  );
};

type CommentProps = {
  userId: number,
};

const Comment = (props: CommentProps) => {
  const { userId } = props;
  const [comment, setComment] = useState("");

  return (
    <>
      <div>{userId}</div>
      <input value={comment} onChange={(e) => setComment(e.target.value)} />
      <div>{comment}</div>
    </>
  );
};

export default App;

CommentコンポーネントuserIdを key として持たせることで解決できる。key が違うと別のコンポーネントとみなされるため,userIdが変化する度に,commentstate は初期化される。

Initializing the application

初期マウント時に一度だけ行いたい処理に useEffect を使用した時,開発環境では2回レンダリングが起こる。ログイン処理など,一度だけ行われるべき処理では,以下のように対処すべき。

import { useEffect } from "react";

let didInit = false;

const App = () => {
  useEffect(() => {
    if (didInit) return;

    didInit = true;
    console.log("Hello world");
  }, []);

  return (
    <>
      <h1>Hello World</h1>
    </>
  );
};

export default App;

Subscribing to an external store

外部から取得したデータについて考える。

悪い例

import { useEffect, useState } from "react";

const useOnlineStatus = () => {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    const updateState = () => {
      setIsOnline(navigator.onLine);
    };

    updateState();

    window.addEventListener("online", updateState);
    window.addEventListener("offline", updateState);
    return () => {
      window.removeEventListener("online", updateState);
      window.removeEventListener("offline", updateState);
    };
  }, []);

  return isOnline;
};

const App = () => {
  const isOnline = useOnlineStatus();

  if (isOnline) {
    console.log("Online!");
  } else {
    console.log("Offline!");
  }

  return <h1>Hello World</h1>;
};

export default App;

navigator.onLineというブラウザのオンライン状態を返すために Effect を使用している。

良い例

useSyncExternalStoreという React 独自の hook を使用する。

import { useSyncExternalStore } from "react";

const subscribe = (callback: () => void) => {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
};

const useOnlineStatus = () => {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine,
    () => true
  );
};

const App = () => {
  const isOnline = useOnlineStatus();

  if (isOnline) {
    console.log("Online!");
  } else {
    console.log("Offline!");
  }

  return <h1>Hello World</h1>;
};

export default App;

参考

beta.reactjs.org

2022/07/02

You Might Not Need an Effect

props や state の変化によりコンポーネントを更新したいときに,useEffect を使うべきではない。 以下の二つの場合,useEffect を使うべきではない。

  • レンダリングによりデータを変換するとき。
    • 例:あるリストを表示前にフィルタリングしたい時。
    • リストが変更されたときに,Effect により state を更新したくなるだろうが,これは非効率。
      • コンポーネントを更新しようとした時,画面上に何を表示すべきか計算するために,React はまず component の関数を呼び出す。それからこれらの変更を DOM に反映し,画面を更新する。そして,Effect を実行する。もし Effect 内で,state の更新が走ればコンポーネントの更新が最初から始まる。
      • 上記を避けるために,コンポーネントのトップレベルで全てのデータの変換を行えば良い。この処理は props や state が変化したときにいつでも再実行される。
  • ユーザーのイベントを処理する時。

外部のシステムと同期するときに Effect が必要となる。

  • React の state と jQuery widget との同期
  • データの取得
  • 検索結果

Updating state based on props or state

state を変換して作成される値を,state で管理して,useEffect で更新するべきではない。

悪い例

import { useEffect, useState } from "react";

const App = () => {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");

  const [fullName, setFullName] = useState("");

  useEffect(() => {
    setFullName(firstName + " " + lastName);
  }, [firstName, lastName]);

  return (
    <>
      <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
      <div>{fullName}</div>
    </>
  );
};

export default App;

firstNamelastNameをつなげたfullNameを state として管理し,両者が変更されるたびに,fullNameを更新している。

また,firstNameもしくはlastNameが変更された時には,fullNameが元の古い値のままレンダリングされ,そこからfullNameが更新され,再レンダリングが走るという点で非効率的。

良い例

import { useState } from "react";

const App = () => {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");

  const fullName = firstName + " " + lastName;

  return (
    <>
      <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
      <div>{fullName}</div>
    </>
  );
};

export default App;

fullNameをトップレベルで定数で定義している。firstNamelastNameの state が変化する度に,再レンダリングが起こりfullNameが再定義される。

Caching expensive calculations

先ほどの例で,emailも state で管理する時を考える。

悪い例

import { useState } from "react";

const App = () => {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [email, setEmail] = useState("");

  const fullName = firstName + " " + lastName;

  return (
    <>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
      <div>{fullName}</div>
    </>
  );
};

export default App;

この時,emailstate が変更された時,fullNameの値は変更されていないにも関わらず,もう一度fullNameの計算処理が走ってしまい,非効率的。

良い例

import { useMemo, useState } from "react";

const App = () => {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [email, setEmail] = useState("");

  const fullName = useMemo(() => {
    console.log("fullName rendered!");
    return firstName + " " + lastName;
  }, [firstName, lastName]);

  return (
    <>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      <input value={lastName} onChange={(e) => setLastName(e.target.value)} />
      <div>{fullName}</div>
    </>
  );
};

export default App;

fullNameを useMemo によりメモ化することで,firstNamelastNameが変更された時のみfullNameが計算され,emailstate が変更されてもfullNameは以前の値がそのまま使われる。

参考

beta.reactjs.org

2022/06/25

外部キーのインデックス

rails を使用していると,referencesで外部キーに対して自動でインデックスが貼られる。しかし,そのインデックスの役割を正しく認識していなかった。

usersテーブルに対してpostsテーブルがuser_idを持っている時を考える。postsuser_idに対して,インデックスを貼っていない状態で,where 句にuser_idを指定して検索すると,postsテーブル全件に対して検索が走る。しかし,user_idに対してインデックスを貼ることで,検索対象のレコード数が減少し,高速化が見込まれる。

参考

https://www.amazon.co.jp/%E9%81%94%E4%BA%BA%E3%81%8C%E6%95%99%E3%81%88%E3%82%8BWeb%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9%E3%83%81%E3%83%A5%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0-%E3%80%9CISUCON%E3%81%8B%E3%82%89%E5%AD%A6%E3%81%B6%E9%AB%98%E9%80%9F%E5%8C%96%E3%81%AE%E5%AE%9F%E8%B7%B5-%E8%97%A4%E5%8E%9F-%E4%BF%8A%E4%B8%80%E9%83%8E/dp/4297128462

2022/06/13

eager_load と preload の使い所

includes

eager_loadpreload を呼び分ける。どちらかが呼ばれるかコントロールしにくいので,基本使わない。実装を読んでみようと思ったが,読み切るには重そうだったので,週末読む。

eager_load

belongs_tohas_oneに使用する。

1:N となるの関係となる,user:blogについて考える。user.eager_load(:blogs).limit(10を実行した時,limitは,userに対して行われ,以下のような結果となる。

irb(main):014:0> User.count
  User Count (0.2ms)  SELECT COUNT(*) FROM "users"
=> 12
irb(main):015:0> Blog.count
  Blog Count (0.2ms)  SELECT COUNT(*) FROM "blogs"
=> 1200
irb(main):011:0> User.eager_load(:blogs).limit(10)
  SQL (0.3ms)  SELECT DISTINCT "users"."id" FROM "users" LEFT OUTER JOIN "blogs" ON "blogs"."user_id" = "users"."id" LIMIT ?  [["LIMIT", 10]]
  SQL (6.8ms)  SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."department_id" AS t0_r2, "users"."company_id" AS t0_r3, "users"."created_at" AS t0_r4, "users"."updated_at" AS t0_r5, "blogs"."id" AS t1_r0, "blogs"."title" AS t1_r1, "blogs"."user_id" AS t1_r2, "blogs"."created_at" AS t1_r3, "blogs"."updated_at" AS t1_r4 FROM "users" LEFT OUTER JOIN "blogs" ON "blogs"."user_id" = "users"."id" WHERE "users"."id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)  [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5], ["id", 6], ["id", 7], ["id", 8], ["id", 9], ["id", 10]]

この時,userを一意なものにしてから取得するので,distinctが走り,これがスロークエリの原因となる模様。詳しい実装は割愛。

preload

has_manyに使用する。

注意点としては,IN 句が膨大にならないように注意する。

参考

https://moneyforward.com/engineers_blog/2019/04/02/activerecord-includes-preload-eagerload/

2022/06/12

Unix Processes

Diving into Rack

Rackのrackupコマンドは,プロセスをデーモン化できるため,そのコードを読んでいく。

    exit if fork
    Process.setsid
    exit if fork
    Dir.chdir "/" 
    STDIN.reopen "/dev/null"
    STDOUT.reopen "/dev/null", "a" 
    STDERR.reopen "/dev/null", "a" 

Daemonizing a Process, Step by Step

exit if fork

forkメソッドは値を,親プロセス・子プロセスで計2回返す。親プロセスでは子プロセスのpidを,子プロセスではnilを返す。

そのため,親プロセスは終了し,子プロセスは存在したままになる。親プロセスの存在しない子プロセス(orphaned process)のppidは1となる。

Process.setsid

以下の3つのことを行う。

  1. プロセスが新しいセッションのセッションリーダーとなる
  2. プロセスが新しいプロセスグループのグループリーダーとなる
  3. プロセスはコントロールできるターミナルを持たない

Process Groups and Session Groups

全てのプロセスは一つのグループに属し,それぞれのグループは一意のIDを持つ。代表的なプロセスグループは,親プロセスとその子プロセス群。

普通,プロセスグループのidは,プロセスグループリーダーのpidと同じになる。

irb(main):005:0> puts Process.getpgrp
16852
=> nil                                                        
irb(main):006:0> puts Process.pid
16852
=> nil 

また,プロセスグループの集合がセッショングループ。

exit if fork

forkされ,プロセスグループとセッショングループのリーダーとなった子プロセス自身は終了し,その子プロセスがforkされる。

新しくforkされたプロセスは,プロセスグループのリーダーでもなく,セッションリーダーでもなく,支配できるターミナルも持っていない。

DIr.chdir "/"

カレントディレクトリを,ルートディレクトリに変更する。必ずしも必要な処理ではないが,デーモンのカレントディレクトリが実行中に消えないようにする。

STDIN.reopen "/dev/null"
STDOUT.reopen "/dev/null", "a"
STDERR.reopen "/dev/null", "a"

デーモンプロセスの出力先を変更する。

参考

workingwithruby.com

Rails の select メソッド

カラムを指定することができるメソッド。以下のような,users テーブルを考える。

irb(main):008:0> User
=> User(id: integer, name: string, department_id: integer, company_id: integer, created_at: datetime, updated_at: datetime)

上記の User モデルの一つ目のデータを取得した結果は以下となる。

irb(main):009:0> User.first
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=>
#<User:0x000000010b861bc0
 id: 1,
 name: "ユーザー_0",
 department_id: 1,
 company_id: 1,
 created_at: Sun, 12 Jun 2022 08:06:02.012826000 UTC +00:00,
 updated_at: Sun, 12 Jun 2022 08:06:02.012826000 UTC +00:00>

SELECT 句で"users".*が指定され,全てのカラムが取得されている。しかし,idnameカラムのみが欲しい時は,selectメソッドを使うことで指定したカラムのみを取得できる。

irb(main):010:0> User.select("id", "name").first
  User Load (0.3ms)  SELECT "users"."id", "users"."name" FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User:0x000000010ae98580 id: 1, name: "ユーザー_0">

SELECT 句では,"users"."id", "users"."name"が指定されており,idnameカラムのみを取得できる。

次に,usersテーブルが以下のようなdepartmentsテーブルに属しているとする。

irb(main):011:0> Department
=> Department(id: integer, name: string, department_code: string, created_at: datetime, updated_at: datetime)

usersテーブルとdepartmentsテーブルを内部結合し,selectメソッドを使ってusersテーブルのidnameカラム,departmentsテーブルのnameカラムを取得する。以下では,json 形式で出力する。

    render json: User
      .joins(:department)
      .select(
        "name",
        "id",
        "departments.name"
      )

出力は以下となる。

[{"name":"部門_0","id":1},{"name":"部門_1","id":2},{"name":"部門_2","id":3},{"name":"部門_0","id":4},{"name":"部門_1","id":5},{"name":"部門_2","id":6},{"name":"部門_0","id":7},{"name":"部門_1","id":8},{"name":"部門_2","id":9},{"name":"部門_0","id":10},{"name":"部門_1","id":11},{"name":"部門_2","id":12}]

nameカラムがdepartmentsテーブルのnameとなってしまっている。usersテーブルとdepartmentsテーブルのnameカラムの区別がされず,上書きされてしまっていることが原因。

以下のように,asを使ってdepartmentsテーブルのnameカラムを区別する。

    render json: User
      .joins(:department)
      .select(
        "name",
        "id",
        "departments.name as department"
      )

以下のエラーが出てしまった。

missing attribute: department_id

どうやらasに,テーブル名の単数形は使えないよう。修正したコード及びその結果は以下となる。

    render json: User
      .joins(:department)
      .select(
        "name",
        "id",
        "departments.name as department_name"
      )
[{"name":"ユーザー_0","id":1,"department_name":"部門_0"},{"name":"ユーザー_1","id":2,"department_name":"部門_1"},{"name":"ユーザー_2","id":3,"department_name":"部門_2"},{"name":"ユーザー_3","id":4,"department_name":"部門_0"},{"name":"ユーザー_4","id":5,"department_name":"部門_1"},{"name":"ユーザー_5","id":6,"department_name":"部門_2"},{"name":"ユーザー_6","id":7,"department_name":"部門_0"},{"name":"ユーザー_7","id":8,"department_name":"部門_1"},{"name":"ユーザー_8","id":9,"department_name":"部門_2"},{"name":"ユーザー_9","id":10,"department_name":"部門_0"},{"name":"ユーザー_10","id":11,"department_name":"部門_1"},{"name":"ユーザー_11","id":12,"department_name":"部門_2"}]

また,SQL は以下のようになる。

SELECT "users"."name", "users"."id", departments.name as department_name FROM "users" INNER JOIN "departments" ON "departments"."id" = "users"."department_id"

参考

https://pikawaka.com/rails/select

eager_load と preload

joins

association をキャッシュしないため,join 先のテーブルで検索をかけたい時に使用する。

先ほど使用した,usersdepartmentsテーブルを考える。

usersのうち,departmentsname"部門_0"であるものを取得する。

irb(main):019:0> User.joins(:department).where(department: { name: "部門_0" }).count
  User Count (2.4ms)  SELECT COUNT(*) FROM "users" INNER JOIN "departments" "department" ON "department"."id" = "users"."department_id" WHERE "department"."name" = ?  [["name", "部門_0"]]
=> 4

しかし,departmentsはキャッシュしないため,departments.nameを取得しようとすると,N+1 となる。

irb(main):020:1* User.joins(:department).where(department: { name: "部門_0" }).each do |user|
irb(main):021:1*   puts user.department.name
irb(main):022:0> end
  User Load (0.3ms)  SELECT "users".* FROM "users" INNER JOIN "departments" "department" ON "department"."id" = "users"."department_id" WHERE "department"."name" = ?  [["name", "部門_0"]]
  Department Load (0.3ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
部門_0
  Department Load (0.1ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
部門_0
  Department Load (0.1ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
部門_0
  Department Load (0.1ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
部門_0

eager_load

指定した assocition を LEFT OUTER JOIN で引いてキャッシュする。JOIN しているため,JOIN 先のテーブルで絞り込みができる。

irb(main):024:0> User.eager_load(:department)
  SQL (0.5ms)  SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."department_id" AS t0_r2, "users"."company_id" AS t0_r3, "users"."created_at" AS t0_r4, "users"."updated_at" AS t0_r5, "departments"."id" AS t1_r0, "departments"."name" AS t1_r1, "departments"."department_code" AS t1_r2, "departments"."created_at" AS t1_r3, "departments"."updated_at" AS t1_r4 FROM "users" LEFT OUTER JOIN "departments" ON "departments"."id" = "users"."department_id"

先ほどと同様に,departmentsnameで絞り込みをして,departments.nameを出力する。

irb(main):036:1* User.eager_load(:department).where(department: { name: "部門_0" }).each do |user|
irb(main):037:1*   puts user.department.name
irb(main):038:0> end
  SQL (0.4ms)  SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."department_id" AS t0_r2, "users"."company_id" AS t0_r3, "users"."created_at" AS t0_r4, "users"."updated_at" AS t0_r5, "department"."id" AS t1_r0, "department"."name" AS t1_r1, "department"."department_code" AS t1_r2, "department"."created_at" AS t1_r3, "department"."updated_at" AS t1_r4 FROM "users" LEFT OUTER JOIN "departments" "department" ON "department"."id" = "users"."department_id" WHERE "department"."name" = ?  [["name", "部門_0"]]
部門_0
部門_0
部門_0
部門_0

association 先であるdepartmentsテーブルがキャッシュされているため,N+1 は発生しない。

preload

preloadを実行した結果は以下となる。

irb(main):006:0> User.preload(:department)
  User Load (8.8ms)  SELECT "users".* FROM "users"
  Department Load (3.3ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" IN (?, ?, ?)  [["id", 1], ["id", 2], ["id", 3]]

SQL 文が2回発行されている。usersテーブルのデータを全て取得した後に,その外部キーでdepartmentsテーブルを取得し,キャッシュしている。そのため,eager_loadと同様に N+1 が発生しない。

irb(main):007:1* User.preload(:department).each do |user|
irb(main):008:1*   puts user.department.name
irb(main):009:0> end
  User Load (0.9ms)  SELECT "users".* FROM "users"
  Department Load (0.4ms)  SELECT "departments".* FROM "departments" WHERE "departments"."id" IN (?, ?, ?)  [["id", 1], ["id", 2], ["id", 3]]
部門_0
部門_1
部門_2
部門_0
部門_1
部門_2
部門_0
部門_1
部門_2
部門_0
部門_1
部門_2

しかし,association テーブルで絞り込もうとするとエラーとなる。

irb(main):012:0> User.preload(:department).where(department: { name: "部門_0" }).first
  User Load (1.1ms)  SELECT "users".* FROM "users" WHERE "department"."name" = ? ORDER BY "users"."id" ASC LIMIT ?  [["name", "部門_0"], ["LIMIT", 1]]
/ `initialize': SQLite3::SQLException: no such column: department.name (ActiveRecord::StatementInvalid)

参考

https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58

2022/06/11

Unix Processes

Our First Pipe

複数プロセス間でのやり取りにパイプを使用する。パイプはデータを単方向通信する。writerがwriteした内容をreaderがreadできる。

irb(main):001:0> reader, writer = IO.pipe
=> [#<IO:fd 9>, #<IO:fd 10>]
irb(main):002:0> writer.write("Into the pipe I go...")
=> 21
irb(main):003:0> writer.close
=> nil
irb(main):004:0> puts reader.read
Into the pipe I go...
=> nil      

writeがcloseしてからでないと,readerはreadできない。

Sharing Pipes

親と子プロセス間で,リソースは共有されるため,pipeも共有される。

irb(main):001:0> reader, writer = IO.pipe
=> [#<IO:fd 9>, #<IO:fd 10>]
irb(main):002:1* fork do
irb(main):003:1*   reader.close
irb(main):004:1*   
irb(main):005:2*   10.times do
irb(main):006:2*     writer.puts "Another one bites the dust"
irb(main):007:1*   end
irb(main):008:0> end
=> 15983
irb(main):009:0> writer.close
=> nil
irb(main):010:1* while message = reader.gets
irb(main):011:1*   $stdout.puts message
irb(main):012:0> end
Another one bites the dust
Another one bites the dust                                              
Another one bites the dust                                              
Another one bites the dust                                              
Another one bites the dust                                              
Another one bites the dust                                              
Another one bites the dust                                              
Another one bites the dust                                              
Another one bites the dust                                              
Another one bites the dust   

上記の例だと,親プロセスにwriterとreader,子プロセスにwriterとreaderがそれぞれ存在するため,不要な子プロセスのreader,親プロセスのwriterをcloseしている。

Stream vs. Messages

Unix Socketsは同じ物理マシン上でやり取りできるソケットの一種。TCPソケットよりも高速。ソケットでは双方向通信が可能。

irb(main):001:0> require "socket"
=> true
irb(main):002:0> child_socket, parent_socket = Socket.pair(:UNIX, :DGRAM, 0)
=> [#<Socket:fd 9>, #<Socket:fd 10>]
irb(main):003:0> max_len = 1000
=> 1000
irb(main):004:1* fork do
irb(main):005:1*   parent_socket.close
irb(main):006:1*   
irb(main):007:2*   4.times do
irb(main):008:2*     instruction = child_socket.recv(max_len)
irb(main):009:2*     child_socket.send("#{instruction} accomplished!", 0)
irb(main):010:1*   end
irb(main):011:0> end
=> 16197
irb(main):012:0> child_socket.close
=> nil
irb(main):013:1* 2.times do
irb(main):014:1*   parent_socket.send("Heavy lifting", 0)
irb(main):015:0> end
=> 2
irb(main):016:1* 2.times do
irb(main):017:1*   parent_socket.send("Feather lifting", 0)
irb(main):018:0> end
=> 2
irb(main):019:1* 4.times do
irb(main):020:1*   $stdout.puts parent_socket.recv(max_len)
irb(main):021:0> end
Heavy lifting accomplished!
Heavy lifting accomplished!                                                    
Feather lifting accomplished!                                                  
Feather lifting accomplished!  

子プロセスが親プロセスから受信したメッセージに文字列を加えて,親プロセスに送信し,最後に親プロセスが受信したメッセージを出力している。

Daemon Processes

daemon processは,ターミナル上でユーザが支配できるというよりは,裏で動いているプロセス。代表的な例は,WebサーバーやDBサーバー。

参考

workingwithruby.com

2022/06/05

Unix Processes

Communicating with Process.wait2

Process.wait2は子プロセスのpidとexit codeを含むstatusを返す。

irb(main):024:1* def use_process_wait2
irb(main):025:2*   5.times do
irb(main):026:3*     fork do
irb(main):027:3*       random_number = rand(5)
irb(main):028:3*       puts "random_number: #{random_number}, pid: #{Process.pid}"
irb(main):029:3*       
irb(main):030:4*       if random_number.even?
irb(main):031:4*         Process.exit 111
irb(main):032:4*       else
irb(main):033:4*         Process.exit 112
irb(main):034:3*       end
irb(main):035:2*     end
irb(main):036:1*   end
irb(main):037:1*   
irb(main):038:2*   5.times do
irb(main):039:2*     pid, status = Process.wait2
irb(main):040:2*     
irb(main):041:3*     if status.exitstatus == 111
irb(main):042:3*       puts "#{pid} enuntered an even number!"
irb(main):043:3*     else
irb(main):044:3*       puts "#{pid} encountered an odd number!"
irb(main):045:2*     end
irb(main):046:1*   end
irb(main):047:0> end
=> :use_process_wait2
irb(main):049:0> use_process_wait2
random_number: 0, pid: 1331
random_number: 3, pid: 1332
random_number: 4, pid: 1333
random_number: 0, pid: 1334
random_number: 1, pid: 1335
1334 enuntered an even number!
1335 encountered an odd number!
1333 enuntered an even number!
1332 encountered an odd number!
1331 enuntered an even number!
=> 5

Waiting for Specific Children

Process.waitpidProcess.waitpid2により,特定の子プロセスの処理が終わるまで,親プロセスをブロックすることができる。

irb(main):045:1* def process_waitpid2
irb(main):046:2*   favorite = fork do
irb(main):047:2*     Process.exit 77
irb(main):048:1*   end
irb(main):049:1*   
irb(main):050:2*   middle_child = fork do
irb(main):051:2*     abort "I want to be wanted on!"
irb(main):052:1*   end
irb(main):053:1*   
irb(main):054:1*   pid, status = Process.waitpid2 favorite
irb(main):055:1*   puts status.exitstatus
irb(main):056:0> end
=> :process_waitpid2
irb(main):057:0> process_waitpid2
I want to be wanted on!
77                                                                        
=> nil  

Race Conditions

irb(main):001:1* def race_conditions
irb(main):002:2*   2.times do
irb(main):003:3*     fork do
irb(main):004:3*       puts Process.pid
irb(main):005:2*     end
irb(main):006:1*   end
irb(main):007:1*   
irb(main):008:1*   puts Process.wait
irb(main):009:1*   sleep 5
irb(main):010:1*   
irb(main):011:1*   puts Process.wait
irb(main):012:0> end
=> :race_conditions
irb(main):013:0> race_conditions
2766
2767                                                                   
2766                                                                   
2767                                                                   
=> nil    

上記の例だと,1つ目のProcess.waitにより,2766の子プロセスが終了時まで,親プロセスがブロックされ,そこからsleepが5秒間走るので,先に子プロセスが終了する。しかし,二個目のProcess.waitでは終了した子プロセスのpidを取得できる。これは,Process.waitは終了した順に子プロセスのpidを取得できるから。

逆に,終了した子プロセスが存在しないときは,エラーとなる。

irb(main):034:0> Process.wait
(irb):34:in `wait': No child processes (Errno::ECHILD)

Unicorn

fork型のプロセスはWebサーバーのUnicornで使用されている。親プロセスから複数の子プロセスがforkして作られ,並列性と信頼性が保証される。

Zombie Processes

Process.waitが呼び出されるまで,子プロセスのstatus情報が残り続ける。Process.detachによりこの情報を解放することができる。

irb(main):001:1* pid = fork do
irb(main):002:1*   puts "Child Process"
irb(main):003:0> end
Child Process
=> 3157                                              
irb(main):004:0> Process.detach pid
=> #<Process::Waiter:0x0000000111408780 run>
irb(main):005:0> Process.wait
(irb):5:in `wait': No child processes (Errno::ECHILD)

Process.detach pidにより,子プロセスのstatus情報が無くなっているため,Process.waitがエラーとなる。

Trapping SIGCHLD

trap(:CHLD)ブロック内の処理は,子プロセスが終了したときに行われる。

irb(main):023:1* def trap_sigchld
irb(main):024:1*   child_processes = 3
irb(main):025:1*   dead_processes = 0
irb(main):026:1*   
irb(main):027:2*   child_processes.times do
irb(main):028:3*     fork do
irb(main):029:3*       sleep 3
irb(main):030:2*     end
irb(main):031:1*   end
irb(main):032:1*   
irb(main):033:2*   trap(:CHLD) do
irb(main):034:2*     puts Process.wait
irb(main):035:2*     dead_processes += 1
irb(main):036:2*     exit if dead_processes == child_processes
irb(main):037:1*   end
irb(main):038:1*   
irb(main):039:2*   loop do
irb(main):040:2*     puts "Parent Process"
irb(main):041:2*     sleep 2
irb(main):042:1*   end
irb(main):043:0> end
=> :trap_sigchld
irb(main):044:0> trap_sigchld
Parent Process
Parent Process               
3921                         
3922                         
3923        

3つの子プロセスがforkされ,親プロセスの処理が走りつつも,子プロセスが終了したタイミングでtrapブロック内の処理が走る。

SIGCHLD and Concurrency

数の子プロセスが同時に終了したとき,それらのプロセスが終了したことを見逃してしまうことがある。

補足

Rubyは標準出力をバッファするため,以下の設定で,標準出力がバッファリングされなくなる。

$stdout.sync = true

kurochan-note.hatenablog.jp

補足終了。

子プロセスの終了を見逃さない実装は以下となる。

irb(main):001:1* def catch_all_child_processes_deaths
irb(main):002:1*   child_processes = 3
irb(main):003:1*   dead_processes = 0
irb(main):004:1*   
irb(main):005:2*   child_processes.times do |i|
irb(main):006:3*     fork do
irb(main):007:3*       sleep 3*i
irb(main):008:2*     end
irb(main):009:1*   end
irb(main):010:1*   
irb(main):011:1*   $stdout.sync = true
irb(main):012:1*   
irb(main):013:2*   trap(:CHLD) do
irb(main):014:3*     begin
irb(main):015:4*       while pid = Process.wait(-1, Process::WNOHANG)
irb(main):016:4*         puts pid
irb(main):017:4*         dead_processes += 1
irb(main):018:3*       end
irb(main):019:3*     rescue Errno::ECHILD
irb(main):020:2*     end
irb(main):021:1*   end
irb(main):022:1*   
irb(main):023:2*   loop do
irb(main):024:2*     exit if dead_processes == child_processes
irb(main):025:2*     puts 1
irb(main):026:2*     sleep 1
irb(main):027:1*   end
irb(main):028:0> end
=> :catch_all_child_processes_deaths
irb(main):029:0> catch_all_child_processes_deaths
irb(main):029:0> catch_all_child_processes_deaths
1
4578                                             
1                                                
1                                                
1                                                
4579                                             
1                                                
1                                                
1                                                
4580                   

上記のように,子プロセスが終了するたびに,trapブロック内の処理が行われ,終了していない子プロセスが存在していても,メインプログラムが実行される。逆に,Process.waitに第二引数を渡さない以下の方法では,終了していない子プロセスが存在する限り,メインプログラムは実行されない。

irb(main):030:1* def catch_all_child_processes_deaths
irb(main):031:1*   child_processes = 3
irb(main):032:1*   dead_processes = 0
irb(main):033:1*   
irb(main):034:2*   child_processes.times do |i|
irb(main):035:3*     fork do
irb(main):036:3*       sleep 3*i
irb(main):037:2*     end
irb(main):038:1*   end
irb(main):039:1*   
irb(main):040:1*   $stdout.sync = true
irb(main):041:1*   
irb(main):042:2*   trap(:CHLD) do
irb(main):043:3*     begin
irb(main):044:4*       while pid = Process.wait
irb(main):045:4*         puts pid
irb(main):046:4*         dead_processes += 1
irb(main):047:3*       end
irb(main):048:3*     rescue Errno::ECHILD
irb(main):049:2*     end
irb(main):050:1*   end
irb(main):051:1*   
irb(main):052:2*   loop do
irb(main):053:2*     exit if dead_processes == child_processes
irb(main):054:2*     puts 1
irb(main):055:2*     sleep 1
irb(main):056:1*   end
irb(main):057:0> end
=> :catch_all_child_processes_deaths
irb(main):058:0> catch_all_child_processes_deaths
1
4626                                             
4627                                             
4628         

参考

workingwithruby.com