「コーディングを支える技術」を読みました(3)
勉強記録
今日は「コーディングを支える技術」を読みました。
第10章から第12章にかけて、重要そうなところや新たに学んだことを中心にまとめました。
<第10章> 並行処理
並行処理とはなにか
複数の処理を時間軸でオーバーラップして実行することを並行処理という。
並行処理を実現するために、プロセスやスレッドなどの概念が誕生。
並列処理を行うことで新しい問題もでてきたが、それに対処するためにロックやファイバーなどの概念が発明された。
細かく区切って実行する
複数の処理を同時に実行するには、人間が気づかないくらいの短い間隔で、複数の処理を切り替えながら実行する。
処理を切り替える方法
「どういうときに処理を交代するのか」の決め方は2通りある。
協調性マルチタスク
協調性マルチタスクは切りのいいところで交代する方法。
しかし、協調性マルチタスクではどれか1つの処理が「交代していいよ」と言わない限り、他の処理にいつまで経っても交代しないという問題がある。
あくまで「すべての処理が、適度な間隔で交代して良い、と言う信頼のもとで成り立っている」システム。
プリエンプティブマルチタスク
プリエンプティブマルチタスクは一定時間で交代する方法。
一定時間ごとに今走っている処理を強制的に中断させて、ほかのプログラムが処理を実行できるようにする。
プリエンプティブは「他人の行動を阻止するための」という意味で、協調性マルチタスクと違って「止められる側のプログラムの協力」がなくても強制的に処理を止めてしまう。
競合状態を防ぐには
プリエンプティブマルチタスクでは、いつ「交代しなさい」と言われるかわからない状況で、ちゃんと動くプログラムを書くのは難しいことだった。
並行処理では、競合状態(レースコンディション)が起きることがあり、このプログラムは「スレッドセーブでない」と表現したりもする。
競合状態の3条件
1. 2つの処理が変数を共有している
2. 少なくとも1つの処理がその変数を書き換える
3. 一方の処理がひと段落落ち着くまえに、もう一方の処理が割り込む可能性がある
この3つの条件のどれか1つでもなくせば、並行処理を実行しても安全なプログラムを書くことができる。
共有しない
プロセスではメモリを共有しない
UNIXでは実行中のプログラムを「プロセス」と呼ぶ。異なるプロセスはメモリを共有しない。
データベースへの接続、ファイルの読み書き、など何かを共有したときだけ気をつける。
のちに「軽量プロセス」がつくられ、これが「スレッド」と呼ばれるようになった。
今でもスレッドを使って、共有メモリの扱いに悩みながらプログラムが書かれている。
アクターモデル
メモリを共有しない設計方針で、もう一つの流れがアクターモデル。
書き換えない
メモリを共有しても、書き換えなければ問題ないというアプローチもある。
すべての値が変更不可能であるHaskellなど。
Scalaではvarとvalの2通りの変数宣言があり、valで宣言したものは書き換えられない。
割り込まない
協調的なスレッドを使う
ファイバーやコルーチン、グリーンスレッドなどと呼ばれている手法。
協調的なスレッドを作れば良いというアプローチ。
割り込まれると困る処理中は印をつける
「今割り込まれると困る」という印を共有する方法。
ロックの問題点
デッドロックが発生してしまう
デッドロックはお互いにロックが解放するのを待ってしまい、処理が進まない状態のこと。
何をロックすべきかだけではなく、どういう順でロックすべきかも把握しなければならない。
<第11章> オブジェクトとクラス
言語によってオブジェクト指向の意味が違う
少なくとも2人のオブジェクト指向言語の設計者が、「オブジェクト指向」という言葉を全く違う意味に使っている。
特に型と継承に関しては意見が逆。
クラスが持つ3つの役割
1. まとまったものを作る生成器
2. どういう操作が可能かという仕様
3. コードを再利用する単位
1は、よく言われる「クラスはたい焼きをつくるためのたい焼きの型」という表現。
2については、Javaではこの役割に特化した「インターフェース」が作られた。
3については、継承がそれにあたるが詳しくは12章で。
<第12章> 継承によるコードの再利用
継承は諸刃の剣
クラスを継承して差分を実装するというコーディングスタイルは、深い継承ツリーを作り出し、コードをわかりにくくしてしまう可能性が高い。
コードの再利用のために継承を行うのではなく、親子関係のために継承を行う。
実際にGo言語では継承がない。