2013年6月30日日曜日

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

条件変数 (condition variable)

あるスレッドで変数を更新し、それを別のスレッドで待って処理を行うために Objectクラスのモニタ機構を用いることができる。
このモニタによって更新の通知と待ち合わせが行われる変数を条件変数と呼ぶ。

変数の更新を待つスレッドはつぎのように記述される。
 Object_lock(object);
 Object_wait(object, 0L);
 // objectに関連付けられた変数を読み出す
 Object_unlock(object);

Object_lockによりロックが行われると Object_waitにより更新されたことの通知を待つ。
この間一時的にロックは自動解除される。そうしないと、更新する側もロックを取得しようとするため、更新動作に入れなくなる。
重要なのは、再び waitを行っていたスレッドがロックを獲得した状態で Object_waitから復帰するということである。
Object_waitの 2つ目の引数は待ち時間の上限をミリ秒単位の long値で指定する。 0Lを指定した場合は通知が行われるまで無限に待ち続ける。
条件変数は任意の変数で構わないが、objectが Objectのサブクラスであれば、条件変数はそのクラスのメンバとするのが良い。
変数を読み書きする処理をそのクラスの関数にする事で処理の詳細を隠蔽し、併せてその関数の中でロックも行うようにすることができる。
ロック操作を利用者側に任せるのはロック漏れの原因となる。
変数を読み出している最中に更なる変数の更新が行われないようオブジェクトをロックしなければならない。

変数を更新するスレッドはつぎのように記述される。
 Object_lock(object);
 // objectに関連付けられた変数に書き込む
 Object_notify(object);
 Object_unlock(object);

Object_lockと Object_unlockで囲まれた中で変数を更新し、Object_notifyで objectで何かが起こることを待っているスレッドを再開させる。
最後に Object_unlockでロックを解放する。
Object_lockを行おうとしたときに、他のスレッドが変数に書き込み中だったり、読み出し中のものがあればそれらが完了するまで待たされる。
objectを待っているスレッドが複数あるとき、Object_notifyはその中の 1個のスレッドだけを再開させる。
どのスレッドが再開されるかは処理系の実装依存である。最初に待ち状態に入ったものが解除されることを期待するようなコードを書いてはならない。
objectを待っている複数のスレッド全てを再開させるには Object_notifyの変わりに Object_notifyAllを使用する。

なお、softiesプロジェクトでは Object_lockは必ずロックを獲得した状態で復帰する。
ロックが獲得できるかどうか試してみる tryLockの仕組みは提供していない。
他のスレッドから割り込まれてキャンセルされた場合例外をスローすることを検討されているが、現在のところ例外機構は実験段階であり、キャンセルは行われない。



アンタって少な目。 半分の整髪。

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つにしてしまうと、ある部分の変更が外の部分に影響が及ばないか検証する範囲が広がり、無駄なコストが発生することである。

目覚めよ!正恩。