・概要
(データ共有)
プロセス間通信は一言でいうと「プログラム同士でのデータのやりとり」です。
同一、または異なったEXEファイルを実行して、一方のプログラムが他方のプログラム内の
データを使って計算してもとのプログラムに戻すなどといったことを指します。あるいはもっと
単純に他のプログラムのデータを取得するといった目的でも使われます。
人間同士で通信する場合は原始的には紙にデータを書いて他者に渡せばいいのですが、
コンピュータの場合ではそう簡単にはいかないです。
コンピュータの世界でのデータとはメモリに展開された数値の集合体です。
そのメモリはプロセスごとに管理されていて他のプロセス(≒プログラム)からはそのメモリの
アドレスには直接アクセスできません。このあたりのOSのプロセス/スレッド/メモリ管理の
ことは今回の本題ではないし、私自身も詳しくないのでこれ以上は書けないので他のサイトで
調べてください。
ちなみに、私みたいなものでも以下のサイトはわかりやすかったので紹介しておきます。
http://yokohama.cool.ne.jp/chokuto/advanced/memory.html
異なったプロセス間では直接他者のメモリアドレスにアクセスできないのですが、 当然それを何とかする方法がいくつかあり、今回はその方法のうちのひとつをお題としてみます。
(同期)
マルチスレッド/プロセス間通信は話題としてはかなり難しいです。 (こんなの簡単とおっしゃる方は相当頭脳明晰でうらやましい限りです。)なぜ難しいかというと このプロセス間のデータ共有ということもありますが、それ以上にいわゆる同期という 問題があるからです。ここでデータを共有できたとして考えてみます。
プログラムの実行はスレッド単位で行われています。プロセス内では1つ以上の
スレッドが稼動していて、2つ以上のスレッドをもつプロセス(プログラム)をマルチスレッドといいます。
話が難しくなるので、2つのプロセスともスレッドが1つの場合を考えてみます。
通常プログラムの流れは上から下でその筋は1つです。同時刻にある関数が
同時に実行されたり、データにアクセスされることはありません。それはプロセス内の
あるスレッド(大抵はメインスレッド)内では流れは1つだからです。
しかし、2つのプロセスのそれぞれのスレッドはお互い影響しあうことなしに 独立で動作してます。例えばAプロセス内にデータαがあり、この値に1加算しようとしています。 普通でしたらα+1が結果として返ってくることが期待できます。プログラムの流れが1つ の時は他が入り込む余地がないので当然そうなりますが、もうひとつのプロセスからデータαに アクセスされる可能性があると話は違ってきます。Aプロセスが「データαに1加えろ」と命令してから 計算するまでにBプロセスから「データαに2加えろ」という命令が入り込んだときどうなるでしょう。 α+2というBプロセスの命令に加えてもともとの1加算が加わってα+3という結果がAプロセスに 返るでしょう。一方Bプロセスもα+2が返るかもしれないし、Aプロセスの命令が加えられて しまうかもしれません。ということで、Aプロセスの計算が済むまでデータα自体にアクセスさせない(ロック) や、BプロセスをAプロセス計算終了まで待機させるなどといったことが必要となってきます。 この作業が同期ということになります。
・データ共有の方法
まず、データ共有の方法ですが次のような方法があります。
1.COMの利用
2.メッセージによる通信での共有(WM_COPYDATAを含む)
3.ソケットの使用
4.名前付きパイプの使用
5.共有メモリの作成
・
・
<ファイルマッピングオブジェクトによる共有メモリとしての利用>
ファイルマッピングオブジェクトとはもともとファイルシステム内のファイル
(エクスプローラで確認できる各ファイル)をまるでメモリに読み込んだかのようにプロセスの
アドレス空間に割り当てる(マッピング)ことができるオブジェクトです。このオブジェクトを
使えば、いちいちファイルをオープンして読み込まなくてもメモリ上のデータとして利用できます。
ただ実際はファイルなので、メモリ上に展開されたインスタンスという意味でのオブジェクトとは
異なります。しかし、アドレス空間に割り当ててメモリアドレスとして利用できるので
オブジェクトとみなすことができます。
ということで、ファイルマッピングオブジェクトを利用するにはプロセスに 割り当てる実際の物理ファイルがいるのですが、今回の場合は、そんな物理ファイルを 利用しようとしているわけではなく、複数のプロセスから利用できる共有メモリの器として ファイルを使いたいだけなのに、いちいちファイルを作らなければならないのは面倒です。
実際にファイルマッピングオブジェクトを作るには次のAPIを使います。
HANDLE CreateFileMapping( HANDLE hFile, // ファイルのハンドル LPSECURITY_ATTRIBUTES lpAttributes, // セキュリティ DWORD flProtect, // 保護 DWORD dwMaximumSizeHigh, // サイズを表す上位 DWORD DWORD dwMaximumSizeLow, // サイズを表す下位 DWORD LPCTSTR lpName // オブジェクト名 );
第一引数に物理ファイルのハンドルを指定してオブジェクトを作成しますが、ここに INVALID_HANDLE_VALUEを指定すると、ファイルを実際につくる必要がなくなり、共有メモリとして 使うことができるようになります。この場合のファイルを作る必要がないという意味はユーザが 明示的に作る必要がないということで、OSがページングファイル上に勝手に作っちゃってます。
このページングファイル、PCに積まれた物理メモリのサイズでは足りなくったときに OSが仮想メモリとして使うファイルで、コントロールパネル→システム→詳細タブ→パフォーマンスオプション で表示される(Windows2000の場合)ダイアログに表示される仮想メモリのサイズ分だけ存在してます。
第二引数は取得したハンドルを子プロセスへ継承できるかを決めるためにありますが、 今回はNULLを指定します。
第三引数は取得したオブジェクトを読み込みや読み書き可能などの属性を設定します。 プロセス間の通信では読み書きできないと意味がないので、PAGE_READWRITEを指定します。
第四、第五引数は作成するファイルマッピングオブジェクトのサイズです。
2つの引数で32ビット+32ビット=64ビット分のサイズ、2の64乗分のサイズが指定できます。
このうち第四引数は2の32乗分、つまり4Gより大きくなる分を指定することになります。
32ビットOSが管理できるメモリサイズ4Gよりも大きなオブジェクトを作ることはまれなので、
実際は第四引数は0で、第五引数に必要サイズを指定することになります。
第六引数はファイルマッピングオブジェクトの名前です。ここに名前を指定して、
あるプロセスがファイルマッピングオブジェクトを作成し、その後他のプロセスが同じ名前で
CreateFileMappingを実行してオブジェクトを作成しようとすると、別のオブジェクトを作るのではなく、
先に作られたオブジェクトへアクセスしようとします。これにより、2つのプロセスが
同一のものにアクセスできるようになります。
もし、あるプロセスがNULLを指定してファイルマッピングオブジェクトをつくり、通信しようとする
別プロセスも同様に名前なしファイルマッピングオブジェクトを作ると、2つのプロセスで作られた
オブジェクトは別物になり共有ができなくなるので、共通の名前を指定する必要があります。
ファイルマッピングオブジェクトで注意しないといけない点は、 管理がプロセスではなくOSレベルということです。もし、プロセスレベルの管理なら、 最初に書いたようにマッピングされた状態で他のプロセスからはアクセスできません。 ですが、実際はその上のOSレベルの管理なので複数のプロセスからOSに対してアクセス要求できる ということです。それを可能にしているのが、CreateFileMappingであり、次のMapViewOfFileです。
<ファイルマッピングオブジェクトを各プロセスのアドレス空間へ割り当てる(マッピング)>
CreateFileMappingでファイルマッピングオブジェクトを作ることはできましたが、 プロセスのアドレス空間に割り当てないとポインタでデータにアクセスできません。 この割り当てられたアドレス領域をビューといいますが、オブジェクトを実際にプロセス内で 投影させたものと理解しています。無理があるかもしれませんが、データベースのテーブルが ファイルマッピングオブジェクトとすると、その内容を表示するUI(フォームなど)が ビューに当たるのではないでしょうか。
このビューをアドレス空間に割り当てるのがMapViewOfFileです。
LPVOID MapViewOfFile( HANDLE hFileMappingObject, // ファイルマッピングオブジェクトのハンドル DWORD dwDesiredAccess, // アクセスモード DWORD dwFileOffsetHigh, // オフセットの上位 DWORD DWORD dwFileOffsetLow, // オフセットの下位 DWORD SIZE_T dwNumberOfBytesToMap // マップ対象のバイト数 );
第一引数は、前項のCreateFileMappingで作られた ファイルマッピングオブジェクトのハンドルです。
第二引数はビューに対しての読み書きの設定ができます。ただし、 CreateFileMappingの第三引数で指定した以上のアクセスはできません。つまり、 ファイルマッピングオブジェクトでPAGE_READONLYで読込みのみなのに、ビュー側で FILE_MAP_WRITEの書込み可は指定できません。逆に、ファイルマッピングオブジェクトで PAGE_READWRITEの指定であれば、ビューは FILE_MAP_WRITEで読み書き可能にできますし、 FILE_MAP_READで書込みを制限することもできます。
第三、第四引数は作成されたファイルマッピングオブジェクトのどの位置から ビューとして認識させるかを指定します。4G以上のファイルマッピングオブジェクトがあって、 ビューの開始位置をそれ以上の位置に設定する場合は第三引数を設定する必要があります。 ファイルマッピングオブジェクトの先頭から割り当てる場合は、両者とも0を指定します。
第五引数はビューに割り当てるファイルマッピングオブジェクトのサイズを 指定します。0の時はオブジェクト全体をマップします。
MapViewOfFileが成功するとビューが設定されたアドレスの先頭が返ってくるので、 その領域を操作するとファイルマッピングオブジェクトを操作していることになり、 他のプロセスからも参照できるようになります。
これまでの共有メモリの仕組みを図示してまとめると以下のようになります。
・同期の種類
これでめでたく複数プロセスからアクセスできる共有メモリができあがりました。 今度はそのために最初に書いたようなデータの不整合が起こる可能性がでてくるので、 排他制御などの同期を考慮しなければなりません。同期をとるためのしくみは 以下のようなものが用意されています。
同期の種類 | MFCクラス | 機能 |
クリティカルセクション | CCriticalSection | この同期オブジェクトが一番理解しやすかと思います。複数のスレッド間での相互排他の同期を 行います。あるスレッドがクリティカルセクションの所有権を取得すると、他のスレッドで、 このクリティカルセクションの所有権を取得しようとしても、最初に所有権を取得したスレッドが 解放するまで制御が返らず、共有リソースにアクセスすることを防ぐことができます。 |
ミューテックス | CMutex | クリティカルセクションと同様の機能ですが、こちらは複数のプロセス間での共有リソースの 同期に用いられます。クリティカルセクションは1つのプロセス内での複数のスレッドでの 同期ですが、ミューテックスは複数プロセス間での同期に使われます。 |
イベント | CEvent |
いわゆる「待ち合わせ」機能です。初期化のときにイベント名を指定できるのでプロセス間の
同期でも有効です。プロセスAで共有データの更新をして、プロセスBがそのデータを使って
印刷などをする場合、プロセスBはプロセスAのデータ更新完了まで印刷しないで待機する
必要があります。この場合、両プロセスで同名のイベントを非シグナルで作成して、
プロセスBは待機関数で待機させます。 プロセスAでデータ更新が完了した段階でイベントをシグナル化すると、待機していた プロセスBが解放されて、それ以降のコード(印刷)をします。 このとき、プロセスAのシグナル化は、プロセスBの実行のトリガーとしての機能を 果たしていて、プロセスBはプロセスAの作業完了を「待ち合わせ」ています。 |
セマフォ | CSemaphore |
慣れない単語でもあり分かりづらいかもしれませんが、「共有データアクセス許可証」が
いちばんぴったりかと思います。共有データにアクセス可能なスレッド数(アクセス許可証の枚数)を
指定してセマフォを作成します。 その後、他のスレッドがその共有データにアクセスしようと待機関数を実行すると、セマフォが カウントを減らしていき(許可証を渡す)、カウントが0になるまでは待機関数を実行したスレッドは シグナルになり、以降のコードを実行できます。0になった時点で待機関数を実行したスレッドは、 他のセマフォを獲得したスレッドが返還しない限り、待機状態を維持します。 |
各同期オブジェクトの機能は上表のとおりですが、4つのMFC同期クラスの基底クラスに CSyncObjectがあり、これらの同期オブジェクトをサポートするCSingleLock、 CMultiLockクラスがあるので少し書いておきます。
CSyncObjectは4つの同期クラスの基底クラスで、仮想関数CCyncObject::Lock()で 待機関数を呼び出して、共有データへのアクセス権を要求していて、各派生クラスでは オーバーライドされていません。なので、他のプロセス/スレッドから許可がおりるまで (他のスレッドが共有データにアクセスし終わるまで)待機しつづけることになりデータの 不整合が起こることを防いでいます。
待機関数から制御が戻って、共有データへの占有アクセス権を取得したとします。 データへの処理を施したらそのままにしてはいけません。他のスレッドが共有データにアクセス できるように許可を与えなければなりません(占有権を放棄しなければなりません)。 占有権の放棄は各同期クラスのUnlockメンバ関数で実行できるのですが、これは自動でやってくれる わけではないので、例外・エラーが起きたりするとアプリ自体がフリーズする可能性がでてきます。
そこで、これを自動的に?やってくれるようサポートしているのが
CSingleLock、CMultiLockクラスです。このクラスは上記4つのいずれか、または複数を
メンバ変数として保持して、各クラスのLock、Unlockを代行してやってくれます。Lockに関しては
代行する意味はあまりないですがUnlockは自動でやってくれるので安心です。
どう自動でやっているかですが、簡単です。CSingleLock、CMultiLockのデストラクタで
CSingleLock::Unlock、CMultiLock::Unlockを呼び出していて、そこでさらに4つ各同期クラスの
Unlockを呼んでいるので、わざわざ Unlockを呼ばなくてもCSingloLock、CMultiLock
インスタンスがスコープから外れたら占有権が放棄されます。
とういうことで、MSはこの2つのクラスを使用することを推奨しているみたいです。 ただ、CEventに関しては、その機能が「待ち合わせ」で、自動のUnlock機能は意味ないような気がしますが その理由はCEventの解説で。
<CCriticalSection>
上表で各同期オブジェクトの機能を簡単に書きましたが、これだけではよくわからないかと 思いますので、各同期クラスについてもう少し詳しく書きます。
CCriticalSectionですが、排他制御の機能を持っていて以下のように使います。
スレッドA | スレッドB |
class CMyClass { public: int m_nShare; CCriticalSection m_critsec; ・ ・ }; void CMyClass::Func() { CSingleLock slock(&m_critsec); //アクセス権要求 slock.Lock(); //(C) //共有リソースへのアクセス m_nShare++; //slockはスコープを抜けたら //自動的にUnlockを実行するので //以下の記述は省略可 /*slock.Unlock();*/ } //---------------------------------- void Cxxx::OnTimer(UINT_PTR nIDEvent) { m_MyClass.Func(); //(A) } |
//セカンダリスレッド制御関数 UINT ThreadFunc(LPVOID pParam) { CMyClass *pMyClass = NULL; pMyClass = static_cast<CMyClass *>(pParam); pMyClass->Func(); //(B) return 0; } |
スレッドAとBは互いに無関係に動作しています。スレッドAでCMyClassが定義されていて、
何かしらのウィンドウのタイマーで一定間隔でCMyClass::Funcが実行されているとします。
スレッドAからスレッドBをつくり、その際にm_MyClassへのポインタを渡しているとします。
スレッドBの制御関数内でも同様にpMyClass->Funcが実行されています。ですから、Funcに同時に
アクセスされてm_nShareに不整合がおきる可能性がありますが、Func内では
クリティカルセクションを使っているので大丈夫です。
例えば、スレッドAの(A)でFuncが実行されたとします。(C)では他のスレッドは アクセス権を持ってないので、即制御を返して次の行に移ります。この時スレッドBが(B)を 実行したとします。(C)までは進みますが、スレッドAで既にアクセス権を占有されているので、 スレッドAのFuncが終了するまで(スレッドAのslock.Unlock()が実行されるまで)、 ここで待機し続けます。スレッドAでFuncが終了したら、今度はスレッドB側で待機していた(C)で アクセス権を取得して次の行以降に進むことができます。こうしてm_nShareに同時に アクセスしないように同期がとられています。
<CMutex>
機能はクリティカルセクションと同様ですが、 クリティカルセクションは1つのプロセス内の複数スレッドでの同期ですが、ミューテックスは 主に複数プロセス間での同期でつかわれます。また、ミューテックスに名前をつけることで 複数のミューテックスで管理できたり、構築時にアクセス権の所有をコントロールできます。 プロセス間の排他制御を行えるので、よくアプリケーションの二重起動の防止に使われたりします。 その例でCMutexの使用例を挙げます。
class CxxxApp : public CWinApp { ・ ・ protected: CMutex m_mutex; //順番が大事 CSingleLock m_slock; ・ }; //コンストラクタ CxxxApp::CxxxApp() : m_mutex(FALSE, _T("mtname"), NULL)//任意のミューテックス名 , m_slock(&m_mutex) { } BOOL CxxxApp::InitInstance() { m_slock.Lock(0); if (! m_slock.IsLocked() ) { ::AfxMessageBox(_T("二重起動です")); return FALSE; } ・ ・ } |
CWinAppの派生クラス(アプリケーションクラスのメンバに、CMutexとCSingleLockの メンバを加えます。コンストラクタを見ればわかりますが、この時CMutexを先に定義しないと アサーションが起きます。最初に起動したプロセスでは、対象名前のミューテックスはどこも所有権を 取得してないので、CxxxApp::InitInstance内で取得してm_slock.IsLocked()内は進まないので そのままアプリケーションが起動します。その後、同じアプリケーションを起動しようとしとき、 InitInstanceで所有権を獲得しようとしますが、既に同名のミューテックスが前のプロセスで 獲得してしまっているのでIsLockedはFALSEを返して結果、InitInstanceの戻値がFALSEになり 起動できないということになります。
<CEvent>
この同期オブジェクトは「待ち合わせ」機能を実装しています。 スレッドAがある作業を完了するまで、スレッドBが共有リソースにアクセスさせたくない場合に使用します。 両スレッドで初期状態を非シグナル状態(待機関数を呼ぶと待機状態なる状態)で作成し、Aで先に 行わなければならない処理をしたのち、イベントのSetEventを呼ぶとイベントがシグナル状態 (待機関数から制御がもどってくる)になるので、Bスレッドで次に実行する共有リソースへの処理を 行うことができるようになります。
スレッドA (プロセスA) | スレッドB (プロセスB) |
class CMyClass { int m_nShare; //共有リソース }; void CMyClass::Func() { CEvent event(FALSE, FALSE, _T("evtname")); m_nShare++; event.SetEvent(); //(B) event.ResetEvent(); //(C) } |
//セカンダリスレッド制御関数 UINT ThreadFunc(LPVOID pParam) { int *pShare = static_cast<int *>(pParam); CEvent event(FALSE, FALSE, _T("evtname")); CSingleLock slock(&event); slock.Lock(INFINITE); //(A) *pShare++; return 0; } |
例えばスレッドAでCMyClassのコンストラクタでスレッドBを
起動したとします。スレッドBでは非シグナル状態(bInitialOwn引数がFALSE)でイベントが作成され、
CSingleLockオブジェクトにラップされます。その後、他スレッドに同名のイベントもなく、現在の状態が
非シグナル状態なので(A)で待機し続けます。
共有リソースにアクセスする段階でスレッドAでFuncを呼ぶと、共有リソースの演算の後SetEventを
呼んでスレッドBの(A)以降の実行を促すトリガを引いています。
(C)でResetEventを実行しているのは一応意味があります。この例の場合はスレッド間の
同期ですが、プロセス間の場合に少し問題がおきます。スレッドA・BをプロセスA・Bに置き換えて、
(C)の記述がないことを考えてみます。プロセスBが起動していない段階でプロセスAのCMyClass::Funcを
実行した時、SetEventが実行されイベントがシグナル状態になります。ところが、プロセスBはないので
スレッドが開放されないのでそのままイベントはシグナルのままでいつづけます。
SetEvent実行後は少なくとも1つのスレッドが開放されない限りずっとシグナル状態のままです。
(自動イベント場合)
この状態でプロセスBを起動したら(A)で待機することはなく、いきなり共有リソースにアクセス
してしまいます。プロセスAで(A)の処理をしているので問題ない場合もあるかもしれませんが、
アプリの種類によってはまずい場合があります。実際、このあとで紹介するアプリでは問題がでてきます。
ということで誤解がでてくることを承知であえて(C)を加えました。
また、スレッドBでCSingleLockを使っていますが、 本当はCSingleLockを使うメリットはあまり感じないです。というのはこのオブジェクトを使うメリットは 自動的に行われるUnlockなのですが、CEventに関しては以下のとおりUnlockでは何もしてないからです。
BOOL CEvent::Unlock() { return TRUE; }
それは自動イベントという機能があるという理由からかも しれないですが、個人的にはイベントにはCSingleLock、CMultiLockを使わないほうが楽かと思います。
<CSemaphore>
クリティカルセクションやミューテックスは相互排他で 1つのスレッドがアクセス権を取得したら、他は共有リソースにアクセスできませんが、セマフォは 設定数のスレッドだけアクセスを許可する同期オブジェクトです。コード自体は クリティカルセクションと同じような感じですが、排他制御ではないので複数スレッドがアクセス権を 取得できる点が異なります。
スレッドA | スレッドB |
class CMyClass { public: CSemaphore m_sema; //共有リソース int m_nShare; }; CMyClass::CMyClass() : m_sema(1, //(A) 1, _T("semaname"), NULL) , m_nShare(5) { } void CMyClass::Func() { CSingleLock slock(&m_sema); slock.Lock(INFINITE); //(C) TRACE(_T("値-%d\n"), m_nShare); } |
//セカンダリスレッド制御関数 UINT ThreadFunc(LPVOID pParam) { CMyClass *pMyClass = NULL; pMyClass = static_cast<CMyClass *>(pParam); CSingleLock slock(&(pMyClass->m_sema)); slock.Lock(INFINITE); //(B) TRACE(_T("値-%d\n"), pMyClass->m_nShare); return 0; } |
(A)の第二引数で最大の共有リソースの
最大アクセス数を1に設定し、初期値を第一引数で1に設定しているので、最初はどのスレッドも
アクセス権を所有していない状態です。適当な時期にスレッドBを起動し、ThreadFuncが実行され
(B)でセマフォを獲得します。
このときセマフォのカウント(アクセス許可証)は1減って0となります。その後スレッドBの
制御関数が実行中にスレッドAがFuncを実行して(C)まで着ても、スレッドBがセマフォを
獲得していてカウントが0なので待機状態になります。もし、(A)で
m_sema(2, 2, _T("semaname"), NULL)
となっていたらアクセス権は2つあるので(C)で待機状態になることはありません。 ここが、クリティカルセクションと大きくことなるところです。
リソースの共有方法と同期方法がプロセス間通信の必要条件ですが、 実際にプログラミングしてみると感じると思いますが、とても難しいです。理由は最初に書いたとおり コード実行の流れが1つではないからです。常に他のプロセス/スレッドからのアクセスを 念頭に置かないとバグになるからです。ということで、次に実際にごく簡単なアプリを掲載します。 機能としては実用性のないごく簡単なものですが中身はそこそこ濃い?と思ってます。