Подтвердить что ты не робот

Какое использование шаблона SyncRoot?

Я читаю aС# книгу, которая описывает шаблон SyncRoot. Это показывает

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

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

и сравнивается с шаблоном SyncRoot:

object syncRoot = new object();

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

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

Однако я не очень понимаю разницу здесь; кажется, что в обоих случаях оба метода могут быть доступны только одному потоку за раз.

Книга описывает... поскольку объект экземпляра также может использоваться для синхронизированного доступа извне, и вы не можете управлять этой формой самого класса, вы можете использовать шаблон SyncRoot Eh? "объект экземпляра"?

Может кто-нибудь сказать мне разницу между двумя подходами выше?

4b9b3361

Ответ 1

Если у вас есть внутренняя структура данных, которую вы хотите предотвратить одновременному доступу несколькими потоками, вы всегда должны убедиться, что объект, который вы блокируете, не является общедоступным.

Поводом для этого является то, что публичный объект может быть заблокирован кем угодно, и, таким образом, вы можете создавать взаимоблокировки, потому что вы не полностью контролируете шаблон блокировки.

Это означает, что блокировка на this не является опцией, так как любой может заблокировать этот объект. Точно так же вы не должны блокировать то, что вы открываете для внешнего мира.

Это означает, что лучшим решением является использование внутреннего объекта, и, таким образом, совет должен просто использовать Object.

Блокирование структур данных - это то, что вам действительно нужно иметь полный контроль, иначе вы рискуете создать сценарий взаимоблокировки, что может быть очень проблематичным для обработки.

Ответ 2

Вот пример:

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();
        }
    }
}

Вы видите, что второй класс может использовать экземпляр первого в операторе блокировки. Это приводит к тупиковой ситуации в этом примере.

Правильная реализация SyncRoot:

object syncRoot = new object();

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

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

as syncRoot является частным полем, вам не нужно беспокоиться о внешнем использовании этого объекта.

Ответ 3

Вот еще одна интересная вещь, связанная с этой темой:

Сомнительная ценность SyncRoot на коллекциях (Брэд Адамс):

Вы заметите свойство SyncRoot во многих коллекциях в System.Collections. В ретроспективе (так) я думаю, что это свойство было ошибкой. Кшиштоф Квалина, руководитель программы в моей команде, только что поделился со мной некоторыми соображениями по поводу того, почему это так - я согласен с ним:

Мы обнаружили, что API-интерфейсы синхронизации на основе SyncRoot недостаточно гибки для большинства сценариев. API-интерфейсы обеспечивают потокобезопасный доступ к одному члену коллекции. Проблема в том, что существует множество сценариев, в которых вам нужно заблокировать несколько операций (например, удалить один элемент и добавить другой). Другими словами, обычно это код, который использует коллекцию, которая хочет выбрать (и может фактически реализовать) правильную политику синхронизации, а не саму коллекцию. Мы обнаружили, что SyncRoot на самом деле используется очень редко, и в тех случаях, когда он используется, он на самом деле не добавляет особой ценности. В тех случаях, когда он не используется, это просто раздражает разработчиков ICollection.

Будьте уверены, мы не допустим той же ошибки, что и при создании универсальных версий этих коллекций.

Ответ 4

Фактическая цель этого шаблона - правильная синхронизация с иерархией оберток.

Например, если класс WrapperA обертывает экземпляр ClassThanNeedsToBeSynced, а класс WrapperB обертывает тот же экземпляр ClassThanNeedsToBeSynced, вы не можете блокировать WrapperA или WrapperB, так как если вы заблокируете WrapperA, блокировка WrappedB не будет ждать. По этой причине вы должны заблокировать wrapperAInst.SyncRoot и wrapperBInst.SyncRoot, которые делегируют блокировку ClassThanNeedsToBeSynced.

Пример:

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
        }
    }
}

Ответ 5

См. эту статью Джеффа Рихтера. Более конкретно, этот пример демонстрирует, что блокировка на "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 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 Finalize method
   ~App() {
      // For demonstration purposes, have the CLR 
      // 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

Другой конкретный пример:

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 будет блокироваться до тех пор, пока блокировка не будет выпущена. Второй вызов будет заблокирован, так как основной поток удерживает блокировку монитора на тесте.

Проблема заключается в том, что Main может блокировать экземпляр объекта, а это означает, что он может заставить экземпляр делать что-либо, что объект считает синхронизированным. Дело в том, что сам объект знает, что требует блокировки, а внешнее вмешательство просто требует неприятностей. Вот почему шаблон наличия переменной частного члена, которую вы можете использовать исключительно для синхронизации, не беспокоясь о внешних помехах.

То же самое касается эквивалентного статического шаблона:

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);
    }
}

Используйте закрытый статический объект для синхронизации, а не тип.