日曜日にBCB!



Do It Yourselfでひとつプログラムでも作ってみませんか。


第8話「マルチスレッド化」

エクスプローラで大きいファイルをコピーしているとドキュメントが飛んでいるアニメーションが表示されます。またプログレス(進捗)バーであとどのくらいでコピーが終わるかが、大体わかります。当然中断もできます。このような処理を寿分割で行うにはマルチスレッドが簡単かつ最適です。
プログラムは最低ひとつのスレッドという単位で動いています。これをシングルスレッドと呼びます。これに対してマルチスレッドとは、一つのプログラムで複数の処理を同時進行で行う手法です。スレッドは別のスレッドを生成することができます。スレッドの生成や管理方法を述べると長くなりますので、BCBのマニュアルをよく読んでおいてください。BCBではスレッドもクラス化されているので、取り扱いは比較的簡単です。

マルチスレッドについて補足します。マルチスレッドとは一つのプログラムを二つ以上のスレッドという単位に分割し、一つのプログラムで複数の処理を同時に動かす手法です。この手法を活用するとファイル分割実行などの重たい処理と、敏速にユーザーからの要求をうける処理を平行して行うことが出来ます。プログラムも非常にすっきりします。ただ、シングルスレッドにはない独特の考え方(スレッド生成・起動・同期・破棄)を知っておく必要はあります。ZipFinderの広域検索はこの手法を使っています。
ここで注意が必要です。スレッド同士は同じプログラムメモリ空間で動きますので、オブジェクトを含む変数を共有できます。ただ、どちらのスレッドかが変数を更新すると、他方に通知する必要が生じます。また、折角マルチスレッドで動いていても、お互いが同期を取り合うと実行効率が落ちてしまいます。あまりスレッド間で変数を変更し合うのは考え物です。当然ですが、関数の独立性を高めて、メンテナンスし易いプログラムをつくれるように設計しましょう。寿分割は大丈夫です。


マルチスレッド

スレッドの役割分担

ここまでの寿分割は当然シングルスレッドで動いています。「分割実行」メニューが選択されるとGoDivideMNUClick関数が実行されます。ファイルを分割しますが、ユーザーからの中断は受け付けません。これでは大きなファイルの分割中に中断できません。シングルスレッドでユーザー中断を受けることも可能です。これまでのように分割ファイルを一回でファイル出力するのではなく、いくつかに分割して出力し、その合間にユーザー中断を監視する方法もあります。しかし、ファイル出力中にはユーザー中断に反応できません。特に書きこみ速度が遅いフロッピーディスクへの分割で、この状況は顕著になります。
マルチスレッド化はユーザー中断をスムーズに行えるようにするのが目的です。進捗状況表示だけではマルチスレッド化する必要はありません。マルチスレッド化と同時に分割の進捗状況表示を実現します。寿分割では、 の2つのスレッド構成とします。スレッドの生成・起動・同期・破棄はVCLのTThreadクラスの仕様に基づいて行います。

もう少し2つのスレッドの役割分担を明確にします。分割スレッドはできるだけGUIスレッドからの独立性を高めます。一方のスレッドで仕様変更が生じた場合、できるだけもう一方に影響がでないようにするのと、分割スレッドは他のソフトにも流用できるようにするためです。さらにエラー処理分担も明確にする必要があります。
マルチスレッドは抽象的な話のようですが、以下のようなストーリーを思い浮かべてもらえれば良くわかると思います。
GUIスレッド 「分割スレッド君、ファイル分割をしてくれたまえ。必要な情報はこれだ。もし分割先に容量がなくなったら声をかけてくれ。」
分割スレッド 「はいわかりました」...v( .. )ブンカツブンカツ
GUIスレッド (p_q) ← ユーザー対応中
分割スレッド 「分割先容量がなくなりました。」
GUIスレッド 「そうかそうか。分割先メディアを交換したぞ。」
分割スレッド v( .. )ブンカツブンカツ
GUIスレッド (p_q) ← ユーザー対応中
分割スレッド 「分割は終わりました。これが報告書です。」
GUIスレッド 「ふむふむ、問題なく終わったな。ご苦労様。」
分割スレッド 「では、失礼します。」 m(_ _)m
分割スレッドは分割のみに専念させ、ユーザー対応は行わせません。第2話の「プログラムを複雑にする犯人」や、第4話の「データの流れ道」でも述べたように、要は「データの責任がはっきりすると、プログラムの動きからデータの流れが明らかになる」と言う事です。

GUIスレッドの働き

GUIスレッドは、 どのファイルをどのくらい分割しているのかは、分割先ファイル一覧とプログレスバーで表示させましょう。

分割スレッドの働き

分割スレッドはGoDivideMNUClick関数で実行したファイル分割を実際に行います。GUIスレッド(メインスレッド)から生成され、分割に必要な情報を受け取り、GoDivideMNUClick関数と同じことを行います。加えて、 を行います。


分割情報

ここで分割に必要な情報を明確にしておく必要があります。二つのスレッドの実行パフォーマンスを落とさないためにも、分割前にGUIスレッドから分割スレッドに渡す情報も検討します。必要な情報としては、
情報 内容
呼出しフォーム 分割スレッドを呼び出したフォーム。分割進捗状況を通知する先。
分割進捗状況メッセージ 分割進捗状況を通知するためのユーザー定義のWindowsメッセージID。
容量不足メッセージ 分割先容量の不足を通知するためのユーザー定義のWindowsメッセージID。
分割元ファイル 分割元のファイル名、パスを含む完全な名前。ファイル選択ダイアログが保持している。
分割先パス 分割先のパス。分割元のファイル名、パスを含む完全なパス名前で、分割先の完全ファイル名作成が容易なように末尾は'\'で終わる形式。ステータスバーが保持している情報を元に作成。
分割ファイルリスト 分割先ファイルおよび結合バッチファイルの名前と分割オブジェクトのリスト。リストビューのアイテムリストがファイル名のコピー(GUIスレッドでは画面再描画時にリストビューを参照するため、スレッド競合をさけるために分割スレッドはコピーを参照します)。
分割サイズ 分割バッファ確保サイズ。GetCutSize関数で得られる値。
結合バッチコマンド 結合バッチコマンド文字列。MakeConnectBatch関数で得られる文字列。
が挙げられます。これらをGUIスレッドから受け取り、分割を実行します。ここで使っているWindowsメッセージの定義方法や発行方法はさして難しいものではありません。いろいろと応用の効く技法ですから是非習得してください。分割スレッドで固定値にしない理由は、プログラムを機能追加していく時にメッセージIDを重複させないためです。
分割進捗状況と容量不足を関数呼出しではなくメッセージ通知にする理由は、分割スレッドGUIスレッドの内部構造を意識させないためです。言いかえると分割スレッドにとってはGUIスレッドは一時的な雇い主であって、GUIスレッドとは独立しています。こうすることによって互いの機能を独立にかつ頑強にすることが可能となります。キーワードは再利用です。再利用を念頭に設計すると言うことは、後々楽をするためです。そこのプログラム開発のSEさん、とりあえず作ってませんか? 「時間がないんだ」は言訳になりませんよ。私だってプログラミングを生業としていた頃は時間がなかったですが、絶対再利用だけは考えてました。「楽したい!! 楽するために苦労するんだ。絶対楽になるんだ」と信じて。


分割進捗状況の表示

どのファイルを分割しているのかは分割ファイル一覧で選択表示しましょう。1つのファイルをどのくらい分割しているかはプログレスバーで表示しましょう。

分割ファイル表示

この時、2つの些細な問題があります。

・現在の分割先ファイル一覧は分割先ファイル名は選択されるが、サイズは選択されない。

これは分割先ファイル一覧のリストビュー ListVEWのプロパティを変更すれば解決します。

・分割先ファイルが画面に収まらない場合、分割中ファイル選択が見えなくなってしまう。

リストビューコントロールにはスクロールメソッドがあります。また、表示している先頭行(リストアイテム)のインデックス番号や、表示している行数がプロパティーから取得できます。これらを組合せて分割中のファイル選択が必ず見えるようにスクロールする関数を作りましょう。

ScrollListToViewItem関数

//---------------------------------------------------------------------------
void __fastcall TMainWND::ScrollListToViewItem(TListView *VEW)
{
// VEW (TListView*) スクロールさせるリストビュー

    TListItem *Top = VEW->TopItem;    // 先頭リストアイテム取得
    if (Top==NULL) return;            // リストが空なら終了

    if (VEW->ViewStyle==vsReport){    // スクロールは詳細表示でのみ実行
        int ViewCount    = VEW->Selected->Index - Top->Index;    // 選択行の表示先頭からの行数取得
        int VisibleCount = VEW->VisibleRowCount;                 // 表示可能リストアイテム行数取得
        if (ViewCount>=VisibleCount){                            // 選択行が表示されていなければ
            TRect ViewRect = VEW->Selected->DisplayRect(drSelectBounds);
                                                             // 選択リストアイテムの表示矩形領域取得
            VEW->Scroll(0, (ViewRect.Bottom-ViewRect.Top)*(ViewCount-VisibleCount+1));
                                                             // 選択行が表示されるまで縦方向スクロール
        }
    }
}
//---------------------------------------------------------------------------
分割状況は、 を分割スレッドからGUIスレッドに通知します。

プログレスバー設計

分割プログレスバー追加 寿分割の画面にプログレスバーを付加します。エクスプローラのコピー・移動中のようなダイアログを作っても良いのですが、画面をコンパクトに仕上げるためにステータスバーと重ねてプログレスバーを表示したいと思います。

といっても重ねて画面設計できませんので、ちょっと小手先を使います。リストビューとステータスバーの間にプログレスバーを入れて、 とします。

これまでは分割先ファイルには分割ファイルサイズを一気にfwrite関数を使って書きこんでいました。これでは1分割ファイルの進捗状況を表示することはできません。そこで、1分割ファイルをさらに複数のブロックに分割し、ブロック毎に書きこみと進捗状況の表示を交互に行います。
プログレスバーは最小値を0、最大値を100とします。分割ファイルサイズが全て同じであれば最大値を分割ファイルサイズとしても良いのですが、最後のファイルは他よりも小さく、また結合バッチファイルサイズも他よりも小さいので、分割進捗状況はパーセントで表示させます。


スレッド間同期

またマルチスレッドに話が戻ります。複数のスレッドが一つの画面を共有する際、必ず「同期」を守らなければなりません。GUIの様にユーザー等からのイベントに対するコールバックを記述するのに対して、スレッドプログラムを記述するのは気を使います。目には見えにくいスレッドの作法をしっかりと守らないといけないのと、不具合の原因が追求しにくくなるためです。しかしデータの流れは基本的にシングルスレッドでもマルチスレッドでも同じですし、マルチスレッド化する時も、データの流れを最重要視することに変わりはありません。プログラムを強くする点、進捗状況の表示も含め、GUIスレッドと分割スレッドの同期を整理するためにもタイミングチャートを書きましょう。

タイミングチャート


この流れはマルチスレッドでは一般的なものです。GUIスレッドに分割実行イベントが発生すると、分割スレッドを生成、分割スレッド情報設定、分割スレッド実行して、処理を終えます。つまり、分割実行は分割スレッドに任せて、ユーザーからのイベントを待ちます。
分割実行中のGUIスレッドには以下のイベントが発生します(発生しないものもあります)。 ここでひとつ注意は、スレッドの衝突をさけることです。分割進捗状況と容量不足を分割スレッドからGUIスレッドに通知する時にスレッドの衝突が発生するかは実は言うと私も定かではありません。しかし安全のため同期をとります。BCBにはスレッド同期専用の関数Synchronize()が準備されていますので、この関数を使います。


分割スレッドクラス

BCBではいろいろなものをクラス化して取り扱いを簡単にしています。寿分割では分割スレッドをクラス化し、その振る舞いを明確にすることにより分割実行に対する処理の独立性を高めています。では早速分割スレッドクラスの定義からはじめましょう。BCBの「ファイル」メニューから「新規作成」を選択し、「スレッドオブジェクト」を新規作成します。スレッドオブジェクトのクラス名にはTDivideThreadを指定します。スレッドオブジェクトの雛型としてヘッダファイルにクラス定義とソースファイルに手続きが作成されますので、ソースファイルには'dividtrd'と名づけて保存しておきます。ではクラス定義から行います。

TDivideThreadクラス

//---------------------------------------------------------------------------
class TDivideThread : public TThread
{
private:
    TForm        *OwnerForm;        // 呼出しフォーム
    unsigned int ReportMessage;     // 分割進捗状況報告メッセージ
    unsigned int NoSpaceMessage;    // 分割容量不足メッセージ
    AnsiString   SourceFile;        // 分割元ファイル
    AnsiString   DstPath;           // 分割先パス
    TStringList  *DstFiles;         // 分割先ファイルリスト
    int          CutSize;           // 分割サイズ
    AnsiString   ConnectBatch;      // 結合バッチコマンド
    int          DivideIndex;       // 分割ファイルインデックス番号
    int          DivideStatus;      // 分割実行ステータス
protected:
    void __fastcall Execute();          // 分割実行
    void __fastcall ReportDividing();   // 分割進捗状況通知
    void __fastcall NoDivideSpace();    // 分割先容量不足通知
public:
    __fastcall TDivideThread(TForm *_OwnerForm,                   // コンストラクタ(初期処理)
                             unsigned int _Messages[],
                             AnsiString _SourceFile,
                             AnsiString _DstPath,
                             TStringList *_DstFiles,
                             AnsiString _ConnectBatch,
                             int _CutSize);
    int __fastcall GetDividedStatus(){ return(DivideStatus); }    // 分割終了ステータス取得
};
//---------------------------------------------------------------------------
プライベートメンバの呼び出しフォームから分割可能メディアまでは分割情報と同じです。分割ファイルインデックス番号と実行ステータスは分割進捗状況をGUIスレッドに通知するために、分割スレッドが内部的に使用します。クラス内のグローバル変数としているのは、GUIスレッドに分割ステータスを通知する際にSynchronize関数を経由するため引数を使用できない(VCLのスレッドクラスの仕様)ためです。前にも述べたようにグローバル変数はデータの流れを不明瞭にする悪玉ですが、クラスの仕様上仕方ありません。

分割実行ステータスは0〜100で分割進捗状況を表します。さらに0以下の値で分割の完了ステータスを示します。

定数定義 定数値 意味
dsSuccess 0 正常終了
dsNoSpace -1 分割先容量不足
dsSrcError -2 分割元ファイルオープンエラー
dsDstError -3 分割先ファイル作成エラー
dsReadError -4 分割元ファイル読みこみエラー
dsWriteError -5 分割先ファイル書きこみエラー
dsUserBreak -6 ユーザー中断
この定義は分割スレッドオブジェクトが提供しますので、クラス定義をしたヘッダファイルに定義します。また、第5話で定義した分割サイズクラスも同様に分割スレッドのクラス定義ヘッダに移動します。
各種エラーやユーザー中断を保持する意味はないように思われるかもしれませんが、使われ方は分割スレッドを呼び出した方の自由です。逆に言えば「自由」と言うことは、出来るだけ詳細なステータスを保持する必要があります。GetDividedStatus関数で分割実行ステータスを取出せるようにします。パブリックメソッドのGetDividedStatus関数は、GUIスレッドが分割の完了時に分割結果を知るためのものです。単に分割実行ステータスを返すだけですのでインライン定義しています。当然ですが分割実行ステータスは他のオブジェクトから変更されないように隠蔽し、パブリックなメソッドからのみ参照させます。オブジェクト指向の常識ですね。

コンストラクタでは分割スレッドオブジェクト生成時に分割情報を設定します。

TDivideThreadコンストラクタ

//---------------------------------------------------------------------------
__fastcall TDivideThread::TDivideThread(TForm *_OwnerForm,          // 呼出しフォーム
                                        unsigned int _Messages[],   // 通知メッセージID
                                        AnsiString _SourceFile,     // 分割元ファイル
                                        AnsiString _DstPath,        // 分割先パス
                                          TStringList *_DstFiles,   // 分割先ファイルリスト
                                        AnsiString _ConnectBatch,   // 結合バッチコマンド
                                        int _CutSize)               // 分割サイズ
    : TThread(true)
{
    FreeOnTerminate = true;             // スレッド終了時にスレッドオブジェクトを自動的に破棄
    OwnerForm       = _OwnerForm;       // 呼出しフォーム設定
    ReportMessage   = _Messages[0];     // 分割進捗状況通知メッセージ設定
    NoSpaceMessage  = _Messages[1];     // 分割先容量不足メッセージ設定
    SourceFile      = _SourceFile;      // 分割元ファイル設定
    DstPath         = _DstPath;         // 分割先パス設定
    DstFiles        = _DstFiles;        // 分割先ファイルリスト(名前+分割オブジェクト)のコピー設定
    ConnectBatch    = _ConnectBatch;    // 結合バッチコマンド設定
    CutSize         = _CutSize;         // 分割サイズコピー
}
//---------------------------------------------------------------------------
ちなみにこの分割スレッドにはデストラクタは必要ありません。

TDivideThreadクラス Executeメソッド

さて、分割実行です。分割実行はTThreadクラスの仮想メソッドのExecuteをオーバーライドし、ここに実際に分割実行する手続きを記述します。基本的には第7話のGoDivideMNUClick関数をマルチスレッドに対応させます。基本的な流れは同じですが、以下を追加・変更します。 ここでまた些細な問題が発生します。私自身もプログラムをテストしてからわかったのですが、GoDivideMNUClick関数の分割ファイルに対する書きこみ方法では分割進捗状況が正しく表示されません。Windowsは書きこみバッファを持っていて、書きこみバッファが一杯になったらバッファの内容をディスクに書きこみます(これを遅延書きこみと言います)。分割進捗状況は書きこみバッファを含めて分割の割合を表示します。つまり実際にディスクに書き込まれていなくても分割進捗状況は進みます。これでは正しく進捗状況を表示できないので、書きこみ毎にバッファをクリアする必要があります。これには分割ファイルの書きこみの最初は新規作成モードで行い、以降は一旦分割ファイルを閉じて進捗状況の通知し、追加モードで書きこみを行います。プログラムを作るには予期せぬ問題もクリアしていかなければいけないのは、プログラムを作ったことがない人にはなかなか理解してもらえませんよね。
//---------------------------------------------------------------------------
void __fastcall TDivideThread::Execute()
{
// 分割元ファイル読込みオープン
    FILE *SrcFP = fopen(SourceFile.c_str(), "rb");
    if (SrcFP==NULL){        // 分割元ファイルオープンエラー?
        DivideStatus = dsSrcError;      // 分割元ファイルエラー
        return;                         // 分割中断
    }

// 分割バッファ準備
    char *buffer = new char [CutSize];
    if (Terminated){        // ユーザー中断?
        DivideStatus = dsUserBreak;     // ユーザー中断
        fclose(SrcFP);                  // 分割先ファイルクローズ
        return;                         // 分割中断
    }

// 分割数繰り返し(結合バッチファイルは除く)
    for (int i=0;i<DstFiles->Count-1;i++){
        DivideIndex = i;               // 分割中ファイルインデックス
        int ProgressSize = 0;          // 書き込み進捗サイズクリア

    // 分割先ファイル情報取得
        cDivideFile *File = (cDivideFile*)DstFiles->Objects[i];      // 分割ファイルオブジェクト取得
        int ACutSize = File->GetSize();                              // 分割サイズ取得

        int DriveNum = (int)DstPath[1] - 'A' + 1;           // ドライブ番号取得
        while(GetDiskFree(DriveNum)<(double)ACutSize){      // 空き容量確保ループ
            Synchronize(NoDivideSpace);                     // 分割先容量不足通知
            if (DivideStatus<dsSuccess){     // 分割中断?
                DivideStatus = dsUserBreak;     // ユーザー中断
                delete buffer;                  // 分割バッファ開放
                fclose(SrcFP);                  // 分割先ファイルクローズ
                return;                         // 分割中断
            }
        }

    // 分割先ファイル作成
        int BlockSize = ACutSize/16;                                    // 書き込みブロックサイズ取得
        AnsiString DstFilePath = DstPath + DstFiles->Strings[i];        // 分割先ファイル名作成
        FILE *DstFP = fopen(DstFilePath.c_str(), "wb");       // 分割先ファイル作成オープン
        if (DstFP==NULL){               // 分割先ファイル作成エラー?
            DivideStatus = dsDstError;  // 分割先ファイル作成エラー
            delete buffer;              // 分割バッファ開放
            fclose(SrcFP);              // 分割先ファイルクローズ
            return;                     // 分割中断
        }

    // ブロック書きこみループ
        while(ProgressSize<ACutSize){        // 分割進捗サイズが分割サイズに達するまで繰り返し
            if ((ProgressSize+BlockSize)>ACutSize) BlockSize = ACutSize - ProgressSize;    // 1ブロックサイズ計算
            if (fread(buffer, BlockSize, 1, SrcFP)!=1){    // 1ブロック読みこみ
                DivideStatus = dsReadError;     // 分割元ファイル読みこみエラー?
                fclose(DstFP);                  // 分割先ファイルクローズ
                delete buffer;                  // 分割バッファ開放
                fclose(SrcFP);                  // 分割元ファイルクローズ
                return;                         // 分割中断
            }
            ProgressSize += BlockSize;          // 分割進捗サイズ更新
            if (fwrite(buffer, BlockSize, 1, DstFP)!=1){    // 1ブロック書きこみ
                DivideStatus = dsWriteError;    // 分割先ファイル書きこみエラー?
                fclose(DstFP);                  // 分割先ファイルクローズ
                delete buffer;                  // 分割バッファ開放
                fclose(SrcFP);                  // 分割元ファイルクローズ
                return;                         // 分割中断
            }
            DivideStatus = ProgressSize*100/ACutSize;   // 分割進捗状況計算
            Synchronize(ReportDividing);        // 分割進捗状況報告
            if (Terminated){                    // ユーザー中断
                fclose(DstFP);                  // 分割先ファイルクローズ
                DivideStatus = dsUserBreak;     // ユーザー中断
                delete buffer;                  // 分割バッファ開放
                fclose(SrcFP);                  // 分割先ファイルクローズ
                return;                         // 分割中断
            }

        // 書き込みバッファフラッシュ
            fclose(DstFP);                                // 分割先ファイルクローズ
            DstFP = fopen(DstFilePath.c_str(), "ab");     // 分割先ファイル追加モード際オープン
        }
        fclose(DstFP);                  // 分割先ファイルクローズ
    }

// 分割元ファイルクローズ
    fclose(SrcFP);

// 分割バッファ開放
    delete buffer;

// 結合バッチコマンド出力
    AnsiString DstFilePath = DstPath + DstFiles->Strings[DstFiles->Count-1];  // 結合バッチファイル名作成
    FILE *DstFP = fopen(DstFilePath.c_str(), "wb");       // 結合バッチファイル作成オープン
    if (DstFP==NULL){                   // 結合バッチ作成エラー?
        DivideStatus = dsDstError;      // 分割先ファイル作成エラー
        return;                         // 分割中断
    }

    DivideIndex  = DstFiles->Count-1;   // 分割中ファイルインデックス更新
    DivideStatus = 0;                   // 分割進捗状況更新(バッチファイルは0と100のみ)
    Synchronize(ReportDividing);        // 分割進捗状況通知

    if (fwrite(ConnectBatch.c_str(), ConnectBatch.Length(), 1, DstFP)!=1){    // 結合バッチコマンド書込み
        fclose(DstFP);                  // 書きこみエラー?
        DivideStatus = dsWriteError;    // 分割先書きこみエラー
        return;                         // 分割中断
    }
    fclose(DstFP);                      // 結合バッチファイルクローズ

    DivideStatus = 100;                 // 分割進捗状況更新(バッチファイルは0と100のみ)
    Synchronize(ReportDividing);        // 分割進捗状況通知
    DivideStatus = dsSuccess;           // 分割成功
}
//---------------------------------------------------------------------------
この関数の中ではGetDiskFree関数を使用しています。この関数はmain.cppに定義してあり、これでは分割スレッドの独立性が損なわれます。もともとGUIスレッド(MainWNDクラス)のメンバー関数ではありませんので、main.cppに定義している意味はありません。同様にメンバー関数ではない と共にディスクユーティリティー関数として別ファイル(diskutil.h、diskutil.cpp)にまとめます。ライブラリ化しても構いません。

分割の進捗状況報告と分割先容量不足はGUIスレッドと同期を取るためSynchronize関数を経由して呼び出します。

ReportDividing関数

//---------------------------------------------------------------------------
void __fastcall TDivideThread::ReportDividing()
{
    OwnerForm->Perform(ReportMessage, DivideIndex, DivideStatus);    // 分割ファイルインデックス、進捗状況をメッセージ通知
}
//---------------------------------------------------------------------------

NoDivideSpace関数

//---------------------------------------------------------------------------
void __fastcall TDivideThread::NoDivideSpace()
{
    int ChangeMedia = OwnerForm->Perform(NoSpaceMessage, 0, 0);      // 分割先容量不足をメッセージ通知
    switch (ChangeMedia){                                            // 返答を解読
        case dsNoSpace:     DivideStatus = dsNoSpace;   break;       // 分割容量不足
        case dsUserBreak:   DivideStatus = dsUserBreak; break;       // ユーザー中断
    }
}
//---------------------------------------------------------------------------
以上で分割スレッドクラスのプログラミングは完了です。あとはGUIスレッドが分割スレッドをインスタンス(実装)し、利用します。


GUIスレッド

さて、やっとGUI部分の改造です。スレッド間同期でGUIスレッドに記述した内容をプログラミング、その前にGUIスレッドが分割スレッドを利用できるようにします。main.cppのソースをアクティブにして「ファイル」「ユニットを使う」でGUIスレッドを取り込みます。ソースレベルでは、
#include "dividtrd.h"
がmain.cppに追加されます。これで分割スレッドクラスが自由に使えます。

分割スレッドオブジェクトとコピー分割ファイルリスト

スレッド間同期を見るとわかるように、分割スレッドオブジェクトはGUIスレッドの複数箇所から制御されます。ですからMainWNDクラスのグローバル的なオブジェクトとなります。ここでもうひとつ少々したくないことを追加しなければなりません。GUIスレッドから分割スレッドに渡す情報の中に「分割ファイルリスト」がありますが、これはマルチスレッドの衝突回避の関係でコピーを渡す必要があります。分割スレッドが働いている間はコピーを破棄できないため、コピーを作成する関数と破棄する関数が異なってしまいます。従ってまたまたグローバル変数にしなければなりません。他になにかスマートな方法があればいいのですが。
分割スレッドオブジェクトとコピー分割ファイルリストは密接に関連しています。分割スレッドが動いているときはコピー分割ファイルリストが有効で、分割スレッドがなくなったときにはコピー分割ファイルリストもなくなります。つまりペアで働きます。生成や破棄のタイミングも同じです。まずは両者をMainWNDクラスのプライベートメンバとして登録します。
TDivideThread *DivideTRD;       // 分割スレッドオブジェクト
TStringList   *DivideFiles;     // コピー分割ファイルリスト
続いて管理方法を決めます。
  1. オブジェクトポインタがNULLの場合は無効である。
  2. オブジェクトポインタがNULLでない場合は生成(インスタンス)されている。
  3. MainWNDが生成された時は両者とも無効(NULL)とする。
  4. 分割実行時にコピー分割ファイルリスト、分割スレッドを生成する。
  5. 分割が完了したときに両者とも破棄し、無効化(NULLを設定)する。
3番目の設定はMainWNDのコンストラクタで行います。
//---------------------------------------------------------------------------
__fastcall TMainWND::TMainWND(TComponent* Owner)
    : TForm(Owner)
{
    DivideTRD = NULL;       // 分割スレッドなし
    DivideFiles = NULL;     // 分割ファイルリストなし
}
//---------------------------------------------------------------------------
4番目、5番目は個々の関数で処理を行います。

分割→GUIスレッド通知メッセージ

分割スレッドからGUIスレッドに通知するユーザー定義Windowsメッセージを定義します。WindowsではWM_USERから上を自由に使えますので、WM_USER+1とWM_USER+2に割当てています。main.hに以下を定義します。
#define FDM_REPORTDIVIDING (WM_USER+1)              // 分割進捗状況レポート
    // WParam (int) 分割ファイルインデックス番号(0〜分割数+1)
    // LParam (int) 分割進捗状況(0〜100)
    // Result (void)
#define FDM_NOWRITESPACE (WM_USER+2)                // 分割先容量不足
    // WParam (long) 0
    // LParam (long) 0
    // Result (int)  dsSuccess = メディア変更 / dsNoSpace = 容量不足(中断) / dsUserBreak = ユーザー中断
さてGoDivideMNUClick関数です。スレッド間同期のGUIスレッドの流れに加え、分割中断イベントを受けるためにコールバック関数の追加と、進捗状況表示のためのプログレスバー初期設定を行います。分割中断コールバック関数は「分割」メニューの「分割実行...」を「分割中断」に、さらにコールバック関数をGoDivideMNUClick関数からStopDivideMNUClick関数に入れ替えて処理します。

GoDivideMNUClick関数

//---------------------------------------------------------------------------
void __fastcall TMainWND::GoDivideMNUClick(TObject *Sender)
{
// 分割実行チェック
    AnsiString DrivePath = StatusBAR->SimpleText.SubString(1, 3);   // ドライブパス取得
    bool Removable = (GetDriveType(DrivePath.c_str())==DRIVE_REMOVABLE);
    if (!Removable){                                // 固定ディスク
        if (!CheckDivide2Fix(DrivePath)) return;    // 分割中断
    }

// 分割リスト作成
    DivideFiles = new TStringList;      // 分割ファイルコピーリスト作成
    for (int i=0;i<ListVEW->Items->Count;i++){      // 全ての分割ファイルの
        TListItem *Item = ListVEW->Items->Item[i];  // 分割ファイルオブジェクト取得
        DivideFiles->AddObject(Item->Caption, (TObject*)Item->Data);    // 名前と分割ファイルオブジェクトをコピー
    }

// 分割スレッド生成
    AnsiString DstFilePath = StatusBAR->SimpleText;    // 分割先パス作成
    if (DstFilePath[DstFilePath.Length()]!='\\'){      // パス区切り文字追加
        DstFilePath = DstFilePath + "\\";
    }
    unsigned int Messages[2];               // 2つのユーザー定義メッセージを登録
    Messages[0] = FDM_REPORTDIVIDING;           // 分割進捗状況通知
    Messages[1] = FDM_NOWRITESPACE;             // 分割先容量不足通知
    DivideTRD = new TDivideThread(this,     // スレッドオブジェクト生成
                                  Messages,
                                  OpenDLG->FileName,
                                  DstFilePath,
                                  DivideFiles,
                                  MakeConnectBatch(OpenDLG->FileName.c_str(), ListVEW->Items->Count-1),
                                  GetCutSize());
    DivideTRD->OnTerminate = OnDivideTRDTerminate;    // 分割スレッド終了処理登録

// 分割実行中画面設定
    GoDivideMNU->Caption = "分割中断(&D)";            // 分割実行→中断メニュー変更
    GoDivideMNU->OnClick = StopDivideMNUClick;        // 分割中断コールバック登録
    DividePRG->Position = 0;                          // 分割状況プログレスバー初期化
    DividePRG->Visible = true;                        // 分割状況プログレスバー表示
    StatusBAR->Visible = false;                       // ステータスバー非表示

// 分割スレッド実行
    DivideTRD->Resume();
}
//---------------------------------------------------------------------------

StopDivideMNUClick関数

//---------------------------------------------------------------------------
void __fastcall TMainWND::StopDivideMNUClick(TObject *Sender)
{
    if (DivideTRD!=NULL) DivideTRD->Terminate();    // 分割スレッドが有効な場合は終了
}
//---------------------------------------------------------------------------
続いて分割スレッドからの通知メッセージ処理を行います。まずmain.hにイベントハンドラーを登録します。
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(FDM_REPORTDIVIDING, TMessage, OnReportDividing)
MESSAGE_HANDLER(FDM_NOWRITESPACE, TMessage, OnNoWriteSpace)
END_MESSAGE_MAP(TForm)
同時にコールバック関数の宣言をTMainWNDクラスに登録します。分割終了イベントのコールバック関数宣言も同時に登録しましょう。
void __fastcall OnReportDividing(TMessage &Message);
void __fastcall OnNoWriteSpace(TMessage &Message);
void __fastcall OnDivideTRDTerminate(TObject *Sender);
実際のコールバック処理をプログラミングします。

OnReportDividing関数

//---------------------------------------------------------------------------
void __fastcall TMainWND::OnReportDividing(TMessage &Message)
{
    int Index =   (int)Message.WParam;      // 分割ファイルインデックス番号取得
    int Percent = (int)Message.LParam;      // 分割進捗状況取得

    ListVEW->Selected = ListVEW->Items->Item[Index];    // 分割中ファイル選択
    ScrollListToViewItem(ListVEW);                      // 分割中のファイルが見えるようにスクロール
    DividePRG->Position = Percent;          // 分割進捗状況をプログレスバーに表示
}
//---------------------------------------------------------------------------

OnNoWriteSpace関数

//---------------------------------------------------------------------------
void __fastcall TMainWND::OnNoWriteSpace(TMessage &Message)
{
    AnsiString DrivePath = StatusBAR->SimpleText.SubString(1, 3);    // ドライブパス取得
    bool Removable = (GetDriveType(DrivePath.c_str())==DRIVE_REMOVABLE);    // ドライブタイプ取得
    if (!Removable){                    // 固定メディア?
        Message.Result = dsNoSpace;     // 空き容量不足返答
        return;
    }
    int okcancel = Application->MessageBox("ディスクを交換してください", "分割実行", MB_OKCANCEL);
    if (okcancel==IDCANCEL){           // 交換中断?
        Message.Result = dsUserBreak;  // ユーザー中断返答
        return;
    }
    Message.Result = dsSuccess;        // メディア交換返答
}
//---------------------------------------------------------------------------

OnDivideTRDTerminate関数

//---------------------------------------------------------------------------
void __fastcall TMainWND::OnDivideTRDTerminate(TObject *Sender)
{
// 分割完了報告
    AnsiString Message = "";        // エラーメッセージクリア(空文字=正常終了)
    switch (DivideTRD->GetDividedStatus()){     // 分割実行ステータス取得
        case dsNoSpace:     Message = "空き容量が不足しています";       break;
        case dsSrcError:    Message = "分割元ファイルを開けません";     break;
        case dsReadError:   Message = "分割元ファイルを読み込めません"; break;
        case dsDstError:    Message = "分割先ファイルを開けません";     break;
        case dsWriteError:  Message = "分割先ファイルに書き込めません"; break;
    }
    if (Message!="") Application->MessageBox(Message.c_str(), "分割実行", MB_ICONEXCLAMATION);
                                                    // エラー発生時、メッセージ表示

// 分割開始画面設定
    ListVEW->Selected = NULL;                       // 分割中ファイル選択消去
    GoDivideMNU->Caption = "分割実行(&D)";          // 分割中断→開始メニュー変更
    GoDivideMNU->OnClick = GoDivideMNUClick;        // 分割実行コールバック登録
    DividePRG->Visible = false;                     // 分割状況プログレスバー非表示
    StatusBAR->Visible = StatusBarMNU->Checked;     // ステータスバー表示

    delete DivideFiles;                             // コピー分割ファイルリスト解放
    DivideFiles = NULL;                             // コピー分割ファイルリストなし
    DivideTRD = NULL;                               // 分割スレッドなし
}
//---------------------------------------------------------------------------

いやいや今回は寿分割の画面ヅラはあまり変わらないにも関わらず、話もプログラムも長くなってしまいました。マルチスレッドはプログラミング技法的にも中級から上級の部類に入ると思います。日曜日にBCB!のコーナーでは難しすぎるかなとも思ったのですが、「必要は発明の母である」に基づきできるだけ詳しく解説したつもりです。私の思い違いもあるかもしれません。間違いの指摘、ご意見等は是非 クリックするとメーラーを起動します までご連絡ください

第8話ビルド結果 分割実行中


こちらから今回作成したBCBのプロジェクト一式をダウンロードできます。ダウンロード(16,111バイト) BCBのバージョン3、4、5でビルドできます。参考にしてください。BCBバージョン5ではの修正を行ってください。


次回は寿分割を使いやすくします。ツールバーやドラッグアンドドロップをサポートして、一人前のアクセサリに仕上げたいと思います。お楽しみに。

今回のプログラミング行数: 243行
今回までのプログラミング行数: 495行


目次に戻る目次に戻る トップに戻るトップに戻る