スリム・リーダー/ライター・ロック (Slim Reader/Writer Lock, SRW Lock) の使い方

スリム・リーダー/ライター・ロックとは?

スリム・リーダー/ライター・ロック (Slim Reader/Writer Lock, SRW ロック) を用いると、単一プロセス内でのスレッドを同期することができます。 単一プロセス内のスレッドの同期、というと、クリティカルセクションによる同期を行うことが一般的でしたが、 SRW ロックはクリティカルセクションより使用メモリが少なく高速に動作します。

また、名前が示しているようにリーダースレッド (共有データを読むスレッド) とライタースレッド (共有データに書き込むスレッド) のそれぞれに対して、異なる動作をすることができます。

リーダースレッドはデータを読むだけなので、複数のスレッドがグローバルな共有データブロックにアクセスしても本来問題ありません。 読み込みが複数同時に起こってもデータは壊れないからです。データが壊れるのは書き込みのときです。 このため、データを読むだけの場合に、スレッドを同期させるためにスレッドをブロックするのは無駄です。

そこで、読み込みの処理は同時処理を許可。しかし、書き込みは1つのスレッドからのみを許可、ということができればブロックされるスレッドは少なくて済みます。

それを実現するのが SRW ロックです。

データを読みたいだけのリーダースレッド (Reader thread) は、共有モード (shared mode) で SRW ロックを取得します。 一方、データに書き込みを行いたいライタースレッド (Writer thread) は、排他モード (exclusive mode) で SRW ロックを取得します。

こうすると、リーダースレッドは同時に複数 SRW ロックを取得しますが、ライタースレッドは (リーダー・ライター両方含めて) ひとつのスレッドだけが SRW ロックを取得します。

動作確認

Exclusive で保護されることを確認

#include <windows.h>
#include <process.h>
#include <stdio.h>


SRWLOCK g_SrwLock;
LONG g_nData = 0;

unsigned int __stdcall ThreadFunc(PVOID pv)  {

     DWORD dwThread = GetCurrentThreadId();

     for ( int i=0; i<10000; i++ ) {

          AcquireSRWLockExclusive( &g_SrwLock );
          g_nData++;
          ReleaseSRWLockExclusive( &g_SrwLock );
     }

     for ( int i=0; i<10000; i++ ) {

          AcquireSRWLockExclusive( &g_SrwLock );
          g_nData--;
          ReleaseSRWLockExclusive( &g_SrwLock );
     }

     return 0;
}



int main( int argc, char* argv[] ) {

     DWORD dwThread[2];
     HANDLE hHandle[2];

     //
     //  SRW ロックの初期化
     //

     InitializeSRWLock( &g_SrwLock );


     //
     // スレッド 1
     //

     hHandle[0] = (HANDLE)_beginthreadex(
         NULL, 0, ThreadFunc, NULL,
         CREATE_SUSPENDED, (unsigned int*) &dwThread[0]);

     if( !hHandle[0] ) {
         printf( "_beginthreadex Failed.\n" );

         return 1;
     }


     //
     // スレッド 2
     //

     hHandle[1] = (HANDLE)_beginthreadex(

         NULL, 0, ThreadFunc, NULL,
         CREATE_SUSPENDED, (unsigned int*) &dwThread[1]);

     if( !hHandle[1] ) {
         printf( "_beginthreadex Failed.\n" );

         return 1;
     }

     //
     // スレッドを走らせる
     //

     ResumeThread ( hHandle[0] );

     ResumeThread ( hHandle[1] );

     //
     // Wait the threads end
     //

     WaitForMultipleObjects (
         sizeof(hHandle)/sizeof(hHandle[0]),

         hHandle,
         TRUE,
         INFINITE );

     //
     // クリーンアップ
     //

     CloseHandle ( hHandle[0] );

     CloseHandle ( hHandle[1] );

     printf( "g_nData = %d\n", g_nData );

     return 0;
}

実行結果は次の通り。何度やっても正しい値になります。

>srw1
g_nData = 0

>srw1
g_nData = 0

>srw1
g_nData = 0

>srw1
g_nData = 0

リーダースレッドが同時に複数ロックを取得し、ライタースレッドは排他的であることの確認

#include <windows.h>
#include <process.h>
#include <stdio.h>


SRWLOCK g_SrwLock;

unsigned int __stdcall WThreadFunc(PVOID pv)  {

     printf( "Entering Writer ThreadFunc...\n" );

     AcquireSRWLockExclusive( &g_SrwLock );
     printf( "Acquired SRW (W) Lock - %u\n", GetCurrentThreadId() );
     Sleep( 5000 );
     ReleaseSRWLockExclusive( &g_SrwLock );

     printf( "Exit WThreadFunc\n" );
               
     return 0;
}



unsigned int __stdcall RThreadFunc(PVOID pv)  {

     printf( "Entering Reader ThreadFunc...\n" );

     AcquireSRWLockShared( &g_SrwLock );
     printf( "Acquired SRW (R) Lock - %u\n", GetCurrentThreadId() );          
     Sleep( 5000 );
     ReleaseSRWLockShared( &g_SrwLock );

     printf( "Exit RThreadFunc\n" );
               
     return 0;
}



int main( int argc, char* argv[] ) {


     DWORD dwThread[4];
     HANDLE hHandle[4];

     //
     // SRW ロックの初期化
     //


     InitializeSRWLock( &g_SrwLock );


     //
     // スレッドの作成
     //

     // 0 - リーダースレッド
     // 1 - リーダースレッド
     // 2 - ライタースレッド
     // 3 - リーダースレッド
     //


     for( int i=0; i<sizeof(hHandle)/sizeof(hHandle[0]); i++ ) {
          hHandle[i] = (HANDLE)_beginthreadex(
              NULL, 0, (i==2) ? WThreadFunc : RThreadFunc, 
              NULL, CREATE_SUSPENDED, (unsigned int*) &dwThread[i]);
     }     
     

     //
     // スレッドを走らせる
     //
     

     ResumeThread ( hHandle[0] );
     Sleep(500);

     
     ResumeThread ( hHandle[1] );
     Sleep(500);
     
     ResumeThread ( hHandle[2] );
     Sleep(500);
     
     ResumeThread ( hHandle[3] );
     

     //
     // スレッドの終了を待つ
     //
     

     WaitForMultipleObjects ( 
         sizeof(hHandle)/sizeof(hHandle[0]),
         hHandle,
         TRUE,
         INFINITE );
     

     //
     // クリーンアップ
     //
     
     for( int i=0; i<sizeof(hHandle)/sizeof(hHandle[0]); i++ ){
          CloseHandle ( hHandle[i] );
     }
          
     return 0;
}

実行結果は次の通り。

Entering Reader ThreadFunc...
Acquired SRW (R) Lock - 1016
Entering Reader ThreadFunc...
Acquired SRW (R) Lock - 4880
Entering Writer ThreadFunc...
Entering Reader ThreadFunc...
Exit RThreadFunc
Exit RThreadFunc
Acquired SRW (W) Lock - 4696
Exit WThreadFunc
Acquired SRW (R) Lock - 3784
Exit RThreadFunc

スレッドを四つ起動し、最初の二つをリーダースレッド。3番目をライタースレッド。4番目をリーダースレッドとしています。 すなわち、3番目は SRW ロックを取得する時に AcquireSRWLockExclusive を用い、それ以外は AcquireSRWLockShared を用いています。

この結果、リーダースレッドは同時に二つが SRW ロックを取得し、ライタースレッドはその解放を待つ。ライタースレッドがロックを取得中はリーダースレッドは入らない、ということが確認できました。

いつ SRW ロックを使うか?

まず、単一プロセス内でのデータの同期ということが前提です。そうでない場合は、SRW ロックはつかえません。また、再帰的にロックを取得要求する場合もサポートしていませんから、そうした場合も利用できません。 上記の例のように単純にデータを保護する場合にのみ使います。

それから、SRW ロックはクライアント OS では Vista 以降、サーバー OS では Windows 2008 以降でサポートされていますから、これ以外の環境では使えません。

リーダー・ライターの区別が可能な点で、共有データへの書き込みが稀で、たいていの場合読み込みのみの場合 SRW ロックの利用を検討します。

それ以外の場合はクリティカルセクションを利用すると良いでしょう。デバッグ時にもクリティカルセクションの方がロック情報が取得できるので、望ましいです。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説