2006年02月13日

非同期の処理

例えば、Series60を用いてネットワークを用いたサーバアプリケーションを構築するとしましょう。この際、サーバはクライアントからの接続を待つ必要があります。この「接続を待つ」という行為は明確なタイミングがわかっているものではありません。このような場合、通常は別にスレッドを立て、クライアントからの接続を監視させる事を考えると思います。

また、同一アプリケーションで何らかの時間のかかる処理を走らせている間もユーザーの入力は受け付けなくてはいけないという状況はあるはずです。このような場合も普通に考えるのであれば別にスレッドを立てると思います。今回は、上記のような状況を一般に「非同期の処理」と呼び、その処理をSeries60 上で実現する方法を見てみたいと思います。

マルチスレッドは使用しない?

 Series60では、マルチスレッドを用いたプログラムが可能になっております。しかし、ここで紹介する方法は、マルチスレッドの使用方法を説明するものではありません。スレッドはあくまでもメインのスレッド1本です。非同期の処理を実現するのにスレッドを用いないのは「Series60ではマルチスレッドを用いたプログラムが推奨されない」点に理由があります。マルチスレッドを用いることが推奨されない理由としては

* コンテキストスイッチの切り替えなど、マルチスレッドはリソースを多く消費する
* デットロックの回避等、マルチスレッド特有の問題に対処しなくてはならない

などが挙げられます。

 推奨しないからには、スレッドに変わる非同期の処理を行う機構が必要です。これが「アクティブオブジェクト」と「アクティブスケジューラ」になります。

アクティブオブジェクトとアクティブスケジューラ

 アクティブオブジェクトとアクティブスケジューラは、アプリケーションフレームワークの中で使用されるクラスです。各々の詳細を挙げると、

アクティブスケジューラ
各イベントに応じてアクティブオブジェクトのスケジューリングを行う。アクティブスケジューラ は1つのスレッドにつき1つのみ存在できる。
アクティブオブジェクト
アクティブスケジューラから呼び出され、非同期の処理を行う実体であり、コールバックの関数としてRunL()関数を持つ。

となります。

 つまり「アクティブスケジューラは簡単なスケジューリングを提供する。スケジューリングの対象がアクティブオブジェクトになる」と見ることができます。さらに言えば、「Series60のアプリケーションは、SymbianOSが提供するプロセスごとのスケジューリングと同時に、アプリケーションフレームワーク内部でも各スレッドごとにアクティブスケジューラが行うスケジューリングがある」ということができます。
posted by シンビアン at 21:03| Comment(0) | TrackBack(0) | Symbian OS C++ 実践開発技法 | このブログの読者になる | 更新情報をチェックする

コンストラクション(Cクラス)

次に、Cクラスのオブジェクトのコンストラクションに話を移しましょう。オブジェクトのコンストラクションに絡めて、クリーンアップスタックについて見ていきます。

 前回の記事でも多少触れさせていただきましたが、クラスのインスタンスを作成する時は通常、「2フェーズコンストラクション」を行います。2フェーズコンストラクションは、具体的には

* new(ELeave)演算子を用いてクラスのインスタンスが必要とするメモリを確保する。この新たにオーバーロードされている演算子はリーブを発生させる可能性がある。

* 作成したインスタンスが他のオブジェクトを所有する場合(つまりhas-aポインタを持つ場合)は、概ねConstructLという名前の関数でポインタの参照先インスタンスの生成とポインタの値の設定を行う。

 という流れになります。若干厄介に見えますが、これはメモリリーク対策になります。またまた繰り返しになりますが「メモリの確保が最もリソース不足によるエラーを起こしやすい」に注意してください。

 それでは、次の図を見てください。


 携帯環境ですから、非常にリソースが限られています。ですから、小さなオブジェクトの確保でも失敗をする可能性があるわけです。もし、上記のようなコードで、ptr2の初期化に失敗する可能性を考えるのであれば(これは携帯環境では十分に考えられる)、各々のポインタに対して、nullチェックを行う必要があります。これでは、プログラムを書くのが手間になってしまいます。また、リーブの機構があるのですからこれを使わない手はありません。

 そこで、例外処理をすっきりさせるため、先ほど出てきたリーブの仕組みを使うことを考えます。つまり、new(ELeave)を用いてメモリの確保を行います。このオペレータは先述の通り、メモリ確保に失敗するとリーブを発生させます。

 この場合、例外処理はすっきりしますが、新たな問題を引き起こします。それでは、次の図を見てください。


 つまり、リーブが発生した時点でTRAPマクロやTRAPDマクロで処理される箇所までスタックを遡るわけです。2回目のnew(ELeave)でリーブが発生した場合、ptr1が一時的な作業用のauto変数(コンストラクタの中であったとしても十分にあり得る)であれば、そのポインタが永遠に失われ、結果としてメモリリークの原因になります。

 そこで、次のような対策を講じます。

1. CBaseクラスを継承したクラス(ClassA)の使用するメモリのみを確保する(コンストラクタでは何も行わない)。メモリの確保に失敗すれば、リーブを発生させる。

2. ClassAによって所有される他のクラス(has-aポインタでの参照先)は、他のコンストラクション用の関数(一般にConstructL)で初期化する。ここでもリーブが発生する可能性がある。

3. 1または2でリーブが発生した場合、構築途中のオブジェクトへのポインタが失われないようにグローバルな領域(スタック)にポインタの値をコピーしておく(このスタックをクリーンアップスタックと呼ぶ)。

 それでは、2フェーズコンストラクションのサンプルを見てみましょう(Construction.zip)。このサンプルでは、自前でCBaseクラスから派生をしたCMyObjクラスを作成し、このクラスが2フェーズコンストラクションを行います。ただし、意図的にConstructLの関数内でリーブを発生させてる点に注意してください。

 サンプルをビルドしたのち、下記のアイコンのアプリケーションを実行します。


 すると、次のような画面が現れます。


 その後、メニューから「テスト」を選択します。ここでは意図的にリーブを発生させていますので、次のような画面が現れます。


 最後に次のような画面になります。


 ここで、今回のサンプルコードの重要な部分を確認しておきましょう。

CMyObj* CMyObj::NewL(CConstructionContainer* aContainer )
{
CMyObj* ptr = new(ELeave)CMyObj; // インスタンスが使用するメモリの確保
CleanupStack::PushL( ptr );
// (1)以下の処理でリーブが発生したときに備えクリーンアップスタックにpush
ptr->ConstructL(aContainer); // インスタンスのメンバの初期化
CleanupStack::Pop();
// リーブの可能性がなくなったので、クリーンアップスタックからpop
return ptr ;
}

void CMyObj::ConstructL(CConstructionContainer* aContainer)
{
this->iContainer = aContainer ;
TInt reason = KErrNoMemory; // 勝手にメモリがないことにする
User::Leave(reason); // リーブを発生(2)
aContainer->UpdateMessage(_L("初期化完了") ); // この行は実行されない
}

CMyObj::~CMyObj()
{
this->iContainer->UpdateMessage(_L("CMyObj 削除完了"));
}

 ここでは(1)にて構築途中のオブジェクトをクリーンアップスタックにプッシュし、第二フェーズのコンストラクションの(2)において意図的にリーブを発生させています。ここでクリーンアップスタックの重要な動作として、

リーブが発生したとき、クリーンアップスタックに詰まれた
全てのオブジェクトのデストラクタが呼び出される

という点に注意してください。つまり、2フェーズ目で構築に失敗し、リーブが発生しても、クリーンアップスタックにメモリ確保時に取得したポインタを積んでおけば、自動的にデストラクタが呼び出されるということです。そして、リーブ時にデストラクタを呼び出すためにCBaseクラスに仮想デストラクタが定義されているわけです。今回のサンプルコードの画面遷移でも、デストラクタが呼び出されていることが確認できると思います。

 ただし注意点として、リーブ発生時にはクリーンアップスタックに詰まれた全てのオブジェクトがデストラクトされます。つまり、

リーブが発生する可能性が無くなった時点でクリーンアップスタックから取り除く

必要があります。将来、全く別の理由でリーブが発生した場合、問題のないオブジェクトまでデストラクションする可能性があるためです。

次回の予定

 次回は、非同期の処理を見て生きたいと思います。Series60でもスレッドの利用は可能ですが、リソースの消費用の大きさから異なるフレームワークを使用します。この点について確認していく予定です。
posted by シンビアン at 20:58| Comment(0) | TrackBack(0) | Series60プログラミングテクニック | このブログの読者になる | 更新情報をチェックする

ドメインの再利用

ドメインとは、ある立場から見た場合のシステムの見え方のことでした。ドメインは独立した単位ですので再利用ができます。

ドメインの再利用は何も難しい事ではなく、言ってみれば当たり前の話です。例を挙げると、あるシステムをWebベースで作成した後で、画面や操作はまったく同じにしてJavaで作り直して欲しいと言われた時のことです。こんな時にはデータベースや画面の設計をそのまま持ってきてコーディングだけやり直せばいい、というだけの話です。

しかし、言うだけなら簡単ですが、実際にやってみるとそんなに簡単ではないことがわかるでしょう。ドメインを意識した設計でないと、仕様書のそこかしこにWebの言葉が出てきてしまっているでしょう。「FORMタグを使う」とか「URLに○○のパラメータを含めて……」とか。このようにWebベースであることを前提とした仕様書になっていると再利用することが難しくなります。

こうした場合、本来ならアプリケーション本来の動作を表現するアプリケーションドメインとそれをWeb上で閲覧/操作するためのユーザインターフェースドメインに分けるべきなのです。アプリケーションドメインではURLとかタグとか CGIというような用語は一切使ってはいけません。なぜならそれは本来のアプリケーション(経理とか在庫管理とか)とは一切関係がないからです。そしてしっかりドメイン分けされていれば、関係ない部分が変わっても問題はない、というわけです。
posted by シンビアン at 20:45| Comment(0) | TrackBack(0) | Series60プログラミングテクニック | このブログの読者になる | 更新情報をチェックする

メタモデル

「メタモデル」とは、モデルをモデル化したものです。「CDには複数の曲が入っていて、曲にはタイトルと作曲者がある」というのがCDというものをモデル化したものだとすると、「オブジェクトには属性があり、それぞれのオブジェクトは関係しあっている」というのがモデルをモデル化したものです。

メタモデルというのは案外いろいろな場所で顔を出します。データベースのテーブル設計がモデル化であるとすると、テーブルの定義方法を設計するのがメタモデルです。プログラムそのものに対して「プログラムには関数と変数があって……」というのがメタモデルです。

メタモデルというのは一種のパターンです。メタモデルはただシステムを開発するだけならさほど意識しなくてもすむのですが、開発スタイルや開発ツールを作成しようとすると目を向けなくてはならなくなります。
posted by シンビアン at 20:43| Comment(0) | TrackBack(0) | Series60プログラミングテクニック | このブログの読者になる | 更新情報をチェックする

コンベンション

「コンベンション」とは、システム設計に関する規則のことです。「コードコンベンション」という単語が一番馴染みがあるでしょう。コードコンベンションとはコードを書く時の規則のことです。(例えば)Java言語の構文規則のことではありません。それにのっとった上で、例えば関数のコメントはどこに書くかとか、関数名の決め方などを決めたのがコードコンベンションです。当然のことながら、こうした取り決めはいったん決めてしまえばいろんなシステムに適用できます。

コンベンションはコードだけに限りません。仕様書や設計書の書き方、そしてそもそもどんな仕様書を用意するのかなど、システム開発のやり方を規定する様々な規則がコンベンションです。システム開発のモデル化とも言えるでしょう。「システム開発=モデル化」という考え方からすればこれはメタモデルです。

開発するシステムの種類によって適切なコンベンションは変わってきます。小規模システムと大規模システムでは違いますし、データベース系と組み込みリアルタイム系でもまた違うでしょう。その場にあったコンベンションを開発し、それは同じようなシステムで使い回すというのがよいやり方でしょう。
posted by シンビアン at 20:42| Comment(0) | TrackBack(0) | Series60プログラミングテクニック | このブログの読者になる | 更新情報をチェックする

例外処理(リーブ)

つぎに例外処理を見ていきましょう。また、例外処理に関連して2フェーズコンストラクションについて見ていきたいと思います。このセクションをお読みになるにあたり、しつこいですが「携帯環境のリソースは非常に限られたものを想定している」というのを頭の片隅において置いてください。

 Series60では通常のC++によるtry-catch構文とは異なる例外処理の機構を利用します。これは通常のtry-catchによる例外処理が結果としてコードサイズの肥大化を招くためです。ですから、通常のtry-catchによる例外処理は採用されていません。

 そこで、try-catchの代用として出てくるのが「リーブ(Leave)」という考え方(機構、仕組みといっても良いかもしれません)です。リーブは、従来の例外と概念的にはほぼ同一です。以下にリーブについて概略をまとめます。

* C++の例外の代わりに使用される
* リソースエラー等が発生すると、コードはリーブする
* トラップハーネス(TRAPマクロやTRAPDマクロ)でリーブが処理されるまで関数のスタックをさかのぼる

リーブによる例外処理の例

 それでは、次のサンプルを見てみましょう(LeaveSample.zip)。このサンプルでは、メニューから「リーブのテスト」を選ぶと、意図的にメモリ不足のリーブが発生するようになってます。

// コード例(A)
void CLeaveSampleAppUi::HandleCommandL(TInt aCommand)
{
switch ( aCommand )
{
/// 中略….
case ELeaveSampleCmdAppTest:
{
// 次の関数の呼び出しはリーブを発生させる
this->iAppContainer->TestLeaveL();
break;
}
// 後は省略….

このTestLeaveL()関数の実装は次の通りです。

// コード例(B)
void CLeaveSampleContainer::TestLeaveL()
{
TInt reason = KErrNoMemory; // 勝手にメモリがないことにする
User::Leave(reason); // (1)例外を発生
this->iToDoLabel->SetTextL(_L("問題なく終了しました"));
// 上の一行は実行されない
}

 このサンプルでは、(1)においてメモリ不足のリーブを意図的に発生させています(User::Leave関数でリーブを発生)。このアプリケーションをそのまま実行すると次のような画面がでてきます。

それでは、このアプリケーションに対して例外処理のコードを加えたいと思います。上記コード例(A)を次のように書き換えてみてください。ボールドになっている箇所が書き換えた箇所です。

// コード例(C)
void CLeaveSampleAppUi::HandleCommandL(TInt aCommand)
{
switch ( aCommand )
{
/// 中略….
case ELeaveSampleCmdAppTest:
{
// TRAPDマクロで例外を捕捉
// error に例外コードが入ってくる
TRAPD( error,
this->iAppContainer->TestLeaveL(); );
if( error != KErrNone ){ // 例外が発生しているかを検査
this->iAppContainer->UpdateMessage();
}
break;
}
// 後は省略…

TRAPDマクロの第一引数にあるerrorに例外が発生した理由が格納されます。これをKErrorNone(つまり例外が発生しなかったことを表す)と比較しているわけです。

 もう少しリーブについて見ていきましょう。例外処理自体は、このような形で行うことができます。次に残る問題は「作成した関数、またはAPIで提供されている関数がリーブを発生させるか否か、コンパイラレベルでは判断できない」点にあります。リーブの機構はC++の言語ではなく、Series60で採用されているものです。つまり、例外を適切に処理するのであれば、先のコード例(A)を本来はコード例(C)のようにしなくてはいけません。しかし、コード例(A)のままでも問題なくコンパイルが可能ですし、警告も表示しません。

 そこで出てくるのが関数のコードコンベンションです。今回はコード例(B)で定義した関数は最後に「L」の付く関数になっていますが、一般にリーブを発生させる可能性のある関数はその名前の最後に「L」を付ける習慣になっています。このような「L」の付く関数に関しては(製品としてプログラムを出す場合は)必ずTRAPマクロや今回の様にTRAPDで処理をする必要があります。

補足:今回はリーブによる例外処理を見ていきましたが、一部のAPIには戻り値によって正常/異常終了を表すもあります。
posted by シンビアン at 20:40| Comment(0) | TrackBack(0) | Series60プログラミングテクニック | このブログの読者になる | 更新情報をチェックする

広告


この広告は60日以上更新がないブログに表示がされております。

以下のいずれかの方法で非表示にすることが可能です。

・記事の投稿、編集をおこなう
・マイブログの【設定】 > 【広告設定】 より、「60日間更新が無い場合」 の 「広告を表示しない」にチェックを入れて保存する。


×

この広告は1年以上新しい記事の投稿がないブログに表示されております。