web-dev-qa-db-ja.com

SyncRootパターンの用途は何ですか?

SyncRootパターンについて説明しているc#本を読んでいます。それが示している

void doThis()
{
    lock(this){ ... }
}

void doThat()
{
    lock(this){ ... }
}

syncRootパターンと比較します。

object syncRoot = new object();

void doThis()
{
    lock(syncRoot ){ ... }
}

void doThat()
{
    lock(syncRoot){ ... }
}

ただし、ここでの違いはよくわかりません。どちらの場合も、両方のメソッドに同時にアクセスできるのは1つのスレッドのみです。

この本では...インスタンスのオブジェクトは外部からの同期アクセスにも使用でき、このフォーム自体をクラス自体で制御できないため、SyncRootパターンを使用できるため Eh? 「インスタンスのオブジェクト」?

上記の2つのアプローチの違いを教えてもらえますか?

63
Ryan

複数のスレッドによる同時アクセスを防ぎたい内部データ構造がある場合、ロックしているオブジェクトがパブリックでないことを常に確認する必要があります。

この背後にある理由は、パブリックオブジェクトは誰でもロックできるため、ロックパターンを完全に制御できないため、デッドロックを作成できるからです。

つまり、thisのロックはオプションではありません。誰でもそのオブジェクトをロックできるからです。同様に、外の世界にさらすものをロックしないでください。

つまり、最善の解決策は内部オブジェクトを使用することです。したがって、ヒントはObjectを使用することです。

データ構造のロックは、完全に制御する必要があるものです。そうしないと、デッドロックのシナリオを設定するリスクが生じます。

以下に例を示します。

class ILockMySelf
{
    public void doThat()
    {
        lock (this)
        {
            // Don't actually need anything here.
            // In this example this will never be reached.
        }
    }
}

class WeveGotAProblem
{
    ILockMySelf anObjectIShouldntUseToLock = new ILockMySelf();

    public void doThis()
    {
        lock (anObjectIShouldntUseToLock)
        {
            // doThat will wait for the lock to be released to finish the thread
            var thread = new Thread(x => anObjectIShouldntUseToLock.doThat());
            thread.Start();

            // doThis will wait for the thread to finish to release the lock
            thread.Join();
        }
    }
}

2番目のクラスは、lockステートメントで最初のクラスのインスタンスを使用できることがわかります。これにより、例ではデッドロックが発生します。

正しいSyncRoot実装は次のとおりです。

object syncRoot = new object();

void doThis()
{
    lock(syncRoot ){ ... }
}

void doThat()
{
    lock(syncRoot ){ ... }
}

syncRootはプライベートフィールドであるため、このオブジェクトの外部使用について心配する必要はありません。

18
ybo

このトピックに関連するもう1つの興味深いことがあります。

コレクションのSyncRootの疑わしい値(by Brad Adams)

System.Collectionsのコレクションの多くにSyncRootプロパティがあります。レトロスペック(sic)では、このプロパティは間違いだったと思います。私のチームのプログラムマネージャーであるKrzysztof Cwalinaは、その理由について考えを送ってくれました。私は彼に同意します。

SyncRootベースの同期APIは、ほとんどのシナリオに対して柔軟性が不十分であることがわかりました。 APIを使用すると、コレクションの単一のメンバーにスレッドセーフでアクセスできます。問題は、複数の操作をロックする(たとえば、1つのアイテムを削除して別のアイテムを追加する)必要があるシナリオが多数あることです。つまり、通常、コレクション自体ではなく、適切な同期ポリシーを選択する(実際に実装できる)コレクションを使用するコードです。 SyncRootが実際に使用されることは非常にまれであり、使用される場合、実際にはあまり価値がないことがわかりました。使用されていない場合、それはICollectionの実装者にとって厄介です。

これらのコレクションの汎用バージョンを作成するのと同じ間違いをしないことをご安心ください。

13
Igor Brejc

このパターンの実際の目的は、ラッパー階層との正しい同期を実装することです。

たとえば、クラスWrapperAがClassThanNeedsToBeSyncedのインスタンスをラップし、クラスWrapperBがClassThanNeedsToBeSyncedの同じインスタンスをラップする場合、WrapperAまたはWrapperBをロックすることはできません。このため、wrapperAInst.SyncRootおよびwrapperBInst.SyncRootをロックする必要があります。これらは、ClassThanNeedsToBeSyncedの1つにロックを委任します。

例:

public interface ISynchronized
{
    object SyncRoot { get; }
}

public class SynchronizationCriticalClass : ISynchronized
{
    public object SyncRoot
    {
        // you can return this, because this class wraps nothing.
        get { return this; }
    }
}

public class WrapperA : ISynchronized
{
    ISynchronized subClass;

    public WrapperA(ISynchronized subClass)
    {
        this.subClass = subClass;
    }

    public object SyncRoot
    {
        // you should return SyncRoot of underlying class.
        get { return subClass.SyncRoot; }
    }
}

public class WrapperB : ISynchronized
{
    ISynchronized subClass;

    public WrapperB(ISynchronized subClass)
    {
        this.subClass = subClass;
    }

    public object SyncRoot
    {
        // you should return SyncRoot of underlying class.
        get { return subClass.SyncRoot; }
    }
}

// Run
class MainClass
{
    delegate void DoSomethingAsyncDelegate(ISynchronized obj);

    public static void Main(string[] args)
    {
        SynchronizationCriticalClass rootClass = new SynchronizationCriticalClass();
        WrapperA wrapperA = new WrapperA(rootClass);
        WrapperB wrapperB = new WrapperB(rootClass);

        // Do some async work with them to test synchronization.

        //Works good.
        DoSomethingAsyncDelegate work = new DoSomethingAsyncDelegate(DoSomethingAsyncCorrectly);
        work.BeginInvoke(wrapperA, null, null);
        work.BeginInvoke(wrapperB, null, null);

        // Works wrong.
        work = new DoSomethingAsyncDelegate(DoSomethingAsyncIncorrectly);
        work.BeginInvoke(wrapperA, null, null);
        work.BeginInvoke(wrapperB, null, null);
    }

    static void DoSomethingAsyncCorrectly(ISynchronized obj)
    {
        lock (obj.SyncRoot)
        {
            // Do something with obj
        }
    }

    // This works wrong! obj is locked but not the underlaying object!
    static void DoSomethingAsyncIncorrectly(ISynchronized obj)
    {
        lock (obj)
        {
            // Do something with obj
        }
    }
}
12
Roman Zavalov

this Jeff Richterの記事を参照してください。より具体的には、「this」をロックするとデッドロックが発生する可能性があることを示すこの例:

using System;
using System.Threading;

class App {
   static void Main() {
      // Construct an instance of the App object
      App a = new App();

      // This malicious code enters a lock on 
      // the object but never exits the lock
      Monitor.Enter(a);

      // For demonstration purposes, let's release the 
      // root to this object and force a garbage collection
      a = null;
      GC.Collect();

      // For demonstration purposes, wait until all Finalize
      // methods have completed their execution - deadlock!
      GC.WaitForPendingFinalizers();

      // We never get to the line of code below!
      Console.WriteLine("Leaving Main");
   }

   // This is the App type's Finalize method
   ~App() {
      // For demonstration purposes, have the CLR's 
      // Finalizer thread attempt to lock the object.
      // NOTE: Since the Main thread owns the lock, 
      // the Finalizer thread is deadlocked!
      lock (this) {
         // Pretend to do something in here...
      }
   }
}
6
Anton Gogolev

別の具体例:

class Program
{
    public class Test
    {
        public string DoThis()
        {
            lock (this)
            {
                return "got it!";
            }
        }
    }

    public delegate string Something();

    static void Main(string[] args)
    {
        var test = new Test();
        Something call = test.DoThis;
        //Holding lock from _outside_ the class
        IAsyncResult async;
        lock (test)
        {
            //Calling method on another thread.
            async = call.BeginInvoke(null, null);
        }
        async.AsyncWaitHandle.WaitOne();
        string result = call.EndInvoke(async);

        lock (test)
        {
            async = call.BeginInvoke(null, null);
            async.AsyncWaitHandle.WaitOne();
        }
        result = call.EndInvoke(async);
    }
}

この例では、最初の呼び出しは成功しますが、デバッガーでトレースすると、ロックが解除されるまでDoSomethingの呼び出しがブロックされます。メインスレッドはtestでモニターロックを保持しているため、2番目の呼び出しはデッドロックします。

問題は、Mainがオブジェクトインスタンスをロックできることです。つまり、オブジェクトが同期する必要があると考えることをインスタンスが実行できないようにすることができます。ポイントは、オブジェクト自体がロックが必要なものを知っているということであり、外部干渉は単にトラブルを求めているだけです。そのため、外部の干渉を心配することなく同期にexclusivelyを使用できるプライベートメンバー変数を持つパターンがあります。

同等の静的パターンについても同じことが言えます。

class Program
{
    public static class Test
    {
        public static string DoThis()
        {
            lock (typeof(Test))
            {
                return "got it!";
            }
        }
    }

    public delegate string Something();

    static void Main(string[] args)
    {
        Something call =Test.DoThis;
        //Holding lock from _outside_ the class
        IAsyncResult async;
        lock (typeof(Test))
        {
            //Calling method on another thread.
            async = call.BeginInvoke(null, null);
        }
        async.AsyncWaitHandle.WaitOne();
        string result = call.EndInvoke(async);

        lock (typeof(Test))
        {
            async = call.BeginInvoke(null, null);
            async.AsyncWaitHandle.WaitOne();
        }
        result = call.EndInvoke(async);
    }
}

タイプではなく、プライベート静的オブジェクトを使用して同期します。

2
Darren Clark