2013年5月23日木曜日

Objectクラスと同期プリミティブ その2

オブジェクトのロック

オブジェクトクラスは、それ自身をロックすることができる。

下記のように lock, unlockで囲むことにより、そのオブジェクトをロックすることができる。
Object_lock(object);

// 相互排他が必要な処理

Object_unlock(object);

オブジェクトがロックされている部分にはは唯一つのスレッドしか入ることができない。
1つのスレッドがオブジェクトをロックしている間に別のスレッドが lockを行おうとすると、そのスレッドは待ち状態になる。
ロックしているスレッドが unlockを実行することにより、待ち状態になっていたスレッドがロックを獲得することができる。
これにより複数のスレッドが同時にオブジェクトにアクセスすることによる競合を防止(相互排他)することができる。

ロック待ち状態のスレッドが複数個あった場合、ロックを獲得していたスレッドがロックを解放したことによりロックを獲得するするスレッドがどれになるかは不定である。(待ち状態に入った順にロックを獲得するとは限らない)
ロックを解放したスレッドが直ちにロック待ちに入るような場合、いつも特定のスレッドだけがロックを取得し、いつまでたっても獲得できないスレッドが生じる可能性があることに留意しなければならない。

複数範囲の相互排他

ロックは1つの箇所だけではなく、1つのオブジェクトに対して複数個所で相互排他を行うこともできる。
たとえば Objectクラスのサブクラスである Dataクラスが、次のように read関数と write関数に相互排他を行った場合、readと writeが複数スレッドで同時に実行することはない。
void Data_read(Data this) {
 Data_lock(this);
 // read処理
 Data_unlock(this);
}

void Data_write(Data this) {
 Data_lock(this);
 // write処理
 Data_unlock(this);
}


複数オブジェクトの間の関係

同じ read, write関数であっても、Dataオブジェクトが異なるものの間では排他は発生しない。
たとえば、おなじ Dataクラスの 2個のオブジェクト data1, data2があったとして、data1が read中に別スレッドで data2が writeするようなことはできる。

他の資源の排他

オブジェクトと任意の資源を組み合わせることにより、ロックしたデータ以外の資源の排他制御も行うことができる。
たとえば次の例は、char配列 arrayは自身をロックする仕組みを持たないが、lockObjectと組み合わせることにより相互排他を行うことができる。
arrayにアクセスするときは lockObjectのロックを取得し、アクセスが終わったときにロックを解放すれば良い。
struct Array {
 Object lockObject;
 char array[128];
};


注意事項

注意が必要なのは、unlockを確実の行うのはプログラマの責任である。
lockされている期間に何の配慮も無く例外を送出したり longjmpを行った場合 unlockされなくなり、閉塞状態に陥る。
また、Objectクラスの初期化が完了するまでは排他制御は保証されない。これは、コンストラクタの中で自オブジェクトをコールバック登録することなどにより、コンストラクタ完了前にコールバックされるような場合に問題が発生する。



最近、開発室の床に転がって眠ってしまい、気づくと朝になってることがある。
倉庫から梱包用エアクッションを持ってきて枕代わりにしていたが、先週日本製枕を買ってきた。

2013年5月16日木曜日

Objectクラスと同期プリミティブ その1

softiesプロジェクトのクラスの基底クラスである Objectクラスについて説明する。
Javaのようにすべてのクラスの基底とするかは未決定であるが、同期プリミティブや等価性の判定の仕組みなど、オブジェクトが持っていると都合のよい機能が提供されることになる。

機能一覧

つぎに示すのはインターフェースである。
インターフェースの構造についてはインスタンス関数のテーブルを参照。
typedef struct interface_lwd_lang_Object {
        lwd_lang_Class class_instance;
        void* (*interface_of)(char* name);

        void (*finalize)(lwd_lang_Object this);
        lwd_lang_Class (*getClass)(lwd_lang_Object this);
        int (*hashCode)(lwd_lang_Object this);
        boolean (*equals)(lwd_lang_Object this, lwd_lang_Object object);
        void (*lock)(lwd_lang_Object this);
        void (*unlock)(lwd_lang_Object this);
        void (*wait)(lwd_lang_Object this, long long timeout);
        void (*notify)(lwd_lang_Object this);
        void (*notifyAll)(lwd_lang_Object this);
        lwd_lang_String (*toString)(lwd_lang_Object this);
}* interface_lwd_lang_Object;


幾つか Javaのそれを真似ている。


finalizeはインスタンスを破棄するときに呼ばれる。
Javaではただ呼ばれるだけで、実際のインスタンスのメモリは別途コレクタにより回収される。
softiesでは、finalizeの中で自オブジェクトおよび自オブジェクトとライフサイクルをともにするオブジェクトのメモリを解放しなけれならない。


hashCodeと equalsは同価性の判断に使用される。
ただし、Objectクラスでは equalsは同一性の評価を行う。
同一性は2つのインスタンスのアドレスが等しいかどうかを表す。
同価性は、2つのインスタンスのアドレスにかかわらず、値が等しいかどうかを表す。
Objectのサブクラスは hashCodeと equalsをオーバライドすることができる。
どちらか一方をオーバライドした場合はもう一方もオーバライドする必要がある事と、equalsが成立する場合の hashCodeは等しくなければならない事は Javaと同じ。


lock, unlock, wait, notify, notifyAllは同期プリミティブである。これらを実現するために、pthreadの mutexと条件変数が使用している。
Cでは synchronizedな関数やブロックを記述することができないため、lockと unlockで代用する。分岐や大域脱出などにより unlock漏れが無いようにするのはプログラマの責任である。


wait, notify, notifyAllはいづれも lockと unlockでくくられていなければならない。
notify、notifyAllにより同一のモニタで waitしているスレッドの実行を再開させる。両者の違いは同一のモニタで waitしているスレッドが複数あるときに、notifyは waitしているもスレッドうちの1つだけを再開させるのに対し、notifyAllは全てを再開させることである。
notifyでどのスレッドが再開するかは特定することはできない。waitした順とは限らないし、いつも同じスレッドに偏るかも知れない。
notifyはスレッドプールのように要求を処理するスレッドを割り当てるために使用される。
notifyAllは複数のスレッドでタイミングを同期させるために使用される。



実質的増税が続くなか、関税は下がる傾向にある。 関税も上げよ。

2013年4月21日日曜日

libdwarfのインストール

softiesプロジェクトで使用するかは未だ決めてないが、実験のため Open Solarisに libdwarfをインストールしてみた。

ダウンロード (download)下载

David A's DWARF Pageから
libdwarf-20130207.tar.gzをダウンロードした。

ビルド (build) 构建

tarで展開し、dwarf-20130207にチェンジディレクトリして
sudo ./BLDLIBDWARF

すると自動的にビルドが始まるが、dwarfdumpをビルド中にコケてまう。

# gcc -E tag_tree.list does not work, so use a .c name
rm -f  tmp-t1.c
cp ./tag_tree.list tmp-t1.c
gcc  -g -O2 -I. -I. -I./../libdwarf -DCONFPREFIX=/usr/local/lib  -E tmp-t1.c
  > ./tmp-tag-tree-build1.tmp
./tag_tree_build -s  -i tmp-tag-tree-build1.tmp  -o tmp-tt-table.c
make: *** [tmp-tt-table.c] Segmentation Fault (core dumped)
make: *** Deleting file `tmp-tt-table.c'

欲しいのはライブラリとヘッダのみのため、libdwarfの中だけビルドする。


./configure, makeすれば良いのだが、configure時のオプションでビルドするライブラリのタイプを選択できる。

ライブラリタイプ.configureのオプション
libdwarf.aオプションなし
libdwarf.so--enable-shared --disable-nonshared
両方--enable-shared

インストール (install) 安装

make installは提供されてないので、ハンドコピーする。

ファイルインストール先ディレクトリ
libdwarf.h, dwarf.h/usr/local/include
libdwarf.a, libdwarf.so/usr/local/lib

libdwarf.hは展開したファイルには無く、configure時に libdwarf.h.inからコピーされる。

2013年4月11日木曜日

コーディング規約 その3 (code conventions 3)

コーディング規約の 3回目は、ソフトウェアメトリクスに関するものである。

我々は、2005年にはメトリクスの計測ツールを導入したが、その後、使用するツールの都合により一時的に自動的な計測を停止している。
それでも、以下に掲げる規約は有効である。

なお、これらの基準を逸脱しなければならない場合は、実験的なプロジェクトの場合を除いて社内レビューを行い、その妥当性の検証が必要となる。

関数の大きさ

1個の関数(メソッド)の大きさは、7行までが理想で、多くともこれに +2した 9行が良い。

ただし、言語によっては使用できる構文が違うため、多少差をつけることにしたい。
  • Java -- 12行
  • c++ -- 24行
  • c -- 30行
  • ほか -- T.B.D.
1980年代半ば、私が学生アルバイトをしていた会社の社長と、関数(アセンブリ言語だったのでサブルーチンと読んでいたが)を小さくすると各々は単純になるが、数が増えたり、レベルが深くなって判りにくくなることを議論した記憶がある。そのときには良い答えを見つけることはできなかった。

2000年代に入っても、プロジェクトのメンバにリファクタリングして関数を単純にしてもらえないか相談したときに、彼らから同じような答えが返ってきた。 彼もまた、解決法を見つけていないようだ。

大事なのは関数が大きくなればなるほど「関数を分けなければならないのではないか?」という疑問を持つことである。

上記の基準はちょっと厳しいと思われるかもしれないが、頭の中でトレースするのにはあまり長くないほうが良い。

ほかの考え方としては、できれば 1個の関数を読むために、左右はもちろん、上下にもスクロールしたくない。

引数の数

気軽に使ってよいのは 3個まで。
Immutableなオブジェクトをコンストラクタで全部初期化したい場合でも 7個まで。

ローカル変数の数

3個までに収めるのが理想。言語別上限はつぎの通り。
  • Java -- 5個
  • C++ -- 7個
  • C -- 7個

制御構文の深さ

if, switch-case, for, while, do, try-catchおよび、 ? : 演算子を合わせて 3レベル。

厳しいと思われるかもしれないが、これを守るとコードが読みやすくなる。

使われ方としては、
  • tryを使う場合には、中に一重のループとその中に if文が 1レベル。
  • 二次元データを扱うにはループを二重に使いたい場合があるので、そうするとあと if文が 1レベル。
  • switch-caseの中に if文があって、その中で ? : 。
なお、3レベル以内だったとしても switch-caseの中で switch-caseを使用するのは非推奨。(我々の規約では 8タブを使用するので良いが、 4タブだとどの caseがどの switchに対応するか追跡するのが面倒になり、間違いの元になる。

クラスの大きさ

メンバの変数や関数の数に制約は無いが、空行、コメントを合わせて理想は 500行まで。最大 1000行まで。

大事なことは、凝集度。

分けてはいけないものを分けると無駄な相互作用が発生し、そのためのアクセサが増えるだけでなく、そのアクセサが外部に公開されてしまうことが適切かどうかという問題が生ずる。

逆に一緒である必要のないものを1つにしてしまうと、ある部分の変更が外の部分に影響が及ばないか検証する範囲が広がり、無駄なコストが発生することである。

目覚めよ!正恩。

2013年4月9日火曜日

コーディング規約 その2 (code conventions 2)

前回に引き続き、弊社のコーディング規約を紹介する。

なお、命名規約はローカル変数やプライベートなメンバを除いて設計規約の一部として扱っているため、コーディング規約には現れない。

マクロ置換の禁止 (Prohibition of the macrosubstitution)

マクロはしばしば取り上げられるような引数評価の副作用が問題であるだけでなく、可読性を悪化させる原因にもなる。
本機約では、例外的に標準ライブラリを使用する上で必要となる最小限の使用を認めているが、それ以外の使用は原則的に禁止である。
softiesプロジェクトではこの原則を 2つ破っている。

一つは、importを実現するためのもの。

もう一つは、例外機構の try, catch, finallyである。

前者は置換のルールが単純で混乱を起こすことは少ないと思われるが、後者は危険が多い。そのため例外機構は完全な実験状態であり、ライブラリ中で使用している箇所はまだないのである。
外にも publicや privateなどの可視性を表すものなどがあるが、使用するかしないかは選択できるようになっている。


Javaではアノテーションが追加されるまではコンパイル制御機能はソースコード上に記述できなかったため、記号定数は全てクラスの定数として使用される。
問題は、 C/C++の場合である。
本規約では、記号定数は全てマクロではなく定数 (constant value)として定義しなければならない。
関数型マクロは、C++の場合テンプレートで置換することができる。
Cにはテンプレートが無いため、全て通常の関数を記述しなければならない。多態性実現のために関数型のマクロをしては成らない。
なお、C++のテンプレートもデバッグを難しくする恐れがあるため、慎重に定義しなければならない。それが難しい場合には C同様にいちいち通常の関数を記述することになる。


記号定数の置換のメリットは、単にリテラルをロジックから排除することによる可読性の向上だけでなく、使用されない定数が多くある場合には通常の定数を用いるよりもメモリ削減になる点である。
我々はそのメリットを犠牲にしても、混乱を避けることを選択した。

条件付コンパイルの禁止 (Prohibition of conditional compilation)

#if, #ifdefなどの条件付コンパイルはマクロ置換よりもさらに可読性を悪化させる。
そのため本規約では、多重インクルード防止目的のもの意外の使用は禁止である。

製品コードの中に #if 0 などが含まれるのは論外で、仮にデバッグ用に使用されたとしてもコミット時までには除去されなければならない。
コードが複数の機種や仕向けの仕様を共通で使用するためにも #ifが仕様されることがあるが、弊社では禁止である。
バージョンの切り替えも禁止である。
これらは、デザインパターンで回避するか、適切な構成管理を行うことで解決が可能である。
なお、回避、解決法は何通りかのパターンがあるため、ここでは触れないが、機会があれば紹介したい。

goto文の使用 (using goto statement)

多くの場合、可読性を低下させる。
エラーハンドリングでは gotoを使ったほうがコードがすっきりするという考え方もある。
実際、μITRONのある実装では gotoを使った場合の効果的なエラーハンドリングのコードを見たことがあるが、誰がやってもうまくいくとは限らない。特に関数の中の構造が複雑だとうまくいかない。
弊社の設立前、1990年代中ごろに筆者が携わったものの中にすさまじいコードがあった。それは国内のある会社が実装した CADDAM用のカスタムコード(プラグインソフト)が正常に動作しないため改修を請け負ったのだが、1個の switch caseが 6千行あり、そのほとんどの caseが breakではなく gotoで終わっているような代物だったのである。
goto文を breakに置き換えてみるとなんと、相対ジャンプの距離が遠すぎてコンパイルエラーになるではないか。
つまり、元の作者はそのエラーを逃れるために gotoを用いたようだ。
このような使い方をしなければならない状況と言うのは、何か怪しいのである。まず、保守不可能と言ってよいだろう。


話が遠回りしてしまったが、本規約では goto文の使用は C++では禁止。Cでは禁止しないが、限りなく非推奨に近い。


例外処理機構を有効に用いる手段ができれば、Cでも禁止することになるだろう。

do while文

do while文の使用は禁止していないが、for文、while文を優先的に使用することが推奨される。

定数との比較

定数との比較時に、誤って代入してしまうのを防止するために、しばしば比較する定数を左辺に記述する規約が存在する。
例) WRONG
    if (0 == value)
        ...

本規約では、このような書き方は禁止であり、つぎのように記述しなければならない。
例) RIGHT
    if (value == 0)
        ...

これは、valueが 0と等しいかどうかを判定したいのであり、0が valueと等しいかを判定したいのではないからである。
2000年頃に私が隣のプロジェクトのメンバとした議論の中では、前の書き方でも『0 と valueが等しいかと読めばよいだろう。』という意見がったが、比較するものの主語がどちらかを考えると 0 == valueでは不適切である。


我々の記述方法では誤って値を代入してしまうではないかと思われるかもしれないが、徹底した単体テストで補うことで解決している。
なお、Javaでは、 if (value = 0)と書いてしまってもコンパイルエラーになるため、バグになることは無い。

ブール値との比較禁止

ブール値の判定に比較演算子を用いてはならない。
たとえば、次の例は禁止である。
例) WRONG
    if (files.hasNext() == true)
        ....

この例では、==と trueの組み合わせであるからまだマシなほうで、これが !=や falseとの組み合わせであったら読み手が理解しにくい。

つぎの例のように、定数との比較を行わないことが正しい。
例) RIGHT
    if (files.hasNext())
        ...

hasNextは「次を持っている」と読めるので、何も trueと比較して等しいことを確認する必要は無いのである。
逆に言えば、関数名を付ける場合には hasXXXや isXXX、containsのように「読める」名前を使用しなければならない。

悪い関数名の代表は check()。 チェックしてどうなったら trueなのか、ドキュメントを参照しなければ理解できないだろう。

我々が使用する関数名で validate()というのがあるが、これはブール値を返すのではなく受理されない場合には例外を返すため問題ではない。

関数の途中からの脱出

原則として関数の途中脱出は禁止である。

これには幾つかの例外がある。つぎの場合の途中脱出は認められる。
  • 関数の冒頭で引数や状態異常を検出したことによる return文によるリジェクト
  • 例外がスローされる場合
このほかに exit()もあるが、正常終了であれ異常終了であれ、可能な限り関数の最後に exit()を記述しなければならない。


鳥インフルエンザが流行りそうだな。万一のときはドクターに頼んでコルドラジンをほんの数滴打ってもらうことにしよう。
Bird flu seems to be popular. Let's decide to have I ask a doctor at the time of emergency and inject just several drops of Cordrazine.