2015-12-30 5 views
6

.NET 4.0のSystem.Lazy<T>クラスは、私のように要約します列挙LazyThreadSafetyMode、を介して3スレッドセーフモードを提供しています:System.Lazy <T>は異なるスレッドセーフモードと

  • LazyThreadSafetyMode.None - スレッドセーフではありません。
  • LazyThreadSafetyMode.ExecutionAndPublication-1つの並行スレッドだけが、基礎となる値の作成を試みます。作成に成功すると、待機中のスレッドはすべて同じ値を受け取ります。未処理の例外が作成中に発生した場合は、待機中の各スレッドで再スローされ、キャッシュされ、後続の各値のアクセスに再スローされます。
  • LazyThreadSafetyMode.PublicationOnlyから複数の同時実行スレッドは、基礎となる価値を創造しようとしますが、成功するために最初は、すべてのスレッドに渡された値を決定します。作成中に未処理の例外が発生した場合、そのオブジェクトはキャッシュされません。同時に&の基本値にアクセスしようとすると、作成が再試行されます。&が成功する可能性があります。

私はすなわち、わずかに異なるスレッドの安全規則を次のレイジー初期化された値を持っているしたいと思います:

つだけの同時スレッドが根底にある価値を創造しようとします。作成に成功すると、待機中のスレッドはすべて同じ値を受け取ります。未処理の例外が作成中に発生すると、待機中の各スレッドで再スローされますが、キャッシュされず、その後の値にアクセスしようとすると、作成が再試行されます。&が成功する可能性があります。

のでLazyThreadSafetyMode.ExecutionAndPublicationとキーdifferinceは、作成時に「最初に行くには、」失敗した場合、それは後で再試行することができるということです。

これらのセマンティクスを提供する既存の(.NET 4.0)クラスはありますか、それとも独自にロールバックする必要がありますか?私が自分自身をロールバックする場合、明示的なロック/同期を避けるために、既存のLazy <T>を実装内で再利用するスマートな方法がありますか?


N.B.ユースケースの場合、「作成」は高価で潜在的に断続的なエラーが発生する可能性があると想像してください。リモートサーバーから大量のデータを取得します。私はおそらくすべてが失敗するか、すべて成功するので、データを取得する複数の同時の試みをしたくないです。しかし、失敗した場合は、後で再試行することができます。

+1

をひどく曖昧である "に後で再試行します"。おそらくLazyインスタンスを再作成するTimerを使用することができます。 –

+0

こんにちは@HansPassant。私はLazy 自体が後で再試行するべきではありません。つまり、ユーザーがmyLazy.valueを複数回呼び出すと、最初に失敗した場合、2回目の呼び出しで、以前の例外を単純に再実行するのではなく、基礎となる値をインスタンス化するように再試行します。 – MattBecker82

+0

「後で」はいつですか?メインスレッドがその例外をスローし、待機中のすべてのスレッドがそれを観察することによってブロック解除された後はいつですか?あなたの仮説的な 'LazyWithSprinkles 'を監視している/呼び出している呼び出し元が管理していますか?あなたが投稿したものより少し大きい問題があるように思えます。これは 'Lazy 'に似ているものとはかなり異なる解決策を示唆しています。 –

答えて

0

競合状態I pointed out ...のないバージョンのDarin's updated answerで私の試行を警告しましたが、これは最終的には完全に競合状態がないことは完全にはわかりません。

private static int waiters = 0; 
private static volatile Lazy<object> lazy = new Lazy<object>(GetValueFromSomewhere); 
public static object Value 
{ 
    get 
    { 
     Lazy<object> currLazy = lazy; 
     if (currLazy.IsValueCreated) 
      return currLazy.Value; 

     Interlocked.Increment(ref waiters); 

     try 
     { 
      return lazy.Value; 

      // just leave "waiters" at whatever it is... no harm in it. 
     } 
     catch 
     { 
      if (Interlocked.Decrement(ref waiters) == 0) 
       lazy = new Lazy<object>(GetValueFromSomewhere); 
      throw; 
     } 
    } 
} 

アップデート:私は、私はこれを掲示した後、競合状態を発見したと思いました。この動作は実際には受け入れられるはずです。ある種のスレッドが例外をスローする場合は、おそらくまれです。Lazy<T>他のスレッドが既に成功したファストLazy<T>から戻ってきた後に(将来の要求はすべて成功します)。

  • waiters = 0
  • T1:ちょうどInterlocked.Decrement(= 1 waiters
  • t2の前まで走行に来る:に来て、ちょうどInterlocked.Increment(= 1 waiters
  • 前までに実行され
  • T1:そのInterlocked.Decrementと(= 0 waiters)を上書きする準備
  • T2を行いますちょうどInterlocked.Decrement(前まで走ります= 1)
  • T1:lazy T3
  • 新しいもの(lazy1それを呼び出す)(= 1 waiters
  • とを上書き:T2
  • )= 2 waiterslazy1上及びブロック付属:んそのInterlocked.Decrement( = 1 waiters
  • T3:lazy1から値を取得し、返す(waitersは現在無関係である)
  • T2:その例外を再スロー

「このスレッドは、別のスレッドが成功した結果、このスレッドが例外をスローした」よりも悪いことが起こる一連のイベントが発生しません。

Update2:保護された上書きがすべての読者によって直ちに確認されるように、lazyvolatileと宣言しました。一部の人々(自分自身を含む)はvolatileを参照し、すぐに「よく、おそらく間違って使用されている」と思っています。上記の例のイベントシーケンスでは、lazylazy1に変更した直後に、lazy1の代わりにt3がの代わりにlazy.Valueの直前に配置されていた場合、t3はまだt1がlazyに変更された瞬間を読み取ることができました。 volatileはそのことから保護し、次の試行をすぐに開始できるようにします。

なぜ私は頭の後ろに「ローロック・コンカレント・プログラミングは難しいのですが、C#lockステートメントを使用しています」ということを思い出しました。私が元の答えを書いていた時間全体。

Update3と:ちょうどvolatileが必要になり、実際の状況を指摘アップデート2でいくつかのテキストを変更する - 私はもともと持っていたとして、ここで使用さInterlocked操作は、明らかに、今日ではなく、ハーフフェンスの重要なCPUアーキテクチャにフルフェンスを実施しています単純に並べ替えられているので、私が元々考えていたよりもはるかに狭い部分を保護します。volatileこのような

+0

エレガントなソリューションのように見えますが、私はあなたが説明している競合状態に気にしません。しかし、本当に除外したいのであれば、 'static object syncRoot = new object();'を追加し、 'Interlocked.Increment(ref _waiters);を' lock(syncRoot){++ waiters ;} 'また、' if'文を 'lock(syncRoot){if( - waiters == 0)_lazy = new Lazy (GetValueFromSomewhere);}に変更します。 } '? – MattBecker82

+0

@ MattBecker82:私はそれが役に立たないと思います。 (Monitor.Enter'は完全なメモリバリアを行うので、これはよく分かりません)、フィールド宣言から 'volatile 'を削除することができます。合理的な方法でこの状態を回避する方法を考え出すことはできません。現在実行中の 'catch'ハンドラがあれば、それを待つように強制します(' waiters'はすべて'!currLazy.IsValueCreated'を読んだ直後に、' lazy'を再度読み込みます。それは可能ですが、疑問のために複雑さが増します。 –

+0

これを正確に記しておきますが、これは最も優雅だと感じています。既知の競争条件についての注意点は十分明確です。小道具も[ダーリンの](http://stackoverflow.com/a/34530839/15498)と[ダミアンの](http://stackoverflow.com/a/34531320)の回答。皆さんありがとう。 – MattBecker82

1

レイジーはこれをサポートしていません。これはLazyの設計上の問題です。例外の「キャッシング」とは、遅延インスタンスが実際の価値を永遠に提供しないためです。これにより、ネットワークの問題などの過渡的なエラーのためにアプリケーションが永久にダウンすることがあります。通常、人間の介入が必要です。

私はあなたがこれを行うには、あなた自身の怠惰を記述する必要が

...この地雷はかなりの数の.NETアプリケーションに存在して賭けます。または、CoreFx Githubの問題を開いてください。

+0

なぜこれが 'Lazy 'の設計上の問題だと思いますか? 'Lazy 'の役割が値ファクトリを約1回実行し、その値が要求されるたびに結果をキャッシュするようになっていると仮定した場合、与えた場合には未処理の例外を投げるそれは回復可能な例外を処理しないバリューファクトリーですか? –

+0

ありがとう、usr。私は 'Lazy 'がこれをサポートしていないことを認識していますので、私の質問は実際にはこの動作をサポートする(または容易に適応できる)既存のメカニズムがあるかどうかということでした。申し訳ありませんが、これが明確でない場合。 – MattBecker82

+0

答えはいいえ、私はそれを呼び出す必要があります。 @ MattBecker82それを作りません。残念なこと。 – usr

3

の値を作成しようとするスレッドは1つの同時スレッドだけです。作成に成功すると、待機中のすべてのスレッドは同じ値の を受信します。作成中に未処理の例外が発生すると、待機中の各スレッドで が再スローされますが、キャッシュされず、 以降の値にアクセスしようとすると、 の作成が再試行されます。&が成功する可能性があります。レイジーはそれをサポートしていないので、自分でそれをロールバックしようとすることができ

private static object syncRoot = new object(); 
private static object value = null; 
public static object Value 
{ 
    get 
    { 
     if (value == null) 
     { 
      lock (syncRoot) 
      { 
       if (value == null) 
       { 
        // Only one concurrent thread will attempt to create the underlying value. 
        // And if `GetTheValueFromSomewhere` throws an exception, then the value field 
        // will not be assigned to anything and later access 
        // to the Value property will retry. As far as the exception 
        // is concerned it will obviously be propagated 
        // to the consumer of the Value getter 
        value = GetTheValueFromSomewhere(); 
       } 
      } 
     } 
     return value; 
    } 
} 

UPDATE:

伝播同じ例外についてのあなたの要求を満たすためにすべての待機中のリーダースレッド:

private static Lazy<object> lazy = new Lazy<object>(GetTheValueFromSomewhere); 
public static object Value 
{ 
    get 
    { 
     try 
     { 
      return lazy.Value; 
     } 
     catch 
     { 
      // We recreate the lazy field so that subsequent readers 
      // don't just get a cached exception but rather attempt 
      // to call the GetTheValueFromSomewhere() expensive method 
      // in order to calculate the value again 
      lazy = new Lazy<object>(GetTheValueFromSomewhere); 

      // Re-throw the exception so that all blocked reader threads 
      // will get this exact same exception thrown. 
      throw; 
     } 
    } 
} 
+0

"待機中のスレッドごとに再スローされます"というビットは欠けていますが、とにかく実際の*要件ではないと思います。 –

+0

実際には、2つのスレッドが(GetTheValueFromSomewhereが例外をスローすると仮定して)同時に割り当てられている値を読み取ろうとしている場合、両方のスレッドは2つの異なる例外を生成します最終的に 'GetTheValueFromSomewhere'メソッドを呼び出します。したがって、基本的に、この例の2番目の読み込みスレッドは、値の再作成を再試行します。 –

+0

Joe - 本当の必要条件です。値を呼び出す複数の同時スレッドがあり、ロックを取得する最初のスレッドが例外をスローする 'GetTheValueFromSomewhere()'を呼び出す場合、他の待機スレッドをすべて移動させずに 'GetTheValueFromSomewhere()'を呼び出す必要はありません潜在的に高価です。 – MattBecker82

1

パーtially Darin's answerに触発さが、この「例外を負わせている待機中のスレッドのキュー」を取得して、「もう一度やり直してください」しようとする作業が備わっています:

private static Task<object> _fetcher = null; 
private static object _value = null; 

public static object Value 
{ 
    get 
    { 
     if (_value != null) return _value; 
     //We're "locking" then 
     var tcs = new TaskCompletionSource<object>(); 
     var tsk = Interlocked.CompareExchange(ref _fetcher, tcs.Task, null); 
     if (tsk == null) //We won the race to set up the task 
     { 
      try 
      { 
       var result = new object(); //Whatever the real, expensive operation is 
       tcs.SetResult(result); 
       _value = result; 
       return result; 
      } 
      catch (Exception ex) 
      { 
       Interlocked.Exchange(ref _fetcher, null); //We failed. Let someone else try again in the future 
       tcs.SetException(ex); 
       throw; 
      } 
     } 
     tsk.Wait(); //Someone else is doing the work 
     return tsk.Result; 
    } 
} 

私も少し心配です - 誰もが任意の明白なレースを見ることができますここでそれは明らかでない方法で失敗するでしょうか?

+0

これを行う原因となる操作のシーケンスを思いつくことはできません。値が準備できていなければ、 'tsk.Result'はそれ自身の待機を行い、明示的な' tsk.Wait() 'は冗長です。 –

1

何かが役立つかもしれない:

using System; 
using System.Threading; 

namespace ADifferentLazy 
{ 
    /// <summary> 
    /// Basically the same as Lazy with LazyThreadSafetyMode of ExecutionAndPublication, BUT exceptions are not cached 
    /// </summary> 
    public class LazyWithNoExceptionCaching<T> 
    { 
     private Func<T> valueFactory; 
     private T value = default(T); 
     private readonly object lockObject = new object(); 
     private bool initialized = false; 
     private static readonly Func<T> ALREADY_INVOKED_SENTINEL =() => default(T); 

     public LazyWithNoExceptionCaching(Func<T> valueFactory) 
     { 
      this.valueFactory = valueFactory; 
     } 

     public bool IsValueCreated 
     { 
      get { return initialized; } 
     } 

     public T Value 
     { 
      get 
      { 
       //Mimic LazyInitializer.EnsureInitialized()'s double-checked locking, whilst allowing control flow to clear valueFactory on successful initialisation 
       if (Volatile.Read(ref initialized)) 
        return value; 

       lock (lockObject) 
       { 
        if (Volatile.Read(ref initialized)) 
         return value; 

        value = valueFactory(); 
        Volatile.Write(ref initialized, true); 
       } 
       valueFactory = ALREADY_INVOKED_SENTINEL; 
       return value; 
      } 
     } 
    } 
} 
関連する問題