Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ответы ОСиСП.doc
Скачиваний:
68
Добавлен:
11.05.2015
Размер:
1.78 Mб
Скачать

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

Идея логически связать структуру данных синхронизации с каждым объектом кучи выглядит очень заманчивой, и это действительно так. Но разработчики 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 передать экземпляр значимого типа.

Pulse и Wait

Monitor.Wait- Освобождает блокировку объекта и блокирует текущий поток до тех пор, пока тот не получит блокировку снова.

Monitor.Pulse- уведомляет поток в очереди готовности об изменении состояния объекта с блокировкой. Только текущий владелец блокировки может послать сигнал ожидающему объекту, используя Pulse.

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

Класс Monitor не поддерживает состояние, указывающее, что метод Pulse не вызывался. Таким образом, если вызывается Pulse, когда нет потоков в ожидании, следующий поток вызывает блоки Wait, как если бы Pulse никогда не вызывались.

Если два потока используют Pulse и Wait, чтобы взаимодействовать, это может привести их к взаимоблокировке.

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

Методы Pulse, PulseAll и Wait должны быть вызваны из синхронизированного блока кода.

Для сигнализации нескольких потоков используется метод PulseAll.

========================================================================

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

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

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

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