Какая польза от шаблона SyncRoot? - PullRequest
61 голосов
/ 08 апреля 2009

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

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

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

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

object syncRoot = new object();

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

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

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

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

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

Ответы [ 6 ]

73 голосов
/ 08 апреля 2009

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

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

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

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

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

18 голосов
/ 08 апреля 2009

Вот пример:

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 ){ ... }
}

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

13 голосов
/ 08 апреля 2009

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

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

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

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

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

12 голосов
/ 15 декабря 2011

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

Например, если класс 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
        }
    }
}
6 голосов
/ 08 апреля 2009

См. эту статью Джеффа Рихтера. В частности, этот пример, который демонстрирует, что блокировка «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...
      }
   }
}
2 голосов
/ 08 апреля 2009

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

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 .

Проблема в том, что 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);
    }
}

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...