7

私は流暢なインターフェイス スタイルを公開するクラスを持っていますが、これもスレッド セーフにしたいと考えています。

現時点では、クラスのインスタンスで連鎖可能なメソッドを呼び出すと、操作 ( Func<T>) を使用してさまざまなコレクションがセットアップされます。

結果が要求されると、実際の作業が行われます。これにより、ユーザーは次のようにメソッド呼び出しを任意の順序で連鎖させることができます。

var result = myFluentThing
.Execute(() => serviceCall.ExecHttp(), 5) 
.IfExecFails(() => DoSomeShizzle())
.Result<TheResultType>();

(ここで、5 は失敗したサービス呼び出しを再試行する回数です。)

明らかに、これはスレッドセーフでも再入可能でもありません。

この問題を解決する一般的な設計パターンは何ですか?

Execute メソッドを最初に呼び出す必要がある場合は、クラスの新しいインスタンスを毎回返すだけで動作しますが、チェーンの任意のポイントで任意のメソッドを呼び出すことができるため、この問題をどのように解決しますか?

「正しく機能させる」ためだけの単一の答えではなく、これを解決するためのさまざまな方法を理解することに興味があります。

私が達成しようとしていることについて誰かがより広いコンテキストを必要とする場合に備えて、完全なコードを GitHub に置きました: https://github.com/JamieDixon/ServiceManager

4

2 に答える 2

5

流暢なアプローチは 2 つのタイプに分けることができます。変化するものと変化しないもの。

変更ケースは .NET ではあまり一般的ではありません (流暢なアプローチは、Linq が導入されるまでは一般的ではありませんでした。これは、流暢なアプローチの非常に頻繁な使用です。比較すると、Java は、C# の代わりにプロパティを使用して同じ構文を提供するプロパティ セッターでそれらを頻繁に使用します。フィールドの設定としてプロパティを設定します)。ただし、1 つの例はStringBuilder.

StringBuilder sb = new StringBuilder("a").Append("b").Append("c");

基本的な形式は次のとおりです。

TypeOfContainingClass SomeMethod(/*... arguments ... */)
{
  //Do something, generally mutating the current object
  //though we could perhaps mix in some non-mutating methods
  //with the mutating methods a class like this uses, for
  //consistency.
  return this;
}

これは、問題のオブジェクトを変更するため、本質的に非スレッドセーフなアプローチであり、異なるスレッドからの 2 つの呼び出しが干渉します。そのような呼び出しに直面しても、一貫性のない状態にならないという意味でスレッドセーフなクラスを作成することは確かに可能ですが、一般的に、このアプローチを採用する場合、それらの変更の結果を気にします。そしてそれらだけ。たとえば、上記の例では、 string を最終的に保持するStringbBuilderことを気にします。スレッドセーフは無意味です。これは、それが正常に保持されるか、受け入れられるかの保証を考慮しないためです。そのような仮想クラス自体はスレッドセーフですが、呼び出しコードはそうではありません。sb"abc"StringBuilder"abc""acb"

(これは、スレッド セーフ コードでそのようなクラスを使用できないという意味ではありません。スレッド セーフ コードで任意のクラスを使用できますが、役に立ちません)。

現在、非変更形式は、それ自体がスレッドセーフです。これは、すべての使用がスレッドセーフであることを意味するわけではありませんが、可能であることを意味します。次の LINQ コードを検討してください。

var results = someSource
  .Where(somePredicate)
  .OrderBy(someOrderer)
  .Select(someFactory);

これは、次の場合に限りスレッド セーフです。

  1. someSource の反復処理はスレッドセーフです。
  2. somePredicate の呼び出しはスレッドセーフです。
  3. someOrder の呼び出しはスレッドセーフです。
  4. someFactory の呼び出しはスレッドセーフです。

これは多くの基準のように見えるかもしれませんが、実際には、最後の基準はすべて同じ基準です:Funcインスタンスが機能する必要があります - インスタンスには副作用*がなく、入力に応じた結果を返します。 (スレッドセーフでありながら機能的であることに関するいくつかのルールを曲げることはできますが、今は複雑にしないでください)。とまぁ、そういうケースを想定して名前をつけたのだろうFunc。Linq の最も一般的な種類のケースがこの説明に当てはまることに注意してください。例えば:

var results = someSource
  .Where(item => item.IsActive)//functional. Thread-safe as long as accessing IsActive is.
  .OrderBy(item => item.Priority)//functional. Thread-safe as long as accessing Priority is.
  .Select(item => new {item.ID, item.Name});//functional. Thread-safe as long as accessing ID and Name is.

現在、プロパティの実装の 99% で、getter複数のスレッドからの s の呼び出しは、別のスレッドの書き込みがない限り、スレッドセーフです。これは一般的なシナリオであるため、そのようなケースに安全に対応できるという点ではスレッドセーフですが、別のスレッドがそのような変更を行っている場合はスレッドセーフではありません。

同様に、次のようなソースsomeSourceを 4 つのカテゴリに分割できます。

  1. メモリ内のコレクション。
  2. データベースまたはその他のデータ ソースに対する呼び出し。
  3. どこかから取得した情報を 1 回通過するが、2 回目の反復でその情報を再度取得するために必要な情報がソースにない列挙型。
  4. 他の。

最初のケースの大部分は、他のリーダーだけに直面してもスレッドセーフです。同時書き込みに直面しても、スレッドセーフなものもあります。2番目のケースでは、実装に依存します-現在のスレッドで必要に応じて接続などを取得しますか、それとも呼び出し間で共有されるものを使用しますか? 3 番目のケースでは、別のスレッドが代わりに取得したアイテムを「失う」ことを許容できると見なさない限り、スレッドセーフではありません。まあ、「その他」は「その他」のとおりです。

以上のことから、スレッドセーフを保証するものはありませんが、必要な程度のスレッドセーフを提供する他のコンポーネントと一緒に使用すると、十分な程度のスレッドセーフを提供するものがあります。それを得る。

考えられるすべての用途に直面して、100% スレッドセーフですか? いいえ、何もあなたにそれを与えません。実際には、データ型はスレッドセーフではなく、特定のグループの操作のみです。データ型を「スレッドセーフ」と記述する場合、そのメンバーメソッドとプロパティはすべてスレッドセーフであると言い、メソッドまたはプロパティをスレッドセーフと記述すると、それ自体がスレッドセーフであるため、スレッドセーフ操作グループの一部になることができますが、スレッドセーフ操作のすべてのグループがスレッドセーフであるとは限りません。

この種のアプローチを実装したい場合は、呼び出されたオブジェクト (拡張機能ではなくメンバーの場合) とパラメーターに基づいて、オブジェクトを作成するメソッドまたは拡張機能を作成する必要がありますが、変更はしません。

Enumerable.Select議論のために、メソッドの 2 つの個別の実装を考えてみましょう。

public static IEnumerable<TResult> SelectRightNow<TSource, TResult>(
  this IEnumerable<TSource> source,
  Func<TSource, TResult> selector)
  {
    var list = new List<TResult>();
    foreach(TSource item in source)
      list.Add(selector(item));
    return list;
  }

public static IEnumerable<TResult> SelectEventually<TSource, TResult>(
  this IEnumerable<TSource> source,
  Func<TSource, TResult> selector)
  {
    foreach(TSource item in source)
      yield return selector(item);
  }

どちらの場合も、メソッドは何らかの方法で の内容に基づいた新しいオブジェクトをすぐに返しますsourcesourceただし、linq から得られる一種の遅延反復があるのは 2 番目だけです。最初の方法は、実際には 2 番目の方法よりもいくつかのマルチスレッドの状況にうまく対処できますが、厄介なことになります (たとえば、同時実行管理の一部としてロックを保持しながらコピーを取得したい場合は、ロックを保持しながらコピーを取得することによって行いますロック、他のものの真っ只中ではありません)。

どちらの場合でも、提供できるスレッドセーフの鍵となるのは、返されるオブジェクトです。1 つ目は、その結果に関するすべての情報を取得しているため、単一のスレッドに対してローカルでのみ参照される限り、スレッドセーフです。2 番目には、これらの結果を生成するために必要な情報が含まれているため、単一のスレッドに対してローカルでのみ参照される限り、ソースへのアクセスはスレッドセーフであり、呼び出しFunc はスレッドセーフです。最初に最初のものを作成します)。

要約すると、ソースと s のみを参照するオブジェクトを生成するメソッドがある場合、ソースとFuncs と同じくらいスレッドセーフになりますFuncが、安全ではありません。

*メモ化は、最適化として、外部からは見えない副作用をもたらします。s またはそれらが呼び出すもの (getter など) によって使用される場合Func、スレッドセーフを可能にするには、メモ化をスレッドセーフな方法で実装する必要があります。

于 2012-09-01T19:36:53.757 に答える
0

この問題をどのように解決したかについて少し追加情報を追加するには、連想回答を投稿すると役立つと思いました。

メソッド呼び出しをチェーンする「標準的な」方法は、同じクラスのインスタンスを返すことであり、その上で後続のメソッド呼び出しを行うことができます。

私の元のコードは を直接返すことthisでこれを行いましたが、私のメソッドは のコレクションを構築することによってフィールドを変更していたFunc<T>ため、消費者はスレッド化と再入力の問題にさらされていました。

この問題を解決するために、実装ICloneableして、同じクラスの新しいインスタンスを を介して返すことにしましたobject.MemberwiseClone()。追加されるフィールドは、shallow-clone プロセス中にコピーされる値型であるため、この浅いクローンはこのケースで正常に機能します。

クラスの各パブリック メソッドはインスタンスCloneメソッドを実行し、次のようにクローンを返す前にプライベート フィールドを更新します。

public class ServiceManager : IServiceManager
    {
        /// <summary>
        /// A collection of Funcs to execute if the service fails.
        /// </summary>
        private readonly List<Func<dynamic>> failedFuncs = 
                                             new List<Func<dynamic>>();

        /// <summary>
        /// The number of times the service call has been attempted.
        /// </summary>
        private int count;

        /// <summary>
        /// The number of times to re-try the service if it fails.
        /// </summary>
        private int attemptsAllowed;

        /// <summary>
        /// Gets or sets a value indicating whether failed.
        /// </summary>
        public bool Failed { get; set; }

        /// <summary>
        /// Gets or sets the service func.
        /// </summary>
        private Func<dynamic> ServiceFunc { get; set; }

        /// <summary>
        /// Gets or sets the result implimentation.
        /// </summary>
        private dynamic ResultImplimentation { get; set; }

        /// <summary>
        /// Gets the results.
        /// </summary>
        /// <typeparam name="TResult">
        /// The result.
        /// </typeparam>
        /// <returns>
        /// The TResult.
        /// </returns>
        public TResult Result<TResult>()
        {
            var result = this.Execute<TResult>();

            return result;
        }

        /// <summary>
        /// The execute service.
        /// </summary>
        /// <typeparam name="TResult">
        /// The result.
        /// </typeparam>
        /// <param name="action">
        /// The action.
        /// </param>
        /// <param name="attempts">
        /// The attempts.
        /// </param>
        /// <returns>
        /// ServiceManager.IServiceManager.
        /// </returns>
        public IServiceManager ExecuteService<TResult>(
                                   Func<TResult> action, int attempts)
        {
            var serviceManager  = (ServiceManager)this.Clone();
            serviceManager.ServiceFunc = (dynamic)action;
            serviceManager.attemptsAllowed = attempts;

            return serviceManager;
        }

        /// <summary>
        /// The if service fails.
        /// </summary>
        /// <typeparam name="TResult">
        /// The result.
        /// </typeparam>
        /// <param name="action">
        /// The action.
        /// </param>
        /// <returns>
        /// ServiceManager.IServiceManager`1[TResult -&gt; TResult].
        /// </returns>
        public IServiceManager IfServiceFailsThen<TResult>(
                                      Func<TResult> action)
        {
            var serviceManager = (ServiceManager)this.Clone();
            serviceManager.failedFuncs.Add((dynamic)action);
            return serviceManager;
        }


        /// <summary>
        /// Clones the current instance of ServiceManager.
        /// </summary>
        /// <returns>
        /// An object reprisenting a clone of the current ServiceManager.
        /// </returns>
        public object Clone()
        {
            return this.MemberwiseClone();
        }        
    }

簡潔にするためにプライベート メソッドを削除しました。完全なソース コードは次の場所にあります。

https://github.com/JamieDixon/ServiceManager

于 2012-09-02T16:30:09.267 に答える