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
が変化した時に,comment
state を初期化するために,Effect を使用している。
userId
state が更新された時に,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
が変化する度に,comment
state は初期化される。
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;
参考
2022/07/02
You Might Not Need an Effect
props や state の変化によりコンポーネントを更新したいときに,useEffect を使うべきではない。 以下の二つの場合,useEffect を使うべきではない。
- レンダリングによりデータを変換するとき。
- 例:あるリストを表示前にフィルタリングしたい時。
- リストが変更されたときに,Effect により state を更新したくなるだろうが,これは非効率。
- ユーザーのイベントを処理する時。
外部のシステムと同期するときに Effect が必要となる。
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;
firstName
とlastName
をつなげた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
をトップレベルで定数で定義している。firstName
とlastName
の 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;
この時,email
state が変更された時,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 によりメモ化することで,firstName
とlastName
が変更された時のみfullName
が計算され,email
state が変更されてもfullName
は以前の値がそのまま使われる。
参考
2022/06/25
外部キーのインデックス
rails を使用していると,references
で外部キーに対して自動でインデックスが貼られる。しかし,そのインデックスの役割を正しく認識していなかった。
users
テーブルに対してposts
テーブルがuser_id
を持っている時を考える。posts
がuser_id
に対して,インデックスを貼っていない状態で,where 句にuser_id
を指定して検索すると,posts
テーブル全件に対して検索が走る。しかし,user_id
に対してインデックスを貼ることで,検索対象のレコード数が減少し,高速化が見込まれる。
参考
2022/06/13
eager_load と preload の使い所
includes
eager_load
と preload
を呼び分ける。どちらかが呼ばれるかコントロールしにくいので,基本使わない。実装を読んでみようと思ったが,読み切るには重そうだったので,週末読む。
eager_load
belongs_to
とhas_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つのことを行う。
- プロセスが新しいセッションのセッションリーダーとなる
- プロセスが新しいプロセスグループのグループリーダーとなる
- プロセスはコントロールできるターミナルを持たない
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"
デーモンプロセスの出力先を変更する。
参考
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".*
が指定され,全てのカラムが取得されている。しかし,id
とname
カラムのみが欲しい時は,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"
が指定されており,id
とname
カラムのみを取得できる。
次に,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
テーブルのid
とname
カラム,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 先のテーブルで検索をかけたい時に使用する。
先ほど使用した,users
とdepartments
テーブルを考える。
users
のうち,departments
のname
が"部門_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"
先ほどと同様に,departments
のname
で絞り込みをして,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)
参考
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サーバー。
参考
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.waitpid
,Process.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
補足終了。
子プロセスの終了を見逃さない実装は以下となる。
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