Студопедия

КАТЕГОРИИ:

АстрономияБиологияГеографияДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРиторикаСоциологияСпортСтроительствоТехнологияФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника


Почему же «отличная» идея оказалась такой неудачной




Идея логически связать структуру данных синхронизации с каждым объектом кучи выглядит очень заманчивой, и это действительно так. Но разработчики Microsoft совершили большую ошибку при реализации этой идеи в CLR. Сейчас объясню почему.

Помните, неуправляемый код C++, приведенный в разделе «Отличная идея» в этой главе? Если бы вы писали этот код самостоятельно, сделали ли бы вы поле CRTICAL SECTION открытым? Конечно, нет! Открытое поле позволяет любому коду приложения изменять структуру CRTICAL_SECTION. Злоумышленнику ничего бы не стоило вызвать взаимную блокировку потоков, в которых работают экземпляры этого типа.

Ну и что же — блок синхронизации как раз имеет открытую структуру синхронизации данных, связанную с каждым объектом кучи! Любой код, имеющий ссылку на объект, в любой момент может передать эту ссылку в методы Enter и Exit и перехватить блокировку. Любой код может также передать в эти методы ссылку на любой объект-тип и перехватить блокировку этого типа. Проблема также возникает при передаче интернированного объекта String, поскольку теперь несколько строк использует одну блокировку. И, если передавать ссылку на объект производного от MarshalByRefObject типа, можно заблокировать либо сам объект, либо прокси-объект (сам объект останется не заблокированным).

Ниже приведен код, показывающий, к каким катастрофическим последствиям это может привести. В этом коде поток Main создает объект SomeType и налагает на этот объект блокировку. В какой-то момент времени происходит сборка мусора (в этом коде она принудительно инициируется только для демонстрации), и когда вызывается метод Finalize объекта SomeType, он пытается перехватить блокировку объекта. К сожалению, завершающий поток CLR не может получить блокировку объекта, так как ею владеет основной поток приложения. Это приводит к остановке CLR-потока деструктора — теперь ни один процесс (включая все AppDomain процесса) не сможет завершиться и никакая память, занятая объектами в куче, не может освободиться!

 

using System;

using System.Threading;

public sealed class SomeType

{

// Это метод Finalize объекта SomeType.

~SomeType()

{

// С целью демонстрации CLR-поток деструктора

// пытается перехватить блокировку объекта.

// ПРИМЕЧАНИЕ: поскольку блокировкой объекта владеет поток Main,

// завершающий поток блокируется!

Monitor.Enter(this);

}

}

public static class Program

{

public static void Main()

{

// Создание объекта

SomeType.SomeType st = new SomeType();

// Этот некорректный код перехватывает блокировку объекта

// и не освобождает ее.

Monitor.Enter(st);

// Для демонстрации запускаем сборку мусора

// и ожидаем завершения выполнения методов-деструкторов.

st = null;

GC.Collect();

GC.WaitForPendingFinalizers();

Console.WriteLine("We never get here, both threads are deadlocked!");

}

}

 

К сожалению, CLR, FCL и компиляторы предлагают много функций, в которых используется блокировка объектов. В главе 10 я уже упоминал о возможных проблемах с событиями. CLR также использует открытую блокировку объекта типа при вызове конструктора класса для типа.

В пространстве имен SystemRuntime.CompilerServices есть особый класс атрибутов по имени MethodlmplAttribute. Этот атрибут можно применять к методу, устанавливая флаг MethodlmplOptions.Synchronized. Если это экземплярный метод, то при этом JIT-компилятор заключает весь код метода в функцию lock(this). Если это статический метод, код помещается в функцию lock(typeof(<имя_типа>)), где <имя_типа> — имя самого типа. Это плохо — о причинах уже говорилось ранее, поэтому никогда не следует использовать MethodlmplAttribute с флагом MethodlmplOptions.Synchronized.

Нам не удастся исправить CLR, FCL или компилятор C#, но при написании собственного кода, можно соблюдать максимальную осторожность и обходить проблему открытой блокировки. Для этого лишь нужно определить закрытое поле System.Object в качестве члена своего типа, создать объект, а затем передать ссылку на закрытый объект в оператор lock. Вот исправленная версия класса Transaction, которая более защищена за счет использования закрытой блокировки объекта:

 

internal sealed class TransactionWithLockObject

{

// Выделяем память для закрытого объекта, используемого для блокирования.

private Object m_lock = new Object();

// Поле, указывающее время выполнения последней транзакции.

private DateTime timeOfLastTransaction;

public void PerformTransaction()

{

lock (m_lock)

{

// Блокируем объект закрытого поля.

// Выполнение транзакции...

// Записываем время последней транзакции.

timeOfLastTransaction = DateTime.Now;

} // Отмена блокировки объекта закрытого поля.

// Неизменяемое свойство, возвращающее время последней транзакции.

}

public DateTime LastTransaction

{

get

{

lock (m_lock)

{

// Блокировка объекта закрытого поля.

// Возвращение даты и времени.

return timeOfLastTransaction;

}

// Отмена блокировки объекта закрытого поля.

}

}

}

 

Внимание!В этом коде показано, как избавляться от проблем, о которых говорилось ранее. Самая важная его особенность — блокировка синхронизации потока с использованием закрытого поля. Это не позволяет постороннему коду получить доступ к блокировке и, следовательно, вмешаться в код вашего типа и вызвать взаимную блокировку потоков.

Кажется странным создавать в классе Monitor объект System.Object только для синхронизации. В Microsoft спроектировали класс Monitor некорректно. Его нужно было спроектировать так, чтобы для создания блокировки синхронизации потока нужно было создать экземпляр типа Monitor. Затем статические методы должны быть экземплярными методами, самостоятельно работающими с объектом блокировки — тогда не пришлось бы передавать аргумент System.Object в методы класса Monitor. Это решило бы все проблемы и упростило бы модель программирования. Кстати, приведенный выше код легко модифицировать для синхронизации статических методов — достаточно просто изменить все члены на static. Если в вашем типе уже определены какие-либо закрытые поля данных, можете задействовать одно из них как объект блокировки, передаваемый в методы класса Monitor. Это поможет сэкономить немного памяти, поскольку не нужно будет размещать объект System.Object. Однако я бы не стал делать этого просто ради незначительной экономии памяти; код типов закрытых полей может вызывать метод lock(this). Если это случится, код будет заблокирован, и может возникнуть взаимная блокировка.

Внимание!Никогда не передавайте переменную значимого типа в метод Monitor.Enter или оператор lock языка C#. У распакованных экземпляров значимого типа нет члена «индекс блокировки синхронизации», а поэтому их нельзя использовать для синхронизации. Если передать распакованный экземпляр значимого типа в метод Monitor.Enter, компилятор C# автоматически создаст код упаковки экземпляра. Если тот же экземпляр передать в Monitor.Exit, снова произойдет упаковка. В результате код заблокирует один, но разблокирует совершенно другой объект, и никакой безопасности потоков обеспечено не будет. Если передать распакованный экземпляр значимого типа оператору lock, компилятор обнаружит это и вернет ошибку:

error CSO185: Valuetype' is not a reference type as required by the lock statement (ошибка CSO185: значимый тип не является ссылочным типом, который требуется оператору lock).

Однако компилятор не вернет ошибку или предупреждение, если в метод Monitor.Enter или Monitor.Exit передать экземпляр значимого типа.


Поделиться:

Дата добавления: 2015-04-21; просмотров: 72; Мы поможем в написании вашей работы!; Нарушение авторских прав





lektsii.com - Лекции.Ком - 2014-2024 год. (0.005 сек.) Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав
Главная страница Случайная страница Контакты