web-dev-qa-db-ja.com

TPLでブロックせずに共有データにアクセスする

データを含むクラスを書いています。データのクエリを可能にするメソッドを公開しますが、データは外部ソース(Webサービスなど)からも更新されます。

すべてのメソッドは、デフォルトのタスクスケジューラを使用してワーカースレッドで開始されるタスクを公開します。したがって、クラスの構造は次のとおりです。

public class A
{
    private object _myLock;

    public Task<int> GetSomeNumber(int aParameter)
    {
        return Task.Run(() =>
        {
            lock (_myLock)
            { 
                int result = 0;
                // calculate the result...
                // ...
                return result;
            }
        });
    }

    public Task<string> GetSomeText(int aParameter)
    {
        return Task.Run(() =>
        {
            lock (_myLock)
            {
                string result = "";

                // calculate the result...
                // ...
                return result;
            }
        });
    }
}

メソッドは、データ自体が更新されている間に複数回呼び出される可能性があるため、共有データを必要とする計算は、ロックステートメントで囲まれています。問題は、このコードが、タスクが実行されるスレッドをブロックすることです。もちろん、これらはUIスレッドではないため、UIは応答性を維持しますが、スレッドを消費し、ブロックされたままにします。スレッドプール内のワーカースレッドは、可能な限りそのままにしておくことをお勧めします。

共有データがスレッドセーフであると同時にタスクがブロックされないように、このコードを記述できる方法(ある種のベストプラクティス)はありますか?ロックを待つ代わりに、タスクが他の何かが完了するのを待つ必要がある場合(タスク)、継続タスクを作成して返すためにContinueWithを使用して、スケジュールされるようにします最初のタスクが完了したときに実行します。ロックを使用して同じことを行う方法はありますか?そのため、ロックが使用可能なときにのみタスクがスケジュールされますか?

更新私はいくつかの読み取りを行い、共有データの同期を可能にするいくつかの可能なオプションについて知りました。これらには、SemaphoreSlimAsyncLock、およびConcurrentExclusiveSchedulerPairが含まれます。

読み取りロックと同時に実行できる「読み取り専用」タスクメソッドと、書き込みロックを必要とする「書き込み」タスクメソッドを提供するサービスクラスを作成するベストプラクティスの推奨事項を知りたいです。

2
Kobi Hari

この回答は、コメントに記載されているユースケースに関する追加情報に基づいています。

グラフノードを提供するWebサービスがあります。グラフの一部のローカルコピーを保持し、非同期APIを公開してグラフをクエリします。クエリを実行する領域がキャッシュ内にある場合は、単純に計算して結果を返します。それ以外の場合は、不足している部分のダウンロードを開始し、計算を実行して結果を返します。また、バックグラウンドには、サーバーでノードの変更をポーリングし、変更をローカルキャッシュに適用する定期的なタスクがあります。


要求を満たすために必要なデータが単に利用できない(たとえば、ダウンロードもキャッシュもされていない)ために要求をすぐに完了できない場合は、いくつかの選択肢しかないことを理解する必要があります。

  • 失敗をすぐに返します。これは、ブロックしないTryGetパターンに似ていますが、タスクは成功しません。
  • すぐに失敗を返しますが、外部サーバーからデータをフェッチするための非同期リクエストを開始したことも示します。この方法では、リーダーはブロックしませんが、後で再試行する必要があります。
  • リーダーにコールバック関数を提供するよう要求します。コールバック関数は、データが利用可能になったときに後で呼び出されます。これは、非同期要求アプローチと組み合わせることができます。
  • データがフェッチされるまでリーダーをブロックします。

上記の各オプションには、独自の「ベストプラクティス」のセットとTPLとの統合パターンがあります。ここでそれらすべてを議論するのは多すぎるでしょう。

必要なヘルプ:これらのオプションのベストプラクティスをカバーする優れたオンラインリソースを誰かが知っている場合は、コメントとして投稿してください。


アプリケーションの要件を考慮して、リーダーのブロックが唯一の選択肢であると判断した場合は、次のことに注意してください。

  • .NETのデフォルトのスレッドプールはエラスティックです。つまり、一部のスレッドがブロックされていることを検知すると、より多くのワーカースレッドを起動します。これはいい。つまり、任意の数のリーダーをブロックしても、システム全体に障害が発生することはありません。
  • 伸縮性がない場合、ワーカースレッドの枯渇(枯渇)が原因でアプリケーションがデッドロックする可能性があります。
  • したがって、デフォルトのスレッドプールの設定を決して変更して、弾力性が失われるようにする必要があります-発生したときにサービスを再起動するつもりがない限り。
    • たとえば、ワーカースレッドの最大数を設定しないでください。

並行リーダーがスタックしないようにしたい場合(障害物がないため)、いくつかの選択肢があると思います。


複数のバッファリング

Wikipediaの記事へのリンク

  • システムはグラフの複数のコピーを維持し、変更が必要な場合は常にそれらを回転(循環)します。
  • いつでも、多くても1つのコピーが変更されます。他のすべてのコピーは読み取り専用になります。
  • 変更が行われている場合、各読者は次のいずれかを選択する必要があります。
    • 他の読み取り専用で少し古くなったグラフのコピーの1つからの読み取り。複数のリーダーが同時にアクセスできます。
    • 変更が完了するまで待って、最新バージョンのグラフにアクセスできることを確認します。

グラフの同時更新アルゴリズム

正確な詳細は、アプリケーションに必要な操作とアルゴリズムによって異なります。

それらのいくつかは、ハードウェア対応のアトミック命令を使用して、ロックフリーである場合があります。他のユーザーは、さまざまな種類のロックを使用する可能性がありますが、リーダーがブロックされる可能性または時間の長さを最小限に抑えようとする場合があります。

既存のライブラリを使用するか、独自のライブラリを実装することができます。

同時グラフ更新アルゴリズムに慣れていないので、あまり提案できません。

2
rwong

書き込みよりも読み取りの方が多いと予想される場合、一般的に安全な賭けは ReaderWriterLockSlim です。これにより、同時スレッドがデータを読み取ることができますが、一度に書き込みできるスレッドは1つだけです。

あなたは並行リストと辞書について言及しましたが、 ConcurrentDictionary <、> がこの状況で機能しない理由はありますか?私はそれが非常に高性能であり、ほとんどの状況で対処するのがはるかに面倒ではないことを発見しました。

一部のパフォーマンスが重要なソリューションでは、Interlocked.CompareExcgangeは非常に適切に機能しますが、これは同期する必要があるデータによって異なります。CompareExchangeは数値型で適切に機能しますが、参照ベースの変数は避けます。

0
Ovan Crone

どうですか:

// any child object should freeze when this object freezes...
interface IFreazableCloanable
{
    bool IsFrozen { get; }
    // Any further attempts to change the object should throw an Exception
    void Freeze();

    // Creates mutable Clone of the object
    IFreazableCloanable DeepCloneMutable();
}

class SyncedObject<T> where T : class, IFreazableCloanable
{
    SemaphoreSlim _writersLock = new SemaphoreSlim(0, 1);
    volatile T _internal;

    public SyncedObject(T thing) // if writes are unsynced then only the last one counts.
    {
        _internal = (T)thing.DeepCloneMutable();
    }
    public T Read()
    {
        T result = _internal;

        result.Freeze();

        return result;
    }

    public void Write(Func<T, T> writer)
    {
        _writersLock.Wait();

        T temp = (T)_internal.DeepCloneMutable();

        temp = writer(temp);

        _internal = temp;

        _writersLock.Release();
    }
}

class Data : IFreazableCloanable
{
    private string _data = "lala";
    public string MyData
    {
        get
        {
            return _data;
        }
        set
        {
            if (IsFrozen)
            {
                throw new Exception("Very Bad");
            }
            _data = value;
        }
    }
    public bool IsFrozen
    {
        get;
        private set;
    }

    public void Freeze()
    {
        IsFrozen = true;
    }

    public IFreazableCloanable DeepCloneMutable()
    {
        return new Data();
    }
}
0
AK_