Потоки и синхронизация: часть 1 - Асинхронные делегаты

ОГЛАВЛЕНИЕ

Асинхронные делегаты

При определении делегата в приложении CLR добавляет два дополнительных метода: BeginInvoke и EndInvoke. Эти методы применяются для асинхронного вызова метода, на который ссылается делегат. BeginInvoke возвращает IAsyncResult, применяемый для отслеживания хода асинхронной операции. BeginInvoke внутри использует класс пула потоков для асинхронного вызова метода.

class Program
    {
        public delegate void FactorialDelegate(object obj);

        static void Main(string[] args)
        {
             ThreadsExperiment.Program experiment;
            FactorialDelegate factorialImpl;
            IAsyncResult asyncResult;
            int num1;
            int num2;

            experiment = new Program();

            Console.Write ("Please enter 1st number: ");
            num1 = int.Parse(Console.ReadLine());

            Console.Write("Please enter 2nd number: ");
            num2 = int.Parse(Console.ReadLine());

            factorialImpl = experiment.CalculateFactorial;
            asyncResult =  factorialImpl.BeginInvoke (num1, null, null);
         
            experiment.CalculateFactorial(num2);

            factorialImpl.EndInvoke(asyncResult);
            Console.ReadLine();
        }
        public void CalculateFactorial(object obj)
        {
            int factorial;

            int num = (int)obj;
            factorial = 1;
            for (int iloop = num; iloop >= 1; iloop--)
            {
                factorial = factorial * iloop;
            }
            Console.WriteLine(System.Threading.Thread.CurrentThread.Name +
                " calculates the factrorial of " +
                num.ToString() + " as " + factorial.ToString());
           
        }
    }

Чтобы больше узнать о делегатах, читайте статью Изучение делегатов. Пока были рассмотрены разные способы разработки многопоточного приложения в .NET. Далее рассмотрены способы синхронизации потоков, имеющиеся в каркасе Microsoft .NET.

Синхронизация потоков

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

Кусок кода, где поток обращается к общему ресурсу, называется "критической секцией". Способы синхронизации потоков гарантируют, что если один поток выполняет свою критическую секцию, ни один другой поток не может войти в критическую секцию. Для написания стабильного многопоточного приложения надо ясно понимать способы синхронизации потоков. Если эта область программирования написана плохо, крайне тяжело выявить и устранить ошибки, что ухудшает производительность приложения. Способы синхронизации потоков должны удовлетворять следующим условиям:
•    Взаимное исключение: Если поток находится в критической секции, ни один другой поток не может войти в критическую секцию.
•    Прогресс: Если ни один поток не выполняет свою критическую секцию и есть другие потоки, желающие войти в свою критическую секцию, то выбор потоков, которые следующими войдут в критическую секцию, нельзя откладывать бесконечно.

Каркас Microsoft .NET предоставляет несколько способов синхронизации потоков. Разберем их по одному и обсудим, насколько они отвечают вышеназванным требованиям к синхронизации потоков.

SyncBlock

Когда объект создается в куче, CLR добавляет поле под именем SyncBlock. CLR предоставляет механизм предоставления владения SyncBlock объекта потоку. В любой момент времени только один поток может владеть SyncBlock для конкретного объекта. Потоки могут запрашивать владение SyncBlock для конкретного объекта obj путем выполнения оператора Monitor.Enter(obj). Если никакой другой поток не владеет SyncBlock объекта obj, то владение предоставляется текущему потоку, в противном случае текущий поток приостанавливается. Monitor.Exit(obj) применяется для снятия владения. Следующий пример использует класс Monitor для синхронизации потоков, обращающихся к общему ресурсу historyData;:

class HistoryManager
    {
        private Object threadSyncObject;
        private System.Collections.Hashtable historyData;

        public HistoryManager()
        {
            threadSyncObject = new object();
            historyData = new System.Collections.Hashtable();
        }


        public Object ReadHistoryRecord(int historyID)
        {
            try
            {
                //Пытаемся завладеть SyncBlock объекта threadSyncObject
                System.Threading.Monitor.Enter(threadSyncObject);
                 return historyData[historyID];
            }
            finally
            {
                System.Threading.Monitor.Exit(threadSyncObject);
                //Снимаем владение
            }
        }

        public Object AddHistoryRecord(int historyID, Object historyRecord)
        {
            try
            {
                //Пытаемся завладеть SyncBlock объекта threadSyncObject
                System.Threading.Monitor.Enter(threadSyncObject);
                historyData.Add(historyID, historyRecord);
            }
            finally
            {
                // Снимаем владение
                System.Threading.Monitor.Exit(threadSyncObject);  
            }
        }
    }

Язык C# предоставляет простой оператор "блокировки" в качестве альтернативы классу Monitor. Следуя подходу SyncBlock, помните о следующих типичных ошибках:

Не используйте типы данных значений для синхронизации потоков.

SyncBlock связан с типом ссылочных данных. Если типы данных значений передаются в качестве аргументов Monitor.Enter, они сначала упаковываются, а затем владение SyncBlock упакованного значения предоставляется вызывающему потоку. Поэтому, если два потока пытаются завладеть типом значения, им завладеют оба потока, так как оба ссылаются на два разных упакованных значения. Аналогично, Monitor.Exit не снимет блокировку, так как она будет ссылаться на другое упакованное значение, как показано в следующем примере:

class HistoryManager
    {
        private int threadSync;
        private System.Collections.Hashtable historyData;

        public HistoryManager()
        {
            threadSyncObject = new object();
            historyData = new System.Collections.Hashtable();
        }


        public Object ReadHistoryRecord(int historyID)
        {
            try
            {
                //threadSync будет упакован, а затем
                //владение SyncBlock
                //вновь упакованного значения сразу будет
                //предоставлено текущему потоку
                System.Threading.Monitor.Enter(threadSync);  
                return historyData[historyID];
            }
            finally
            {
                //Не снимет блокировку, так как в этот раз
                //это другое упакованное значение
                System.Threading.Monitor.Exit(threadSync);
            }
        }

        public Object AddHistoryRecord(int historyID, Object historyRecord)
        {
            try
            {
                //threadSync будет упакован, а затем
                //владение SyncBlock
                // вновь упакованного значения будет сразу
                //предоставлено текущему потоку
                System.Threading.Monitor.Enter(threadSync);  
                historyData.Add(historyID, historyRecord);
            }
            finally
            {
                // Не снимет блокировку, так как в этот раз
                // это другое упакованное значение
                System.Threading.Monitor.Exit(threadSync);   
            }
        }
    }

Не используйте класс Тип для синхронизации потоков.

Каждому типу соответствует класс описателя типа. Ссылку описателя типа можно получить путем вызова метода GetType(). Так как описатель типа является объектом и имеет связанный с ним SyncBlock, он может применяться для синхронизации потоков. Так как есть только один экземпляр на каждый тип в процессе, нельзя использовать его для синхронизации потоков, так как некий посторонний код может использовать его для блокировки и способен вызвать зависание. Microsoft рекомендует использовать локальные объекты для синхронизации потоков.

Сборщик мусора собирает мусор в своем собственном потоке.

Если объект блокируется, а затем уничтожается без снятия блокировки, и если одновременно сборщик мусора начинает собирать объект, метод finalize может быть вызван. Причина состоит в том, что когда сборщик мусора начинает собирать мусор, все прочие потоки останавливаются, и метод finalize вызывается потоком сборщика мусора. Если заблокировать объект в методе finalize, поток сборщика мусора не сможет заблокировать объект, так как он уже заблокирован основным потоком. Это остановит приложение. Такая ситуация показана ниже:

class Program
    {
        public delegate void FactorialDelegate(object obj);

        static void Main(string[] args)
        {
           

            ThreadsExperiment.Program experiment;
            experiment = new Program();

            System.Threading.Monitor.Enter(experiment);
            experiment = null;
           
            //Сборщик мусора вынужден собирать и вызывать все ожидающие методы
// Завершить.
            GC.Collect();
            GC.WaitForPendingFinalizers();

            Console.WriteLine("press any enter to exit");
            Console.ReadLine();
        }

        ~Program()
        {
            System.Threading.Monitor.Enter(this);
            //Управление никогда не перейдет сюда
            System.Threading.Monitor.Exit(this);
        }
    }

SyncBlock является одним из способов реализации синхронизации между потоками в каркасе Microsoft .NET. В части 2 данной статьи будут рассмотрены другие способы синхронизации потоков, доступные в каркасе Microsoft .NET.