オブジェクトプールパターン

オブジェクトプールパターンは、ソフトウェア作成の設計パターンです。このパターンでは、オブジェクトを必要に応じて割り当てたり破棄したりするのではなく、初期化された一連のオブジェクト(「プール」)を準備しておき、いつでも使用できるようにしておきます。プールのクライアントは、プールからオブジェクトを要求し、返されたオブジェクトに対して操作を実行します。クライアントは操作を終えると、オブジェクトを破棄するのではなく、プールに戻します。これは手動でも自動でも実行できます。

オブジェクトプールは主にパフォーマンス向上のために使用され、状況によってはパフォーマンスを大幅に向上させることもあります。オブジェクトプールは、プールから取得されたオブジェクトやプールに返されたオブジェクトが実際には作成または破棄されないため、オブジェクトの有効期間を複雑化させます。そのため、実装には注意が必要です。

説明

インスタンス化に特にコストがかかる多数のオブジェクトを扱う必要があり、各オブジェクトが短時間しか必要とされない場合、アプリケーション全体のパフォーマンスに悪影響を与える可能性があります。このような場合、オブジェクトプール設計パターンが望ましいと考えられます。

オブジェクトプール設計パターンは、再利用可能なオブジェクトセットを作成します。新しいオブジェクトが必要な場合は、プールから要求します。事前に準備されたオブジェクトが利用可能な場合は、インスタンス化のコストを回避し、すぐに返されます。プールにオブジェクトが存在しない場合は、新しいアイテムが作成され、返されます。オブジェクトが使用され、不要になった場合は、プールに戻されます。これにより、計算コストの高いインスタンス化プロセスを繰り返すことなく、将来的に再利用できるようになります。オブジェクトが使用され、返されると、既存の参照は無効になることに注意してください。

一部のオブジェクトプールではリソースが限られているため、オブジェクトの最大数が指定されています。この数に達した状態で新しいアイテムが要求されると、例外がスローされるか、オブジェクトがプールに戻されるまでスレッドがブロックされます。

オブジェクトプール設計パターンは、.NET Framework の標準クラスの複数の場所で使用されています。一例として、SQL Server 用の .NET Framework データプロバイダーが挙げられます。SQL Server データベース接続の作成には時間がかかることがあるため、接続プールが維持されます。接続を閉じても、SQL Server へのリンクは実際には解放されません。接続はプールに保持され、新しい接続を要求する際にそこから取得されます。これにより、接続の確立速度が大幅に向上します。

利点

クラスインスタンスの初期化コストが高く、クラスのインスタンス化と破棄の頻度が高い状況では、オブジェクトプーリングによってパフォーマンスが大幅に向上します。この場合、オブジェクトは頻繁に再利用できるため、再利用するたびに大幅な時間を節約できます。オブジェクトプーリングにはリソース(メモリ、場合によってはネットワークソケットなどのリソース)が必要になるため、一度に使用されるインスタンスの数は少ない方が望ましいですが、必須ではありません。

プールされたオブジェクトは、新しいオブジェクトの作成(特にネットワーク経由)に変動時間がかかる場合でも、予測可能な時間で取得されます。これらの利点は、データベース接続、ソケット接続、スレッド、フォントやビットマップなどの大きなグラフィックオブジェクトなど、時間のかかるオブジェクトに特に当てはまります。

他の状況では、単純なオブジェクトプーリング(外部リソースを保持せず、メモリを占有するだけ)は効率的ではなく、パフォーマンスが低下する可能性があります。[ 1 ]単純なメモリプーリングの場合、断片化を減らすことでメモリの割り当てと解放のコストを最小限に抑えることだけを目的とするため、スラブ割り当てメモリ管理手法の方が適しています。

実装

C++などの言語では、スマートポインタを介してオブジェクトプールを自動実装できます。スマートポインタのコンストラクタではプールからオブジェクトを要求し、スマートポインタのデストラクタではオブジェクトをプールに解放することができます。ガベージコレクション言語ではデストラクタ(スタックのアンワインド時に必ず呼び出される)がないため、オブジェクトプールは手動で実装する必要があります。ファクトリからオブジェクトを明示的に要求し、disposeメソッド( disposeパターン)を呼び出してオブジェクトを返す必要があります。ファイナライザを使用してこれを行うのは、ファイナライザがいつ実行されるか(または実行されるかどうか)が保証されないため、通常は推奨されません。代わりに、「try ...finally」を使用して、オブジェクトの取得と解放が例外中立であることを保証する必要があります。

手動オブジェクト プールは実装が簡単ですが、プール オブジェクトの メモリ管理を手動で行う必要があるため、使いにくくなります。

空プールの取り扱い

オブジェクト プールは、プール内に予備のオブジェクトがない場合に、要求を処理するために 3 つの戦略のいずれかを採用します。

  1. オブジェクトの提供に失敗しました (クライアントにエラーを返します)。
  2. 新しいオブジェクトを割り当て、プールのサイズを増やします。この処理を行うプールでは通常、最高使用率(使用されるオブジェクトの最大数)を設定できます。
  3. マルチスレッド環境では、別のスレッドがオブジェクトをプールに返すまで、プールがクライアントをブロックすることがあります。

落とし穴

プールに返されたオブジェクトの状態は、次回の使用時に適切な状態にリセットされるよう注意が必要です。そうしないと、オブジェクトがクライアントにとって予期しない状態になり、エラーが発生する可能性があります。オブジェクトのリセットはクライアントではなく、プールの責任です。危険なほど古い状態のオブジェクトでいっぱいのオブジェクトプールは、オブジェクトセスプールと呼ばれることもあり、アンチパターンと見なされます。

古い状態は必ずしも問題になるわけではありませんが、オブジェクトが予期せぬ動作をするようになると危険になります。例えば、認証情報を表すオブジェクトは、「認証成功」フラグが再利用される前にリセットされないと、実際には認証されていないユーザーが(おそらく別のユーザーとして)認証されていると表示されるため、失敗する可能性があります。しかし、最後に使用した認証サーバーのIDなど、デバッグのみに使用される値をリセットし忘れても、問題は発生しない可能性があります。

オブジェクトの不適切なリセットは情報漏洩につながる可能性があります。機密データ(例:ユーザーのクレジットカード番号)を含むオブジェクトは、新しいクライアントに渡す前に必ずクリアする必要があります。そうしないと、データが権限のない第三者に漏洩する可能性があります。

プールが複数のスレッドによって使用される場合、並列スレッドが同じオブジェクトを並列に再利用しようとするのを防ぐ手段が必要になる場合があります。プールされたオブジェクトが不変であるか、スレッドセーフである場合は、この手段は必要ありません。

批判

一部の出版物では、 Javaなどの特定の言語、特にメモリのみを使用し、外部リソース(データベースへの接続など)を保持しないオブジェクトでは、オブジェクトプーリングの使用を推奨していません。反対派は、ガベージコレクタを備えた現代の言語ではオブジェクトの割り当てが比較的高速であると主張します。演算子はnew10個の命令しか必要としませんが、プーリング設計に見られる古典的なnew-deleteペアはより複雑な処理を行うため、数百個の命令を必要とします。また、ほとんどのガベージコレクタは「ライブ」オブジェクト参照をスキャンし、これらのオブジェクトがコンテンツに使用するメモリはスキャンしません。つまり、参照のない「デッド」オブジェクトは、ほとんどコストをかけずに破棄できます。対照的に、「ライブ」だが使用されていないオブジェクトを大量に保持すると、ガベージコレクションの所要時間が長くなります。[ 1 ]

C++

C++26では、C++標準ライブラリ<hive>にデータ構造を持つ新しいヘッダーが導入されましたstd::hive。これは本質的にオブジェクトプールを実装するものです。これは、削除された要素のメモリを再利用するコレクションです。また、std::hive_limitsブロック容量制限のレイアウト情報を保持するクラスも存在します。[ 2 ]plf::colonyこれはライブラリのクラスに基づいていますplf[ 3 ]

stdをインポートしますstd :: hiveを使用します。std :: plus使用します。std :: ranges :: iota_view使用しますint main ( int argc char * argv []) { hive < int > intHive ;// 100個のintを挿入します: for ( int i : iota_view ( 0 , 100 )) { intHive . insert ( i ); }// 半分を消去します: for ( int i : intHive ) { intHive . erasing ( i ); }int total = std :: ranges :: fold_left ( intHive , 0 , plus < int > ()); std :: println ( "すべての要素の合計: {}" , total );0を返す; }

C#

.NET基本クラスライブラリには、このパターンを実装するオブジェクトがいくつかあります。System.Threading.ThreadPool割り当てるスレッドの数があらかじめ定義されています。スレッドが返されると、別の計算に使用できるようになります。そのため、スレッドの作成と破棄のコストを支払うことなく、スレッドを使用できます。

以下は、C# を使用して実装されたオブジェクトプール設計パターンの基本コードです。複数のプールが必要になることは稀であるため、プールは静的クラスとして示されています。ただし、オブジェクトプールにはインスタンスクラスを使用することも同様に可能です。

名前空間Wikipedia.Examples ;Systemを使用します。System.Collections.Generic使用します// PooledObject クラスは、インスタンス化にコストがかかったり、時間がかかったりするタイプ、または可用性が限られているタイプであるため、オブジェクト プールに保持されます。public class PooledObject { private DateTime _createdAt = DateTime . Now ;パブリックDateTime CreatedAt => _createdAt ;パブリック文字列TempData {取得;設定; } }// Poolクラスは、プールされたオブジェクトへのアクセスを制御します。利用可能なオブジェクトのリストと、プールから取得されて使用中のオブジェクトのコレクションを管理しますプールは、解放されたオブジェクトが適切な状態に戻り、再利用できるようにします。public static class Pool { private static List < PooledObject > _available = new (); private static List < PooledObject > _inUse = new ();public static PooledObject GetObject ( ) { lock ( _available ) { if ( _available.Count ! = 0 ) { PooledObject po = _available [ 0 ] ; _inUse.Add ( po ) ; _available.RemoveAt ( 0 ) ; return po ; } else { PooledObject po = new ( ) ; _inUse.Add ( po ) ; return po ; } } }パブリック静的void ReleaseObject ( PooledObject po ) { CleanUp ( po );lock ( _available ) { _available.Add ( po ) ; _inUse.Remove ( po ) ; } }プライベート静的void CleanUp ( PooledObject po ) { po . TempData = null ; } }

上記のコードでは、PooledObject は作成時のプロパティと、クライアントが変更可能な別のプロパティを持ちます。これらのプロパティは、PooledObject がプールに解放された際にリセットされます。これは、オブジェクトが解放された際に、プールから再度要求される前に有効な状態であることを確認するクリーンアッププロセスを示しています。

行く

次の Go コードは、チャネルを介したリソース競合の問題を回避するために指定されたサイズのリソース プールを初期化し (同時初期化)、プールが空の場合にはクライアントが長時間待機しないようにタイムアウト処理を設定します。

// パッケージプールパッケージプールインポート( "errors" "log" "math/rand" "sync" "time" )const getResMaxTime = 3 *時間.var ( ErrPoolNotExist = errors . New ( "プールが存在しません" ) ErrGetResTimeout = errors . New ( "リソースの取得がタイムアウトしました" ) )//リソースタイプリソース構造体{ resId int }// NewResource 遅いリソース初期化作成をシミュレートします// (例: TCP 接続、SSL 対称キーの取得、認証には時間がかかります) func NewResource ( id int ) * Resource { time . Sleep ( 500 * time . Millisecond ) return & Resource { resId : id } }// Do シミュレーションリソースは時間がかかり、ランダム消費は0~400msです。func ( r * Resource ) Do ( workId int ) { time . Sleep ( time . Duration ( rand . Intn ( 5 )) * 100 * time . Millisecond ) log . Printf ( "リソース #%d を使用して作業を完了しました %d が終了しました\n" , r . resId , workId ) }// リソース競合状態の問題を回避するために、Go チャネル実装に基づいたプールtype Pool chan * Resource// 指定されたサイズのリソースプールを新規作成します // リソースの初期化時間を節約するために、リソースは同時に作成されますfunc New ( size int ) Pool { p : = make ( Pool , size ) wg := new ( sync . WaitGroup ) wg . Add ( size ) for i := 0 ; i < size ; i ++ { go func ( resId int ) { p <- NewResource ( resId ) wg . Done () }( i ) } wg . Wait () return p }// チャネルに基づいてGetResourceを実行します。リソース競合状態は回避され、空のプールに対してリソース取得タイムアウトが設定されます。func ( p Pool ) GetResource () ( r * Resource , err error ) { select { case r := <- p : return r , nil case <- time . After ( getResMaxTime ): return nil , ErrGetResTimeout } }// GiveBackResource はリソースプールにリソースを返しますfunc ( p Pool ) GiveBackResource ( r * Resource ) error { if p == nil { return ErrPoolNotExist } p <- r return nil }// パッケージメインパッケージメインインポート( "github.com/tkstorm/go-design/creational/object-pool/pool" "log" "sync" )func main () { // 5つのリソースのプールを初期化します。// 違いを確認するために1または10に調整できます。size := 5 p := pool . New ( size )// id ジョブを実行するリソースを呼び出します。doWork := func ( workId int , wg * sync . WaitGroup ) { defer wg . Done () // リソース プールからリソースを取得します。res , err := p . GetResource () if err != nil { log . Println ( err ) return } // 返すリソースdefer p . GiveBackResource ( res ) // リソースを使用して作業を処理しますres . Do ( workId ) }// アセットプールからリソースを取得するために100の同時プロセスをシミュレートしますnum := 100 wg := new ( sync . WaitGroup ) wg . Add ( num ) for i := 0 ; i < num ; i ++ { go doWork ( i , wg ) } wg . Wait () }

ジャワ

Javaは、およびその他の関連クラスを介してスレッドプーリングjava.util.concurrent.ExecutorServiceをサポートしています。エグゼキュータサービスには、破棄されることのない一定数の「基本」スレッドがあります。すべてのスレッドがビジー状態の場合、サービスは許可された数の追加スレッドを割り当てます。これらの追加スレッドは、一定期間使用されなかった場合は破棄されます。スレッド数が不足した場合、タスクはキューに配置されます。最後に、このキューが長くなりすぎる可能性がある場合は、要求元のスレッドを一時停止するように設定できます。

PooledObject.javaの場合:

パッケージorg.wikipedia.examples ;パブリッククラスPooledObject {プライベートString temp1 ;プライベートString temp2 ;プライベートString temp3 ;パブリックString getTemp1 () {戻り値temp1 ; }パブリックvoid setTemp1 ( String temp1 ) { this . temp1 = temp1 ; }パブリック文字列getTemp2 () {戻り値temp2 ; }パブリックvoid setTemp2 ( String temp2 ) { this . temp2 = temp2 ; }パブリック文字列getTemp3 () {戻り値temp3 ; }パブリックvoid setTemp3 ( String temp3 ) { this . temp3 = temp3 ; } }

PooledObjectPool.javaの場合:

パッケージorg.wikipedia.examples ;java.util.HashMapをインポートします。java.util.Mapインポートしますpublic class PooledObjectPool { public static final long EXPIRY_TIME = 6000 ; // 6秒private static Map < PooledObject , Long > available = new HashMap <> (); private static Map < PooledObject , Long > inUse = new HashMap <> (); public synchronized static PooledObject getObject ( ) { long now = System.currentTimeMillis ( ); if ( ! available.isEmpty ( )) { for ( Map.Entry < PooledObject , Long > entry : available.entrySet ( )) { if ( now - entry.getValue ( ) > EXPIRY_TIME ) { //オブジェクトの有効期限が切れていますpopElement ( available ) ; } else { PooledObject po = popElement ( available , entry.getKey ( ) ) ; push ( inUse , po , now ) ; return po ; } } }// PooledObject が利用できないか、それぞれ期限が切れているため、新しいものを返します。return createPooledObject ( now ); } private synchronized static PooledObject createPooledObject ( long now ) { PooledObject po = new PooledObject (); push ( inUse , po , now ); return po ; }プライベート同期静的void push ( HashMap < PooledObject Long > map PooledObject po long now ) { map . put ( po now ); }public static void releaseObject ( PooledObject po ) { cleanUp ( po ) ; available.put ( po , System.currentTimeMillis ( )); inUse.remove ( po ) ; } private static PooledObject popElement ( HashMap < PooledObject , Long > map ) { Map.Entry < PooledObject , Long > entry = map.entrySet ( ) . iterator ( ) . next ( ) ; PooledObject key = entry.getKey ( ) ; // Long value = entry.getValue( ) ; map.remove ( entry.getKey ( ) ) ; return key ; } private static PooledObject popElement ( HashMap < PooledObject , Long > map , PooledObject key ) { map.remove ( key ) ; return key ; } public static void cleanUp ( PooledObject po ) { po.setTemp1 ( null ) ; po.setTemp2 ( null ) ; po.setTemp3 ( null ) ; } }

さび

C++の(または)をベースにしたcolonyと呼ばれるRustクレートがあります。 [ 4 ] 2024年1月以降メンテナンスされなくなりました。 std::hiveplf::colony

colony :: Colonyを使用しますfn main () { let mut colony : Colony = Colony :: new ();// 挿入let foo_handle = colony . insert ( "foo" ); let bar_handle = colony . insert ( "bar" );// 削除assert_eq! ( colony . remove ( foo_handle ), Some ( "foo" ));// 検索assert_eq! ( colony . get ( foo_handle ), None ); assert_eq! ( colony . get ( bar_handle ), Some ( & "bar" ));// colony( key & value )の反復処理。iter () { assert_eq ! (( key value )、( bar_handle "bar" )); } }

参照

注記

  1. ^ a b Goetz, Brian (2005年9月27日). 「Javaの理論と実践:パフォーマンスに関する都市伝説の再考」 . IBM . IBM developerWorks. 2012年2月14日時点のオリジナルよりアーカイブ。 2021年3月15日閲覧
  2. ^ 「標準ライブラリヘッダー <hive> (C++26) - cppreference.com」 . cppreference.com . 2025年10月1日閲覧
  3. ^ Matthew Bentley (2025年12月1日). 「PLFライブラリ - コロニー」 . plflib.org . plflib.
  4. ^ LlewVallis (2023年8月4日). 「クレートコロニー」 . docs.rs. docs.rs.

参考文献