web-dev-qa-db-ja.com

文字列をロックオブジェクトとして使用することはできますか?

文字列の有限セットに基づいて、エリアにクリティカルセクションを作成する必要があります。同じ文字列インスタンスに対してロックを共有したい( String.Intern アプローチに多少似ている)。

私は次の実装を検討しています:

public class Foo
{
    private readonly string _s;
    private static readonly HashSet<string> _locks = new HashSet<string>();

    public Foo(string s)
    {
        _s = s;
        _locks.Add(s);
    }

    public void LockMethod()
    {
        lock(_locks.Single(l => l == _s))
        {
            ...
        }
    }
}

このアプローチに問題はありますか?この方法で文字列オブジェクトをロックしても大丈夫ですか?HashSet<string>の使用にスレッドセーフティの問題はありますか?

たとえば、各文字列インスタンスに対して新しいロックオブジェクトを作成するDictionary<string, object>を作成する方が良いですか?


最終実装

私は次の実装で行った提案に基づいています:

public class Foo
{
    private readonly string _s;
    private static readonly ConcurrentDictionary<string, object> _locks = new ConcurrentDictionary<string, object>();

    public Foo(string s)
    {
        _s = s;
    }

    public void LockMethod()
    {
        lock(_locks.GetOrAdd(_s, _ => new object()))
        {
            ...
        }
    }
}
42
Zaid Masud

文字列のロックは推奨されません。主な理由は、(文字列のインターンのため)他のコードがこれを知らなくても同じ文字列インスタンスをロックする可能性があるためです。デッドロック状態の可能性を作成します。

現在、これはおそらく、最も具体的な状況で遠く離れたシナリオです。これはライブラリのより一般的なルールです。

しかし、一方で、文字列の認識されている利点は何ですか?

したがって、ポイントごとに:

このアプローチに問題はありますか?

はい、しかしほとんど理論的です。

この方法で文字列オブジェクトをロックしても問題ありませんか。また、HashSetの使用にスレッドセーフティの問題はありますか?

HashSet<>は、スレッドが同時に読み取るだけである限り、スレッドの安全性には関与しません。

たとえば、文字列インスタンスごとに新しいロックオブジェクトを作成するディクショナリを作成する方がよいでしょうか。

はい。安全のために。大規模なシステムでは、デッドロックを回避するための主な目的は、ロックオブジェクトをできるだけローカルでプライベートに保つことです。限られた量のコードだけがそれらにアクセスできます。

25
Henk Holterman

個人的にはそれは本当に悪い考えだと思います。それが文字列の目的ではありません。

(個人的には、すべてのオブジェクトにそもそもモニターがあるという事実は嫌いですが、それは少し異なる懸念事項です。)

異なるインスタンス間で共有できるロックを表すオブジェクトが必要な場合は、そのための特定のタイプを作成してみませんか?診断目的で十分簡単に​​ロックに名前を付けることができますが、ロックは実際には文字列の目的ではありません。このようなもの:

public sealed class Lock
{
    private readonly string name;

    public string Name { get { return name; } }

    public Lock(string name)
    {
        if (name == null)
        {
            throw new ArgumentNullException("name");
        }
        this.name = name;
    }
}

文字列が時々抑留され、時々されない方法を考えると(単純な検査では識別が困難な場合がある)、簡単に誤って意図しない場所での共有ロック。

24
Jon Skeet

文字列のロックは、インターンされた文字列が本質的にグローバルであるため、問題になる可能性があります。

インターンされた文字列はプロセスごとなので、異なるAppDomain間で共有されます。同じことがタイプオブジェクトにも当てはまります(typeof(x)をロックしないでください)。

6
Brian Rasmussen

文字列値に基づいてコードのセクションをロックするための良い方法を探していた、少し前に同様の問題がありました。これが現時点で用意されているもので、インターンされた文字列の問題を解決し、必要な粒度を備えています。

主なアイデアは、文字列キーを持つ同期オブジェクトの静的な ConcurrentDictionary を維持することです。スレッドは、メソッドに入るとすぐにロックを確立し、同期オブジェクトを並行辞書に追加しようとします。並行ディクショナリに追加できる場合、他のスレッドには文字列キーに基づくロックがなく、作業を続けることができます。それ以外の場合は、並行ディクショナリの同期オブジェクトを使用して、実行中のスレッドが処理を完了するのを待機する2番目のロックを確立します。 2番目のロックが解放されると、現在のスレッドの同期オブジェクトを辞書に追加し直すことができます。

注意点:スレッドはキューに入れられません。そのため、同じ文字列キーを持つ複数のスレッドがロックを同時に競合している場合、それらが処理される順序は保証されません。

私が何かを見落としたと思ったら、遠慮なく批評してください。

public class Foo
{
    private static ConcurrentDictionary<string, object> _lockDictionary = new ConcurrentDictionary<string, object>();

    public void DoSomethingThreadCriticalByString(string lockString)
    {
        object thisThreadSyncObject = new object();

        lock (thisThreadSyncObject)
        {
            try
            {
                for (; ; )
                {
                   object runningThreadSyncObject = _lockDictionary.GetOrAdd(lockString, thisThreadSyncObject);
                   if (runningThreadSyncObject == thisThreadSyncObject)
                       break;

                    lock (runningThreadSyncObject)
                    {
                        // Wait for the currently processing thread to finish and try inserting into the dictionary again.
                    }
                }

                // Do your work here.

            }
            finally
            {
                // Remove the key from the lock dictionary
                object dummy;
                _lockDictionary.TryRemove(lockString, out dummy);
            }
        }
    }
}
2
Bill Ravdin