次にソフトウェアエンジニア採用した際に教材にしたい本(基礎教養部分) - terurouメモ

次にソフトウェアエンジニア採用した際に教材にしたい本(基礎教養部分) - terurouメモ


次にソフトウェアエンジニア採用した際に教材にしたい本(基礎教養部分)

受託の仕事がひと段落したので、現実逃避がてら内容が古い・良くない社内図書の整理を行っていた。整理しながら「そういえば基礎教養部分の社員教育カリキュラムがまったく準備できてないなぁ」と思い、社内図書をベースに教育用に課す本を考えてみた。

以前から「日本語作文能力が足りない〜」と言ってきていたのだが、それに対しての現時点の暫定解がこれかなぁと思っている。

世界一やさしい課題解決の授業


とりあえず最初に読む本。ごく当たり前の話がサクっと書いてあるが、意識がないと全く身についてない話でもある。

読むだけなら数時間(読むのが早い人であれば、30分もあれば読める)、演習もやるなら1日〜1.5日程度。

デキる大人の文章力教室(暫定、省略可)

デキる大人の文章力教室
小林 洋介
日本文芸社
売り上げランキング: 118,760


類書は多いので、別にこれでなくてもよい。軽く数時間〜半日読んで、手元に置いて必要時に読み返せれば十分。

新卒採用の初っ端によく見る「学生っぽい幼稚な文章表現」を矯正する(認知させる)ために使う。切り口は違うのだが、後述の「数学文章作法」と被る内容も多いので、教育時間がなかったり、2年ぐらいの業務経験がある人なら省略してもいいかもしれない。

数学文章作法 基礎編・推敲編

数学文章作法 基礎編 (ちくま学芸文庫)
結城 浩
筑摩書房
売り上げランキング: 25,822




数学文章作法 推敲編 (ちくま学芸文庫)
結城 浩
筑摩書房
売り上げランキング: 28,004


THE 基礎教養。仕事で日本語文章を書く全ての人間は読むべき。

ページ数が少ない本ではあるが、1日〜2日程度かけてじっくり読んだ方がいい。時間的に早く読み終わってしまった場合は、休憩を挟んで読み直しさせるのもいいぐらい。

類書に有名な「理科系の作文技術」があるが、今なら「数学文章作法」だけで十分だと思う。「理科系の作文技術」は名著であることは間違いないのだが、エピソードトークみたいなものが若干多いせいで内容が分かりづらくなっている面もあり、「数学文章作法」の登場によって役目は果たしたのかなと思う。

「聞く技術」が人を動かす(暫定、省略可)

「聞く技術」が人を動かす―ビジネス・人間関係を制す最終兵器 (知恵の森文庫)

伊東 明
光文社
売り上げランキング: 172,217


端的には「相手を怒らせない」「相手のモチベーションを下げない」受け答えの仕方が書かれた本。教育側が「他人との会話することが得意ではないな」と感じた場合は読ませた方がよい。話すのが得意なタイプであれば省略しても構わない。

数時間〜半日程度で読んだら、手元に置いておいて何度も見返すタイプの本。ただ、この本は若干読みづらいなとは思っているので、もっと良い類書が見つかれば差し替えると思う。

メール文章力の基本 大切だけど、だれも教えてくれない77のルール(暫定、省略可)


類書は多いので、これも別にこの本でなくても構わない。これも数時間〜半日程度読んで、手元に置いて必要時に読み直すタイプの本。

新卒採用の場合は必読として、中途採用でもメール文章を書くのが苦手な人(もしくはあまり書いてきてない人)は割と見かけるので、念のため読んでもらった方がいい。仕事で出すメールなんてほとんどのケースが定型なのだけど、定型文をちゃんと学習できてないことで、メールの往復回数が無駄に多くなって時間を無駄にしたり、「お前それ喧嘩売ってるよね」みたいな文章を平気で出してしまってトラブルになったりを未然に防ぐことができる。

顧客折衝をガンガンやってきましたみたいな人の場合は、当然読まなくてよい。

考える技術・書く技術

考える技術・書く技術―問題解決力を伸ばすピラミッド原則
バーバラ ミント
ダイヤモンド社
売り上げランキング: 1,351


ロジカルシンキング・ロジカルライティングの名著。前述までの本とは違って、一気に難易度が上がるのだが、世の中のロジカルシンキング・ロジカルライティング本は大体これの派生みたいな感じがあるので、最初からこれを読んだ方がよい。

できれば1週間ぐらいかけて、要約しながら読むという感じで進めたい。

プログラムは技術だけでは動かない ~プログラミングで食べていくために知っておくべきこと(省略可)


これは厳密には基礎教養というよりかは、おっさんが話す「やらかさないようにこうした方がええで」とか「業界で食ってくならこういう立ち振る舞いをしたほうがええで」的な経験則に沿った内容を体系立てて1冊にまとめた本というべきか。受託開発ありきな内容に若干偏っているところ

Node.jsでのイベントループの仕組みとタイマーについて - 技術探し

Node.jsでのイベントループの仕組みとタイマーについて - 技術探し


Node.jsでのイベントループの仕組みとタイマーについて - 技術探し

  • イベントループ
    • イベントループとは?
    • libuv
    • タスク
    • イベントループの仕組み
      • フェーズ
        • イベントキュー
      • nextTickQueue / microTaskQueue
        • nextTickQueue
        • microTaskQueue
      • Timers Phase
      • Pending Callbacks Phase
      • Idle, Prepare Phase
      • Poll Phase
      • Check Phase
      • Close Callbacks Phase
      • 第一ラウンド
      • 第二ラウンド
      • 第三ラウンド
    • まとめ
  • Timer
    • setTimeout(() => {}, 0)
    • 順番を操作する
  • まとめ
  • リファレンス

イベントループ

f:id:about_hiroppy:20180925085430g:plain

イベントループとは?

イベントループとは、JavaScriptがシングルスレッドなのにもかかわらず、効率よくノンブロッキングI/Oを実行できるようにする仕組みです。

イベントループはメインスレッドで実行されます。

ブラウザのイベントループとは異なるので注意が必要です。

Node.jsのイベントループはlibuvに基づきます。

ブラウザのイベントループはhtml5に基づきます。

libuv

Node.jsの非同期はカーネルと会話するためにlibuvを使います。

もともと、Node.jsのために作られたものですが、今は様々なところで使われています。

libuvとは、非同期I/Oに強く、クロスプラットフォーム対応の抽象化ライブラリです。

基本的には、イベントループと非同期処理を行います。

f:id:about_hiroppy:20180926003353p:plain

libuvは、Node.jsにイベントループ機能全体を提供しています。

デフォルトでは、デフォルトサイズが4のスレッドプールを作ります。

Design overview — libuv documentation

イベントループのコードは以下を参照してください。

github.com

タスク

タスクは、同期タスクと非同期タスクの2種類存在します。

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
(() => console.log(5))();


  • 同期タスク

    • (() => console.log(5))();
  • 非同期タスク

    • setTimeout(() => console.log(1));
    • setImmediate(() => console.log(2));
    • process.nextTick(() => console.log(3));
    • Promise.resolve().then(() => console.log(4));


同期タスクは常に非同期タスクよりも早く実行されます。

また、EventEmitter で発生するイベントはタスクとは呼びません。

このコードの出力は以下の通りになります。

5
3
4
1
2


なぜこのような順番で出力されるかは、次のイベントループの説明でわかるはずです。

イベントループの仕組み

Node.jsが起動すると以下のイベントループが初期化されます。

f:id:about_hiroppy:20180925232715p:plain

http://voidcanvas.com/nodejs-event-loop

https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#event-loop-explained

初期化はされますが、開始前に行われることがあります。

  • タイマーのスケジュール設定
  • process.nextTick等の実行
  • 同期タスクの実行
  • 非同期APIの呼び出し


上記が終わり次第、イベントループが開始されます。

注意点として、イベントループは複数のtaskを同時に処理することはできないため、キューに入れられ順次処理をされるようになっています。

つまり、一つのタスクが完了する時間が長いと健全ではない(イベントループに遅延が出る)ということになります。

また、Node.jsではタスクキューの処理にOSのカーネルが依存しているため、タスクを受け取った瞬間を判断することは不可能で、準備ができている場合のみを知っています。

フェーズ

イベントループには6つのフェーズが存在します。

  • timers
  • pending callbacks
  • idle, prepare
  • poll
  • check
  • close callbacks


JavaScriptの実行は、idle, prepare を除くどこかのフェーズで実行されます。

それぞれフェーズには、実行するコールバックのFIFOジョブキューを持ちます。

そのフェーズに入るとそのフェーズの処理が実行され、キューが処理されます。

そして、キューがemptyになるかコールバックの上限に達したらイベントループは次のフェーズへ遷移します。

libuvとの関係図です。

f:id:about_hiroppy:20180925214205j:plain

Handling IO — NodeJS Event Loop Part 4 – The JS Blog

libuvは、各フェーズ毎にフェーズの結果をNodeに伝達する必要があります。

このときにnextTickQueueとmicroTaskQueueに入れられたイベントのキューをチェックします。

もし、キューが空ではない場合は空になるまでキューの処理を行い、メインのイベントループのフェーズへ移行します。

つまり、各フェーズ後(フェーズが移行する前)にnextTickQueueとmicroTaskQueueが実行されるということです。

フローは以下の図のような感じになります。

f:id:about_hiroppy:20180925083813p:plain

What you should know to really understand the Node.js Event Loop




イベントキュー

libuvから提供されるキューとNodeが提供するキューの6種類があります。

  • libuv

    • Expired timers / intervals queue 
    • IO Events Queue 
    • Immediates Queue 
    • Close Handlers Queue
  • Node

    • nextTick Queue
    • microTask Queue


nextTickQueue / microTaskQueue

code: ib/internal/bootstrap/node.js#L77-L78

code: lib/internal/process/next_tick.js

先にnextTickQueueとmicroTaskQueueの説明をしたいと思います。

この2つはlibuvによる提供ではなく、Nodeにより実装されています。

先程の説明の通り、イベントループの各フェーズの後にnextTickQueueとmicroTaskQueueに入れられたイベントのキューをチェックし、空になるまで実行します。

また、この2つはイベントループのフェーズの一部ではないことに注意してください。

nextTickQueue

process.nextTickを使用して登録されたコールバックを保持します。

すべての非同期タスクの中で最速となります。

nextTickは再帰的に呼び出すとNodeをブロックする可能性があるため注意です。

microTaskQueue

Promisesオブジェクトのコールバックはここに所属します。

microTaskQueueに入っているPromiseはV8によって提供されるネイティブのみが適用対象とされます。

イベントループと同様にnextTickQueueが空になり次第、実行となります。

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
process.nextTick(() => console.log(5));




1
3
5
2
4


先にnextTickQueueが消費されているのがわかります。


ここまでの説明で、以下がなぜこの順番になるのか半分ぐらいわかるかと思います。

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3)); // 2
Promise.resolve().then(() => console.log(4)); // 3
(() => console.log(5))(); // 1


次からは各フェーズの説明を行っていきます。

Timers Phase

code: src/timer.c#L147-L164

イベントループの開始フェーズです。

このフェーズでは、setTimeoutsetInterval のタイマーのコールバックを実行します。

タイマーを最小ヒープに保持し、Nodeは有効期限が切れたタイマーを確認し、コールバックを呼びます。

複数の有効期限が切れたタイマーが存在する場合、登録した順番に実行されます。(FIFO)

OSのスケジューリングや他のコールバックの実行により遅延が発生する可能性があり、Node.jsではコールバックの実行する正確なタイミングや順序付けは保証されません。

指定された時間のできるだけ近い時間で呼び出されます。

https://nodejs.org/api/timers.html#timers_settimeout_callback_delay_args

const start = process.hrtime();

setTimeout(() => {
const end = process.hrtime(start);

console.log(`timeout callback executed after ${end[0]}s and ${end[1]/Math.pow(10,9)}ms`);
}, 1000);




# output
timeout callback executed after 1s and 0.0070209ms
timeout callback executed after 1s and 0.004651383ms
timeout callback executed after 1s and 0.001348922ms


毎回異なる結果となり、0ms となることはありません。

Pending Callbacks Phase

code: src/unix/core.c#L765-L784

イベントループのpending_queueに存在するコールバックを実行するフェーズです。

完了、エラーのI/O操作のコールバックが実行されます。

pollフェーズの最後のラウンドで実行されるコールバックは実行できず、このラウンドのpending callbacksフェーズまで延期となります。

Idle, Prepare Phase

code: src/unix/loop-watcher.c#L48-L60

libuvによって内部的に呼び出されるフェーズです。

次のフェーズであるPoll Phaseが開始されるたびにPrepareも実行されます。

Poll Phase

code: src/unix/posix-poll.c#L134

このフェーズは、サーバの応答、まだ返されていないI/Oイベントを待機するために使用されるポーリング時間です。

新しいソケットコネクトやファイルの読み込みなどの新しいI/Oイベントを取得し、実行します。

このフェーズでは、以下の2つのことを行います。

  • I / Oをブロックしてポーリングする時間を計算する
  • キュー内のイベントを処理する


ポーリングする時間を計算します。(これは様々な状態によって結果が変わります)

I/Oの処理をシステムコールの epoll のキューに全て登録します。

epoll_waitシステムコールを呼び、ポーリングを行います。

完了したら、コールバックを呼びます。

キューになにか存在する場合、キューがemptyになるかシステム依存の限度に達するまで順次同期実行を行います。

キューが空の場合、以下の2つのうち1つが実行されます。

  • スケジューリングされている場合、イベントループはこのフェーズを終了し、次のcheckフェーズへ進みスケジュールされたスクリプトを実行する
  • スケジュールされていない場合、イベントループはコールバックがキューへ追加されるのを待ち実行する


Check Phase

setImmediateのコールバック専用フェーズです。

setImmediateで登録されたすべてのコールバックを実行します。

timerフェーズのものとは異なり、専用のフェーズがあるため、必ず実行が保証されます。

つまり、pollフェーズで実行されていたコールバック内にsetImmediateが存在すれば、setTimeoutよりも先に呼ばれることが保証されます。

Close Callbacks Phase

code: src/unix/core.c#L293-L305

すべての close イベントのコールバックが処理されます。(e.g. readable.on('close', () => {}))

もし、キューに処理するものがなければ、ループが終了となります。

存在すれば、timerフェーズへ遷移します。

const { readFile } = require('fs');

const timeoutScheduled = Date.now();

setTimeout(() => {
console.log(`delay: ${Date.now() - timeoutScheduled}ms`);
}, 100);

readFile(__filename, () => {
const startCallback = Date.now();

while (Date.now() - startCallback < 500) {}
});




# output
delay: 502ms


このコードをぱっと見た時に、100ms後にdelay: 100msと出力されるだろうと思うかもしれません。

このコードのフローを説明します。

第一ラウンド

スクリプトが最初のイベントループに入ったときには、まだ有効期限が切れたタイマーが存在しておらず、実行可能なI/Oコールバックも存在しません。

つまり、この第一ラウンドはポーリングフェーズに入り、カーネルからのファイル読み込み結果を待ちます。

このときは、ファイルの読み込みが軽量であり、タイマーよりも早く結果を取得します。

例えば、setTimeoutに時間を100ではなく0や1にしていた場合、ファイルの結果よりも先にタイマーの有効期限が切れるため、次のループで結果が変わります。

第二ラウンド

今回は、100msでやっていて、ファイルの読み込みのほうが速く、まだtimerの有効期限が切れてません。

もし、0や1であれば、delayが出力されていたでしょう。

すでに、ファイルは取得できているため、pending callbacksフェーズに入ります。

このコールバック内では、500msの同期処理を実行させています。

そして、このコールバックはジョブキューに入っており、次のフェーズへ移行するには、キューを空にする必要があります。

なので、ここで500msの遅延(500msを停止させた)が発生したということになります。

無事、500msの実行が終わったらキューが空になるため、次のイベントループへ移行します。

第三ラウンド

すでに第二ラウンドの遅延により、タイマーの有効期限が切れるため、setTimeoutはタイマーフェーズ中に実行され、delayを出力し終了します。

The Node.js Event Loop, Timers, and process.nextTick() | Node.js

まとめ

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
(() => console.log(5))();




5
3
4
1
2


なぜこの順番の出力になるかは上記のイベントループの流れでわかるかと思います。

1. 同期タスク: (() => console.log(5))();
2. 非同期タスク::nextTickQueue : process.nextTick(() => console.log(3));
3. 非同期タスク::microTaskQueue: Promise.resolve().then(() => console.log(4));
4. 非同期タスク::timers phase: setTimeout(() => console.log(1));
5. 非同期タスク::check phase: setImmediate(() => console.log(2));



  • イベントループはメインスレッドで実行される
  • イベントループは複数のタスクを実行できず、キューに入れられたのを順次処理する
  • イベントループには6つのフェーズが存在する
  • フェーズが遷移する前にnextTickQueueとmicroTaskQueueが実行される


Timer

Node.jsで使えるタイマーは以下の4つとなります。

setImmediateprocess.nextTick はNode.js固有でありブラウザにはないことに注意してください。

setTimeout
setInterval
setImmediate
process.nextTick


setTimeout(() => {}, 0)

setTimeout(() => console.log('setTimeout'));
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));


上記の出力は以下のようになります。

nextTick
setTimeout
setImmediate


nextTickが一番最初に来るのは最初に説明したとおりです。

そして、timersフェーズが来て、checkフェーズなのでこのような出力となります。

しかし、この出力は保証された出力ではありません。

timerフェーズに入ったときに有効期限が切れたかわからないためです。

さて、setTimeout0の時の遅延はどれぐらいなのでしょうか?

第二引数の範囲は、1msから2147483647msと決められており、範囲外の指定をしたときには、1msとなるように規定されています。

つまり 0のときは1msより大きい値となります。

Timers | Node.js v10.11.0 Documentation

ちなみに、setTimeout を 4msに指定したら自分のPCではsetImmediateが先に出力されるようになりました。

setImmediate は、pollフェーズ後に保証された実行ができるため、使う場面によっては、有用な使い方が可能となります。

順番を操作する

const { readFile } = require('fs');

readFile(__filename, () => {
setTimeout(() => console.log('setTimeout'));
setImmediate(() => console.log('setImmediate'));
});




setImmediate
setTimeout


上の例だと必ず setImmediate が先に出力されるようになります。

それは、最初にpending callbacksフェーズに入り、その次がcheckフェーズだからです。

timersフェーズは過ぎてしまっており、次のループなため出力が遅れるのです。

まとめ

ブラウザとは違う部分がありますが、macroTasksやmicroTasksの考えなどは同じ部分があります。

ちなみにブラウザはこの記事がわかりやすいです。

jakearchibald.com

動画はこちら

Philip Roberts: What the heck is the event loop anyway? | JSConf EU - YouTube

イベントループは理解するまで難しいコンセントではありますが、一度理解すればコードの理解が深まったり、最適化できたりします。

(よく言われる「わからないでnextTickを使うのは危険」っていう話とか)

なにかあれば、Twitterまでどうぞ!

リファレンス