5

オブジェクトが完全に構築される前に、Delphi はインスタンス変数を割り当てますか?

つまり、変数が与えられた場合:

var
   customer: TCustomer = nil; 

次に、顧客を作成して変数に割り当てます。

customer := TCustomer.Create;

ではなく、完全に構築された を指していcustomerない可能性はありますか?nilTCustomer


これは、遅延初期化を実行するときに問題になります。

function SacrifialCustomer: TCustomer;
begin
   if (customer = nil) then
   begin
      criticalSection.Enter;
      try
         customer := TCustomer.Create;
      finally 
         criticalSection.Leave;
      end;
   end;
   Result := customer;
end;

バグは次の行にあります。

if (customer = nil) 

別のスレッドが次を呼び出す可能性があります。

customer := TCustomer.Create;

変数には、構築が行われる前に値が割り当てられます。これにより、変数が割り当てられているという理由だけで、スレッドはそれが有効なオブジェクトであると想定します。customer

このマルチスレッド シングルトンのバグは、Delphi (5) で発生する可能性がありますか?


ボーナス質問

Delphiの受け入れられた、スレッドセーフな、1 回限りの初期化設計パターンはありますか? 多くの人が、およびをオーバーライドして Delphi にシングルトンを実装しています。それらの実装は複数のスレッドで失敗します。NewInstanceFreeInstance

厳密に言えば、実装方法とsingletonについての回答は求めていませんが、lazy-initializationです。シングルトンは遅延初期化を使用できますが、遅延初期化はシングルトンに限定されません。

アップデート

よくある間違いを含む回答を 2 人が提案しました。Delphi に翻訳され壊れたダブルチェック ロック アルゴリズム:

// Broken multithreaded version
// "Double-Checked Locking" idiom
if (customer = nil) then
begin
   criticalSection.Enter;
   try
      if (customer = nil) then
         customer := TCustomer.Create;
   finally
      criticalSection.Leave;
   end;
end;
Result := customer;

ウィキペディアから:

直感的に、このアルゴリズムは問題に対する効率的な解決策のように思えます。ただし、この手法には多くの微妙な問題があるため、通常は避ける必要があります。


別のバグのある提案:

function SacrificialCustomer: TCustomer;
var
  tempCustomer: TCustomer;
begin
   tempCustomer = customer;
   if (tempCustomer = nil) then
   begin
      criticalSection.Enter;
      try
         if (customer = nil) then
         begin
            tempCustomer := TCustomer.Create;
            customer := tempCustomer;
         end;
      finally
         criticalSection.Leave;
      end;
   end;
   Result := customer;
end;

アップデート

私はいくつかのコードを作成し、CPU ウィンドウを見ました。このコンパイラは、私の最適化設定を使用して、このバージョンの Windows でこのオブジェクトを使用して、最初にオブジェクトを構築し、次に変数を割り当てているようです。

customer := TCustomer.Create;
       mov dl,$01
       mov eax,[$0059d704]
       call TCustomer.Create
       mov [customer],eax;
Result := customer;
       mov eax,[customer];

もちろん、それが常にそのように機能することが保証されているとは言えません。

4

4 に答える 4

8

あなたの質問を読んだのは、あなたがこれを求めているということです:

x86 ハードウェアを対象とする Delphi 5 を使用して、シングルトンのスレッドセーフな遅延初期化を実装するにはどうすればよいですか。

私の知る限りでは、3 つの選択肢があります。

1. ロックを使用する

function GetCustomer: TCustomer;
begin
  Lock.Acquire;
  try
    if not Assigned(Customer) then // Customer is a global variable
      Customer := TCustomer.Create;
    Result := Customer;
  finally 
    Lock.Release;
  end;
end;

これの欠点は、競合がある場合GetCustomer、ロックのシリアル化によってスケーリングが阻害されることです。人々はそれを必要以上に心配していると思います。たとえば、多くの作業を実行するスレッドがある場合、そのスレッドはシングルトンへの参照のローカル コピーを取得して、競合を減らすことができます。

procedure ThreadProc;
var
  MyCustomer: TCustomer;
begin
  MyCustomer := GetCustomer;
  // do lots of work with MyCustomer
end;

2.ダブルチェックロック

これは、シングルトンが作成されると、ロックの競合を回避できる手法です。

function GetCustomer: TCustomer;
begin
  if Assigned(Customer) then
  begin
    Result := Customer;
    exit;
  end;

  Lock.Acquire;
  try
    if not Assigned(Customer) then
      Customer := TCustomer.Create;
    Result := Customer;
  finally 
    Lock.Release;
  end;
end;

ダブルチェックロックは、かなり複雑な歴史を持つテクニックです。最も有名な議論は、「ダブルチェック ロックが壊れている」宣言です。これは主に Java のコンテキストで設定されており、説明されている問題はあなたの状況には当てはまりません (Delphi コンパイラ、x86 ハードウェア)。実際、Java の場合、JDK5 の登場により、ダブルチェック ロックが修正されたと言えるようになりました。

Delphi コンパイラは、オブジェクトの構築に関して、singleton 変数への書き込みの順序を変更しません。さらに、強力な x86 メモリ モデルは、プロセッサの並べ替えによってこれが壊れないことを意味します。x86 でメモリ フェンスを注文したのは誰ですか? を参照してください。

簡単に言うと、Delphi x86 ではダブル チェック ロックが壊れていません。さらに、x64 メモリ モデルも強力で、ダブル チェック ロックも壊れていません。

3.比較して交換する

シングルトン クラスの複数のインスタンスを作成し、1 つを除いてすべて破棄する可能性を気にしない場合は、比較と交換を使用できます。VCL の最近のバージョンは、この手法を利用しています。次のようになります。

function GetCustomer;
var
  LCustomer: TCustomer;
begin
  if not Assigned(Customer) then 
  begin
    LCustomer := TCustomer.Create;
    if InterlockedCompareExchangePointer(Pointer(Customer), LCustomer, nil) <> nil then
      LCustomer.Free;
  end;
  Result := Customer;
end;
于 2012-05-30T08:27:26.840 に答える
6

施工後に譲渡しても同じ問題があります。2 つのスレッドがほぼ同時に SacrifialCustomer にヒットした場合、どちらif (customer = nil)かがクリティカル セクションに入る前に両方がテストを実行できます。

この問題に対する 1 つの解決策は、ダブル チェック ロックです (クリティカル セクションに入った後に再度テストします)。Delphi では、これは一部のプラットフォームで機能しますが、すべてのプラットフォームで機能するとは限りません。他のソリューションは静的構築を使用します。これは多くの言語で機能します (Delphi についてはわかりません)。静的初期化はクラスが参照されたときにのみ発生するため、事実上遅延であり、静的初期化子は本質的にスレッド セーフです。もう 1 つは、テストと代入をアトミック操作に結合するインターロック交換を使用することです (Delphi の例については、ここで 2 番目の回答を参照してください: How should "Double-Checked Locking" be implement in Delphi? )。

于 2012-05-29T20:12:56.573 に答える
5

いいえ、Delphi はコンストラクタが戻る前にターゲット変数に値を代入しません。Delphi のライブラリの多くは、この事実に依存しています。(オブジェクトのフィールドは nil に初期化されます。オブジェクトのコンストラクターで未処理の例外がデストラクタをトリガーしFree、コンストラクターが割り当てていたすべてのオブジェクト フィールドを呼び出すことが期待されます。これらのフィールドに nil 以外の値がある場合、さらに例外が発生します。 )

おまけの質問はメインの質問とは無関係であり、後から考えるよりもはるかに大きなトピックであるため、取り上げないことにしました。

于 2012-05-29T22:49:30.240 に答える
1

問題を解決する別の解決策は、customer複数のオブジェクトの作成を防ぐ原子ロック変数としてポインターを使用することです。あなたについての詳細は、 Busy-Wait Initializationで読むことができます。 また読む:楽観的および悲観的初期化について

于 2012-05-30T08:41:26.283 に答える