オブジェクトプールパターンは、ソフトウェアの生成設計パターンの一つで、必要に応じてオブジェクトを割り当てたり破棄したりするのではなく、初期化済みのオブジェクトセット(「プール」)をすぐに使用できる状態で保持します。プールのクライアントは、プールからオブジェクトを要求し、返されたオブジェクトに対して操作を実行します。クライアントは処理が完了すると、オブジェクトを破棄するのではなく、プールに戻します。この処理は手動でも自動でも行うことができます。
オブジェクトプールは主にパフォーマンス向上を目的として使用されます。状況によっては、オブジェクトプールによってパフォーマンスが大幅に向上することがあります。オブジェクトプールはオブジェクトのライフサイクルを複雑にします。プールから取得され、プールに戻されるオブジェクトは、実際にはこの時点では作成も破棄もされないため、実装には注意が必要です。
インスタンス化に特にコストのかかる多数のオブジェクトを扱う必要があり、かつ各オブジェクトが短期間しか必要とされない場合、アプリケーション全体のパフォーマンスに悪影響を及ぼす可能性があります。このようなケースでは、オブジェクトプール設計パターンが望ましいと考えられます。
オブジェクトプール設計パターンは、再利用可能なオブジェクトのセットを作成します。新しいオブジェクトが必要になったときは、プールから要求します。事前に準備されたオブジェクトが利用可能な場合は、インスタンス化のコストを回避して、すぐに返されます。プールにオブジェクトが存在しない場合は、新しいオブジェクトが作成されて返されます。オブジェクトが使用され、不要になった場合は、プールに戻されます。これにより、計算コストの高いインスタンス化プロセスを繰り返すことなく、将来再び使用できるようになります。オブジェクトが使用されて返されると、既存の参照は無効になります。
オブジェクトプールによってはリソースに制限があるため、オブジェクトの最大数が指定されています。この数に達した状態で新しいアイテムが要求されると、例外が発生するか、オブジェクトがプールに解放されるまでスレッドがブロックされます。
オブジェクトプール設計パターンは、.NET Framework の標準クラスのいくつかの箇所で使用されています。その一例が、.NET Framework Data Provider for SQL Server です。SQL Server データベースへの接続は作成に時間がかかる場合があるため、接続プールが維持されます。接続を閉じても、実際には SQL Server へのリンクは解放されません。代わりに、接続はプールに保持され、新しい接続を要求する際にそこから取得されます。これにより、接続の確立速度が大幅に向上します。
オブジェクトプーリングは、クラスインスタンスの初期化コストが高く、クラスのインスタンス化と破棄の頻度が高い状況において、パフォーマンスを大幅に向上させることができます。このような場合、オブジェクトは頻繁に再利用され、再利用されるたびに大幅な時間短縮につながります。オブジェクトプーリングには、メモリやネットワークソケットなどのリソースが必要となるため、同時に使用されるインスタンス数は少ない方が望ましいですが、必須ではありません。
プールされたオブジェクトは予測可能な時間で取得できますが、新しいオブジェクトの作成(特にネットワーク経由の場合)には時間がかかる場合があります。これらの利点は、データベース接続、ソケット接続、スレッド、フォントやビットマップなどの大きなグラフィックオブジェクトなど、時間的にコストのかかるオブジェクトに特に当てはまります。
その他の状況では、単純なオブジェクトプーリング(外部リソースを保持せず、メモリのみを占有するもの)は効率的ではなく、パフォーマンスが低下する可能性があります。[ 1 ]単純なメモリプーリングの場合、スラブ割り当てメモリ管理技術の方が適しています。これは、断片化を減らすことでメモリの割り当てと解放のコストを最小限に抑えることが唯一の目的だからです。
オブジェクトプールは、C++ のような言語ではスマートポインタを使用して自動的に実装できます。スマートポインタのコンストラクタでは、プールからオブジェクトを要求でき、スマートポインタのデストラクタでは、オブジェクトをプールに解放できます。ガベージコレクションのある言語では、デストラクタ(スタックのアンワインドの一部として必ず呼び出される)がないため、オブジェクトプールは手動で実装する必要があります。具体的には、ファクトリからオブジェクトを明示的に要求し、dispose メソッドを呼び出してオブジェクトを返してください(dispose パターンと同様)。ファイナライザを使用してこれを行うのは、ファイナライザがいつ実行されるか(または実行されるかどうか)が保証されないため、お勧めできません。代わりに、「try ... finally」を使用して、オブジェクトの取得と解放が例外に依存しないようにする必要があります。
手動オブジェクトプールは実装は簡単ですが、プールオブジェクトのメモリ管理を手動で行う必要があるため、使用はより困難です。
オブジェクトプールは、プール内に予備のオブジェクトがない場合にリクエストを処理するために、次の3つの戦略のいずれかを採用します。
プールに戻されたオブジェクトの状態が、次回の使用に適した状態にリセットされるように注意する必要があります。そうしないと、オブジェクトがクライアントにとって予期しない状態になり、エラーが発生する可能性があります。オブジェクトのリセットはプールの責任であり、クライアントの責任ではありません。危険なほど古い状態のオブジェクトで満たされたオブジェクトプールは、オブジェクト汚水溜めと呼ばれることもあり、アンチパターンとみなされます。
古い状態が常に問題になるわけではありませんが、オブジェクトが予期しない動作をする場合、危険になります。たとえば、認証の詳細を表すオブジェクトは、「認証成功」フラグが再利用前にリセットされていない場合、ユーザーが認証されていないにもかかわらず(おそらく別人として)認証されていると表示されるため、失敗する可能性があります。ただし、最後に使用した認証サーバーのIDなど、デバッグのみに使用される値をリセットし忘れても、問題にならない場合があります。
オブジェクトのリセットが不十分だと、情報漏洩の原因となる可能性があります。機密データ(ユーザーのクレジットカード番号など)を含むオブジェクトは、新しいクライアントに渡す前に必ずクリアする必要があります。そうしないと、データが不正な第三者に漏洩する可能性があります。
プールが複数のスレッドで使用される場合、並列スレッドが同じオブジェクトを並列に再利用しようとするのを防ぐ手段が必要になる場合があります。プールされたオブジェクトが不変であるか、またはスレッドセーフである場合は、この手段は不要です。
一部の出版物では、 Javaなどの特定の言語でオブジェクト プーリングを使用することを推奨していません。特に、メモリのみを使用し、外部リソース (データベースへの接続など) を保持しないオブジェクトの場合です。反対派は通常、ガベージ コレクタを備えた最新の言語ではオブジェクトの割り当てが比較的速いと主張します。演算子はnew10 個の命令しか必要としませんが、プーリング設計で見られる古典的newなdeleteペアは、より複雑な処理を行うため、数百個の命令を必要とします。また、ほとんどのガベージ コレクタは、オブジェクトがコンテンツに使用するメモリではなく、「生きている」オブジェクト参照をスキャンします。つまり、参照のない「死んだ」オブジェクトは、ほとんどコストをかけずに破棄できます。対照的に、使用されていない「生きている」オブジェクトを多数保持すると、ガベージ コレクションの時間が長くなります。[ 1 ]
C++26では、C++ 標準ライブラリ<hive>に、基本的にオブジェクトプールを実装するデータ構造を持つ新しいヘッダーが導入されました。これは、消去された要素のメモリを再利用するコレクションです。これに加えて、ブロック容量制限のレイアウト情報のためのstd::hiveクラスがあります。 [ 2 ]は、ライブラリのクラスに基づいています。[ 3 ]std::hive_limitsstd::hiveplf::colonyplf
import std ;using std :: hive ; using std :: string ;int main () { hive < string > stringHive ;// stringHive.insert ( "foo" ) ; stringHive.insert ( " bar " ) ;// std :: erase ( stringHive , string ( "foo" )) を削除します。// 反復処理for ( const auto & value : stringHive ) { std :: println ( "{}" , value ); }return 0 ; }.NET基本クラスライブラリには、このパターンを実装するオブジェクトがいくつかあります。System.Threading.ThreadPoolは、割り当てるスレッド数が事前に定義されているように構成されています。スレッドが返されると、別の計算に使用できるようになります。したがって、スレッドの作成と破棄にかかるコストを支払うことなく、スレッドを使用できます。
以下は、C# を使用して実装されたオブジェクトプール設計パターンの基本コードです。複数のプールが必要となるケースはまれであるため、`pool` は静的クラスとして示されています。ただし、オブジェクトプールにインスタンスクラスを使用することも同様に問題ありません。
名前空間Wikipedia.Examples ;using System ; using System.Collections.Generic ;// PooledObject クラスは、インスタンス化にコストがかかるか、インスタンス化に時間がかかる、または利用可能性が限られている型であり、オブジェクト プールに保持されます。public class PooledObject { private DateTime _createdAt = DateTime . Now ;public DateTime CreatedAt => _createdAt ;public string TempData { get ; set ; } }// 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 ; } } }public static void ReleaseObject ( PooledObject po ) { CleanUp ( po );lock ( _available ) { _available . Add ( po ); _inUse . Remove ( po ); } }private static void CleanUp ( PooledObject po ) { po.TempData = null ; } }上記のコードでは、PooledObjectには作成時のプロパティと、クライアントが変更可能な別のプロパティがあり、後者はPooledObjectがプールに解放される際にリセットされます。オブジェクト解放時のクリーンアップ処理が示されており、オブジェクトがプールから再度要求される前に有効な状態になっていることが保証されます。
以下のGoコードは、チャネルを介したリソース競合の問題を回避するために、指定されたサイズのリソースプールを初期化します(同時初期化)。また、プールが空の場合には、クライアントが長時間待機することを防ぐためにタイムアウト処理を設定します。
// パッケージプールパッケージプールimport ( "errors" "log" "math/rand" "sync" "time" )const getResMaxTime = 3 * time.Secondvar ( ErrPoolNotExist = errors.New ( "プールが存在しません" ) ErrGetResTimeout = errors.New ( "リソースの取得がタイムアウトしました" ) )//リソースタイプResource struct { resId int }// NewResource 低速なリソース初期化作成をシミュレートします// (例: TCP接続、SSL対称鍵の取得、認証は時間がかかります) func NewResource ( id int ) * Resource { time . Sleep ( 500 * time . Millisecond ) return & Resource { resId : id } }// シミュレーションリソースは時間がかかり、ランダムな消費は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 }// パッケージ mainパッケージmainimport ( "github.com/tkstorm/go-design/creational/object-pool/pool" "log" "sync" )func main () { // 5 つのリソースのプールを初期化します。// 違いを確認するために、1 または 10 に調整できます。size := 5 p := pool . New ( size )// 指定されたジョブを実行するリソースを呼び出す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 ;public class PooledObject { private String temp1 ; private String temp2 ; private String temp3 ; public String getTemp1 () { return temp1 ; }public void setTemp1 ( String temp1 ) { this.temp1 = temp1 ; }public String getTemp2 () { return temp2 ; }public void setTemp2 ( String temp2 ) { this.temp2 = temp2 ; }public String getTemp3 () { return temp3 ; }public void setTemp3 ( String temp3 ) { this.temp3 = temp3 ; } }PooledObjectPool.java内:
パッケージorg.wikipedia.examples ;import java.util.HashMap ; import 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 ; }private synchronized static 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
use 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" ));// 反復処理for ( key , & value ) in colony . iter () { assert_eq! (( key , value ), ( bar_handle , "bar" )); } }