Рекомендации по обеспечению надежности

Приведенные здесь рекомендации по обеспечению надежности относятся преимущественно к SQL Server, однако вы можете руководствоваться ими при работе с любыми размещенными серверными приложениями. Очень важно не допустить утечки ресурсов и отключения на таких серверах, как SQL Server. Однако добиться этого с помощью кода возврата для каждого метода, который изменяет состояние объекта, не удастся. Целью здесь не является написание абсолютно надежного управляемого кода, который будет обеспечивать восстановление после любых ошибок в любом месте с кодом возврата. Это слишком трудоемкая задача с минимальными шансами на успех. Общеязыковая среда выполнения (CLR) не дает полных гарантий того, что написание управляемого кода будет простым. Обратите внимание, что в отличие от ASP.NET в SQL Server задействуется только один процесс, который невозможно использовать повторно без отключения базы данных на недопустимо долгое время.

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

  • Не допускайте утечки ресурсов операционной системы.

  • Выявите все управляемые блоки любых форм в среде CLR.

  • Не нарушайте общее состояние общего для нескольких приложений домена, что необходимо для эффективного повторного использования AppDomain.

Несмотря на теоретическую возможность написать управляемый код для обработки исключений ThreadAbortException, StackOverflowException и OutOfMemoryException, требовать от разработчиков создания настолько надежного кода для всего приложения неразумно. Поэтому специализированные исключения приводят к завершению выполняющегося потока. Если при этом завершающий работу поток редактировал общее состояние, то есть устанавливал блокировку, выполняется выгрузка AppDomain. При завершении метода, который редактирует общее состояние, возможно повреждение состояния, поскольку в этом случае невозможно написать надежный код возврата для обновления общего состояния.

В платформа .NET Framework версии 2.0 единственный узел, требующий надежности, — SQL Server. Если ваша сборка будет выполняться на сервере SQL Server, необходимо написать код для обеспечения надежности для всех ее частей, в том числе для отдельных функций, которые будут отключены при выполнении в базе данных. Это обязательно, поскольку модуль анализа кода проверяет код на уровне сборки и не может отличить отключенный код. При программировании для SQL Server также необходимо учитывать, что сервер SQL Server выполняется в рамках одного процесса, а для высвобождения всех ресурсов, включая память и дескрипторы операционной системы, реализуется возможность повторного использования AppDomain.

В коде возврата нельзя полагаться на методы завершения, деструкторы или блоки try/finally. Гарантировать их вызов или работу без прерывания невозможно.

Асинхронные исключения могут возникать в самых разных местах и практически в любой инструкции машинного кода, например: ThreadAbortException, StackOverflowException и OutOfMemoryException.

В SQL управляемые библиотеки необязательно выполняются в потоках Win32 и могут находиться в волокнах.

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

В SQL Server нередко встречаются ситуации, связанные с нехваткой памяти.

Если размещенные в SQL Server библиотеки некорректно обновляют свое общее состояние, высока вероятность того, что код не восстановится до тех пор, пока не будет перезапущена база данных. Кроме того, в крайних случаях это может привести к сбою процесса SQL Server и вынужденной перезагрузке базы данных. Перезагрузка базы данных может привести к отключению веб-сайта и, соответственно, отрицательно сказаться на уровне доступности и общей эффективности деятельности компании. Медленная утечка ресурсов операционной системы, например памяти или дескрипторов, может в конечном итоге привести к сбою сервера при выделении дескрипторов, восстановление после которого будет невозможно. Также может постепенно снижаться производительность сервера, в результате чего ухудшается степень доступности приложений заказчика. Очевидно, такого развития событий необходимо избежать.

Правила рекомендаций

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

В случае взаимоблокировки или ограниченности ресурсов SQL Server прекратит выполнение потока или демонтирует AppDomain. В этом случае гарантированно будет выполняться только код возврата, который находится в области ограниченного выполнения (CER).

Использование Сейф Handle для предотвращения утечки ресурсов

При выгрузке AppDomain нельзя полагаться на блоки finally или методы завершения, поэтому важно полностью абстрагировать доступ ко всем ресурсам операционной системы, используя класс SafeHandle вместо IntPtr, HandleRef и других аналогичных классов. Благодаря этому среда CLR может отслеживать и закрывать используемые дескрипторы даже в случае демонтажа AppDomain. Класс SafeHandle будет использовать критический метод завершения, который будет выполняться в среде CLR при любых обстоятельствах.

Дескриптор операционной системы хранится в дескрипторе SafeHandle с момента создания и вплоть до момента высвобождения. Утечка дескриптора невозможна, поскольку исключение ThreadAbortException не может возникнуть ни в какой момент. Кроме того, при вызове неуправляемого кода будет подсчитываться число ссылок на дескриптор, что позволит более точно отслеживать время существования дескриптора и предотвращать проблемы безопасности, связанные с состоянием гонки между Dispose и методом, который в настоящий момент использует этот дескриптор.

Для большинства классов, которые в настоящий момент используют метод завершения для простой очистки дескриптора операционной системы, метод завершения больше не потребуется. Вместо этого метод завершения будет размещаться в производном от SafeHandle классе.

Обратите внимание, что SafeHandle не является заменой для IDisposable.Dispose. При этом по-прежнему сохраняются риски состязания за ресурсы и преимущества в производительности, связанные с явным высвобождением ресурсов операционной системы. Необходимо помнить, что блоки finally, которые явно высвобождают ресурсы, могут выполняться не до конца.

SafeHandle позволяет реализовать собственный метод ReleaseHandle, который выполняет операции по высвобождению дескриптора, то есть передает состояние в подпрограмму высвобождения дескриптора операционной системы или высвобождает набор дескрипторов в рамках цикла. В среде CLR выполнение этого метода гарантируется. Ответственность за то, чтобы дескриптор высвобождался при любых обстоятельствах, возлагается на разработчика реализации ReleaseHandle. В противном случае возможна утечка дескриптора, что часто влечет за собой утечку связанных с ним собственных ресурсов. Таким образом, важно структурировать производные от SafeHandle классы так, чтобы для реализации ReleaseHandle не требовалось выделение каких-либо ресурсов, которые могут быть недоступны в момент вызова. Обратите внимание, что методы, которые могут завершаться сбоем в реализации ReleaseHandle, допускается выполнять при условии, что в коде реализованы обработка таких ситуаций отказа и выполнение контракта на высвобождение собственного дескриптора. Для отладки в ReleaseHandle предусмотрено возвращаемое значение типа Boolean, которому присваивается значение false при возникновении неустранимой ошибки, которая препятствует высвобождению ресурса. В этом случае активируется помощник по отладке управляемого кода releaseHandleFailed (если он включен), который способствует выявлению проблемы. При этом никакого другого воздействия на среду выполнения не оказывается. ReleaseHandle не вызывается повторно для того же ресурса, в результате чего происходит утечка дескриптора.

SafeHandle нельзя использовать в некоторых контекстах. Поскольку метод ReleaseHandle может выполняться в потоке метода завершения GC, любые дескрипторы, которые требуется высвобождать в конкретном потоке, не должны помещаться в SafeHandle.

Вызываемые оболочки времени выполнения (RCW) в среде CLR очищаются без использования дополнительного кода. Если вы используете вызовы неуправляемого кода для обработки COM-объекта как IUnknown* или IntPtr, в коде необходимо использовать вызываемые оболочки времени выполнения. Класс SafeHandle в этом случае не подходит, поскольку метод высвобождения неуправляемого кода может обращаться к управляемому коду.

Правило анализа кода

Используйте SafeHandle для инкапсуляции ресурсов операционной системы. Не используйте HandleRef или поля типа IntPtr.

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

Тщательно проверьте методы завершения и убедитесь, что даже если они не выполняются, утечка критических ресурсов операционной системы отсутствует. В отличие от обычной выгрузки AppDomain, при которой приложение выполняется в стабильном состоянии или завершается работа сервера (например, SQL Server), при неожиданной выгрузке AppDomain завершение объектов не производится. Убедитесь, что при неожиданной выгрузке не происходит утечка ресурсов, поскольку в этом случае невозможно гарантировать корректность приложения, однако необходимо обеспечить целостность сервера. Используйте SafeHandle для высвобождения любых ресурсов операционной системы.

Убедитесь, что предложения, наконец, не должны выполняться, чтобы предотвратить утечку ресурсов операционной системы

Выполнение предложений finally вне областей ограниченного выполнения (CER) не гарантируется, в связи с чем разработчики библиотек не должны полагаться на код в блоке finally для высвобождения неуправляемых ресурсов. В таких случаях рекомендуется использовать SafeHandle.

Правило анализа кода

Для очистки ресурсов операционной системы используйте SafeHandle вместо Finalize. Не используйте IntPtr. Вместо этого для инкапсуляции ресурсов следует использовать SafeHandle. Если предложение finally все же необходимо выполнять, поместите его в область ограниченного выполнения.

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

Среде CLR необходимо знать, когда код находится в состоянии блокировки, поскольку в этом случае вместо простого прерывания потока выполняется демонтаж AppDomain. Опасность прерывания потока связана с тем, что используемые в нем данные могут оставаться в несогласованном состоянии. Соответственно, потребуется повторное использование всего AppDomain. Если наличие блокировки не определено, это может привести к возникновению взаимоблокировок и получению некорректных результатов. Для определения областей блокировки используйте методы BeginCriticalRegion и EndCriticalRegion. Эти статические методы класса Thread применяются только к текущему потоку и помогают предотвратить ситуации, когда один поток редактирует счетчик блокировок другого.

В Enter и Exit это уведомление CLR реализовано во встроенной форме, в связи с чем рекомендуется использовать их, а также оператор lock, который использует эти методы.

Эти методы должны вызываться другими механизмами блокировки, такими как спин-блокировки или AutoResetEvent, для уведомления среды CLR о переходе в критическую секцию. Эти методы не принимают блокировки и лишь уведомляют среду CLR о том, что код выполняется в критической секции, в связи с чем в случае прерывания потока общее состояние может быть несогласованным. Эти методы подсчета блокировок необходимо использовать в том случае, если вы определили собственный тип блокировки, например настраиваемый класс ReaderWriterLock.

Правило анализа кода

Пометьте и определите все блокировки, используя BeginCriticalRegion и EndCriticalRegion. Не используйте CompareExchange, Increment и Decrement в цикле. Не выполняйте вызов неуправляемого кода вариантов этих методов для Win32. Не используйте Sleep в цикле. Не используйте изменяемые поля.

Код очистки должен находиться в блоке перехвата, а не после перехвата.

Код очистки никогда не должен располагаться после блока catch. Он должен находиться внутри блоков finally или catch. Это стандартная рекомендация. В большинстве случаев следует использовать блок finally, поскольку он выполняет один и тот же код и при возникновении исключения, и при переходе к концу блока try в обычном режиме. При возникновении непредвиденного исключения, например ThreadAbortException, код очистки не выполняется. В идеальном случае все неуправляемые ресурсы, очистка которых должна выполняться в блоке finally, следует заключать в SafeHandle, чтобы предотвратить утечки. Обратите внимание, что для эффективного высвобождения объектов, включая и дескрипторы, можно использовать ключевое слово using в C#.

Несмотря на то, что при повторном использовании AppDomain очистка может выполняться в потоке метода завершения, по-прежнему важно правильно выбрать место, где будет размещаться код очистки. Обратите внимание, что если поток получает асинхронное исключение и при этом не удерживает блокировку, среда CLR пытается завершить сам поток, не используя AppDomain повторно. Если ресурсы в конечном итоге гарантированно высвобождаются, это увеличивает объем доступных ресурсов и позволяет оптимизировать управление их временем существования. Если дескриптор файла не закрывается явно при обработке какой-либо ошибки, дождитесь, пока он будет высвобожден с помощью метода завершения SafeHandle. Если не выполнить этот метод завершения, при следующем запуске кода попытка получить доступ к тому же файлу может завершиться сбоем. По этой причине рекомендуется убедиться, что код очистки существует и правильно работает, обеспечивая более быстрое и эффективное восстановление после сбоев. Тем не менее делать это необязательно.

Правило анализа кода

Код очистки после блока catch должен находиться в блоке finally. Размещайте вызовы для высвобождения в блоке finally. Блоки catch должны завершаться операторами throw или rethrow. За некоторыми исключениями (например, в коде, обнаруживающем возможность установления сетевого подключения, в котором могут возникать самые разнообразные исключения), в любом коде, где требуется перехватывать некоторое число исключений, в обычных обстоятельствах следует указывать на необходимость проверки успешного выполнения кода.

Изменяемое состояние между доменами приложений должно быть устранено или использовать область ограниченного выполнения

Как было указано во вступительной части этой статьи, крайне сложно написать управляемый код, который сможет надежно отслеживать общее состояние на уровне процесса между доменами приложений. Общее состояние на уровне процесса — это структура данных любого вида, которая совместно используется разными доменами приложений в коде Win32, в среде CLR или в управляемом коде посредством удаленного взаимодействия. При этом очень сложно корректно реализовать любое изменяемое общее состояние в управляемом коде, а статическое общее состояние следует реализовывать с большой осторожностью. В случае с общим состоянием на уровне процесса или компьютера необходимо найти способ устранить его или использовать область ограниченного выполнения (CER). Обратите внимание, что любая библиотека, общее состояние которой не определено и не исправлено, может привести к сбою ведущего приложения (например, SQL Server), которое требует чистой выгрузки AppDomain.

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

Блокировки не работают на уровне процесса или между доменами приложений.

В прошлом для создания глобальных блокировок процессов использовались Enter и оператор lock. Например, это происходит при блокировке гибких классов AppDomain, таких как экземпляры Type из не являющихся общими сборок, объекты Thread, интернированные строки, а также некоторые строки, совместно используемые между доменами приложений посредством удаленного взаимодействия. Теперь эти блокировки не реализуются на уровне процесса. Чтобы определить наличие блокировки на уровне процесса между доменами, выясните, использует ли код в блокировке какой-либо внешний постоянный ресурс, например файл на диске или базу данных.

Обратите внимание, что блокировка внутри AppDomain может привести к проблемам в том случае, если защищенный код использует внешний ресурс. Это связано с тем, что такой код может одновременно выполняться в нескольких доменах приложений. Это может приводить к проблемам при записи в один файл журнала или привязке к сокету для всего процесса. В связи с этими изменениями не существует простого способа выполнить глобальную блокировку процесса с использованием управляемого кода, кроме подхода с применением именованного мьютекса Mutex или экземпляра Semaphore. Создайте код, который не будет одновременно выполняться в двух доменах приложений, либо используйте классы Mutex или Semaphore. Если изменить существующий код нельзя, не используйте для синхронизации именованный мьютекс Win32, поскольку при работе в режиме волокон не гарантируется, что получать и высвобождать такой мьютекс будет один и тот же поток операционной системы. Используйте класс Mutex или именованные объекты ManualResetEvent, AutoResetEvent или Semaphore для синхронизации блокировки кода поддерживаемым средой CLR способом вместо того, чтобы использовать для этого неуправляемый код.

Избегайте использования блокировки lock(typeof(MyType))

Проблемы могут быть связаны с частными и открытыми объектами Type в общих сборках, где все домены приложений используют одну копию кода. Для общих сборок существует только один экземпляр Type на процесс. Это значит, что несколько доменов приложений используют один и тот же экземпляр Type. Блокировка Type применяется ко всему процессу, а не только к AppDomain. Если один AppDomain блокирует Type, то в случае неожиданного прерывания потока блокировка не будет снята. Это может привести к взаимоблокировкам других доменов приложений.

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

private static Object s_InternalSyncObject;
private static Object InternalSyncObject
{
    get
    {
        if (s_InternalSyncObject == null)
        {
            Object o = new Object();
            Interlocked.CompareExchange(
                ref s_InternalSyncObject, o, null);
        }
        return s_InternalSyncObject;
    }
}

Чтобы получить заблокированный объект в случае блокировки, используйте свойство InternalSyncObject. Если в конструкторе класса был инициализирован внутренний объект синхронизации, использовать это свойство не нужно. Код для двойной проверки инициализации блокировки должен иметь следующий вид:

public static MyClass SingletonProperty
{
    get
    {
        if (s_SingletonProperty == null)
        {
            lock(InternalSyncObject)
            {
                // Do not use lock(typeof(MyClass))
                if (s_SingletonProperty == null)
                {
                    MyClass tmp = new MyClass(…);
                    // Do all initialization before publishing
                    s_SingletonProperty = tmp;
                }
            }
        }
        return s_SingletonProperty;
    }
}

Примечание о блокировке (это)

Как правило, блокировка отдельного общедоступного объекта допускается. Тем не менее для одноэлементного объекта, который может привести к взаимоблокировке всей подсистемы, рекомендуется также использовать приведенный выше шаблон разработки. Например, блокировка одного объекта SecurityManager может привести к взаимоблокировке в AppDomain, в результате чего весь AppDomain будет непригоден для использования. Блокировать общедоступные объекты такого типа не рекомендуется. Тем не менее в большинстве случаев при блокировке отдельных коллекций или массивов проблем не возникает.

Правило анализа кода

Не блокируйте типы, которые могут использоваться между доменами приложений или не имеют строгой идентификации. Не вызывайте Enter для Type, MethodInfo, PropertyInfo, String, ValueType, Thread или любых объектов, производных от MarshalByRefObject.

Удаление GC. Вызовы KeepAlive

В существующем коде KeepAlive чаще всего не используется или используется не там, где это необходимо. После преобразования в SafeHandle классам не нужно вызывать KeepAlive, поскольку предполагается, что они не имеют собственного метода завершения и используют SafeHandle для завершения дескрипторов операционной системы. Несмотря на пренебрежимо малое снижение производительности при сохранении вызова KeepAlive, понимание того, что вызов KeepAlive необходим или достаточен для решения проблемы со временем существования, которая более не существует, делает код более сложным в обслуживании. Однако при использовании COM-взаимодействия в коде по-прежнему требуются вызываемые оболочки времени выполнения (RCW) среды CLR, KeepAlive.

Правило анализа кода

Удалите KeepAlive.

Использование атрибута HostProtection

Атрибут защиты ведущего приложения HostProtectionAttribute (HPA) позволяет использовать декларативные операции безопасности для определения требований ведущего приложения к защите. Это позволяет ведущему приложению запретить даже полностью доверенному коду вызывать некоторые методы, которые не поддерживаются таким ведущим приложением, например Exit или Show для SQL Server.

Атрибут защиты ведущего приложения действует только для неуправляемых приложений, на которых размещена среда CLR и реализована защита ведущего приложения, например для SQL Server. Если этот атрибут применен, операция безопасности приводит к созданию запроса ссылки на основе ресурсов ведущего приложения, доступ к которым предоставляет класс или метод. Если код выполняется в клиентском приложении или на сервере без защиты ведущего приложения, этот атрибут не обнаруживается и, соответственно, не применяется.

Внимание

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

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

Этот атрибут определяет следующее:

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

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

  • Методы и классы, которые не соответствуют требованиям модели программирования ведущих приложений и могут привести к дестабилизации самого процесса сервера.

Примечание.

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

Дополнительные сведения об атрибуте защиты ведущего приложения см. в разделе HostProtectionAttribute.

Правило анализа кода

Для SQL Server все методы, реализующие синхронизацию или работу с потоками, должны определяться с использованием атрибута защиты ведущего приложения. К ним относятся методы, которые совместно используют состояние, синхронизируются или управляют внешними процессами. На SQL Server влияют такие значения HostProtectionResource, как SharedState, Synchronization и ExternalProcessMgmt. Тем не менее с использованием атрибута защиты ведущего приложения необходимо определять любой метод, который предоставляет HostProtectionResource, а не только методы, которые используют ресурсы, влияющие на SQL.

Не блокируйте неограниченное время в неуправляемом коде

Использование блокировки в неуправляемом коде вместо управляемого может стать причиной атаки типа "отказ в обслуживании", поскольку в этом случае среде CLR не удастся прервать поток. Из-за заблокированного потока среда CLR не сможет выгрузить AppDomain, не используя некоторые крайне небезопасные операции. Блокировка с помощью примитива синхронизации Windows является четким примером того, что мы не можем разрешить. Блокировка при вызове ReadFile сокета должна быть устранена, если это возможно. В идеале API Windows должен обеспечить механизм для такой операции, как это время ожидания.

В идеальном случае любой метод, вызывающий машинный код, должен использовать вызов Win32 с обоснованным конечным временем ожидания. Если время ожидания может задаваться пользователем, следует запретить установку неограниченного времени ожидания без наличия особых разрешений безопасности. Как правило, если метод выполняет блокировку более 10 секунд, следует использовать версию с поддержкой времени ожидания или реализовать дополнительную поддержку среды CLR.

Ниже приведены некоторые примеры проблемных API. Анонимные и именованные каналы могут создаваться с поддержкой времени ожидания. Тем не менее в коде необходимо гарантировать защиту от вызовов CreateNamedPipe или WaitNamedPipe с NMPWAIT_WAIT_FOREVER. Кроме того, непредвиденная блокировка может возникать даже в том случае, если указано время ожидания. Вызов WriteFile для анонимного канала устанавливает блокировку до тех пор, пока не будут записаны все байты. В этом случае, если в буфере содержатся непрочитанные данные, вызов WriteFile будет удерживать блокировку до тех пор, пока модуль чтения не освободит достаточно места в буфере канала. Для работы с сокетами всегда следует использовать API, которые учитывают механизм времени ожидания.

Правило анализа кода

Блокировка без учета времени ожидания в неуправляемом коде приводит к атаке типа "отказ в обслуживании". Не выполняйте вызовы неуправляемого кода для WaitForSingleObject, WaitForSingleObjectEx, WaitForMultipleObjects, MsgWaitForMultipleObjects и MsgWaitForMultipleObjectsEx. Не используйте NMPWAIT_WAIT_FOREVER.

Определение любых функций, зависимых от STA

Определите код, в котором используются однопотоковые подразделения (STA) модели COM. В процессе SQL Server однопотоковые подразделения отключены. Для SQL Server необходимо отключить функции, которые зависят от CoInitialize, такие как счетчики производительности или буфер обмена.

Убедитесь, что средства завершения не являются проблемами синхронизации

В будущих версиях платформы .NET Framework может существовать несколько потоков методов завершения. Это значит, что методы завершения для разных экземпляров одного типа могут выполняться параллельно. Они не обязаны быть полностью потокобезопасными, поскольку использование сборщика мусора гарантирует, что для конкретного экземпляра объекта метод завершения будет выполняться только в одном потоке. Тем не менее в коде методов завершения должны исключаться состояния гонки или взаимоблокировки при одновременном выполнении для нескольких разных экземпляров объектов. При использовании любого внешнего состояния (например, запись в файл журнала) в методе завершения должны обрабатываться возможные проблемы с потоками. Реализация потокобезопасности не должна полагаться исключительно на функции завершения. Не используйте для хранения состояния в потоке метода завершения локальное по отношению к потоку хранилище (как управляемое, так и собственное).

Правило анализа кода

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

Избегайте неуправляемой памяти, если это возможно

Как и в случае с дескрипторами операционной системы, при работе с неуправляемой памятью возможны утечки. По возможности попробуйте использовать память в стеке с помощью stackalloc или закрепленного управляемого объекта, например фиксированной инструкции или GCHandle с помощью байта[]. В конечном итоге, они будут очищены с помощью GC. Тем не менее, если вам необходимо выделить неуправляемую память, рекомендуется заключать операции выделения памяти в класс, который является производным от SafeHandle.

Обратите внимание, что SafeHandle не подходит как минимум в одном случае. Для вызовов методов COM, которые выделяют или высвобождают память, чаще всего одна библиотека DLL выделяет память с помощью CoTaskMemAlloc, после чего другая библиотека DLL высвобождает эту память, используя CoTaskMemFree. В таких случаях использовать SafeHandle не следует, поскольку это приведет к попытке привязать срок существования неуправляемой памяти к сроку существования SafeHandle, тогда как он должен управляться другой библиотекой DLL.

Просмотрите все использование catch(Exception)

Блоки catch, которые перехватывают все исключения вместо одного конкретного, теперь также могут перехватывать асинхронные исключения. Проверьте каждый блок catch(Exception) и убедитесь, что не пропущен важный код высвобождения ресурсов или возврата, а в самом блоке catch отсутствуют потенциально некорректные функции обработки ThreadAbortException, StackOverflowException или OutOfMemoryException. Обратите внимание, что этот код может записывать информацию в журнал или использовать определенные допущения, связанные с видимостью только некоторых исключений или наличием единственной причины сбоя при возникновении исключения. Для работы с ThreadAbortException эти допущения может потребоваться обновить.

Везде, где перехватываются все исключения, рекомендуется реализовать перехват исключений конкретного типа, которые могут возникать в указанном месте (например, исключений FormatException из методов форматирования строк). Это позволяет предотвратить выполнение блока catch для непредвиденных исключений и гарантировать отсутствие в коде ошибок, скрываемых в результате перехвата таких исключений. В большинстве случаев не следует обрабатывать исключения в коде библиотеки (такая необходимость может свидетельствовать об определенных недостатках в вызываемом коде). Иногда для вывода дополнительных данных требуется перехватить одно исключение и вызвать исключение другого типа. В этом случае следует использовать вложенные исключения, сохраняя сведения о фактической причине сбоя в свойстве InnerException нового исключения.

Правило анализа кода

Проверьте все блоки catch в управляемом коде, которые перехватывают все объекты или исключения. В C#это означает, что примечание обоих catch{} и catch(Exception){}. Рекомендуется как можно точнее указывать тип исключения или тщательно проверить код на корректность поведения при перехвате исключения непредвиденного типа.

Не предполагайте, что управляемый поток является потоком Win32 — это волокна

Вы можете использовать управляемое локальное по отношению к потоку хранилище, однако нельзя использовать аналогичное неуправляемое хранилище или предполагать, что код будет снова выполняться в текущем потоке операционной системы. Не изменяйте такие параметры, как языковой стандарт потока. Не вызывайте InitializeCriticalSection или CreateMutex посредством вызова неуправляемого кода, поскольку в этом случае устанавливать и снимать блокировку должен один и тот же поток операционной системы. Поскольку при работе с волокнами этого нет, в SQL нельзя напрямую использовать мьютексы и критические секции Win32. Обратите внимание, что управляемый класс Mutex не решает эти проблемы, связанные со сходством потоков.

Вы можете безопасно использовать большую часть состояния управляемого объекта Thread, включая управляемое локальное по отношению к потоку хранилище и текущий языковой стандарт пользовательского интерфейса потока. Кроме того, вы можете использовать ThreadStaticAttribute, в результате чего значение существующей статической переменной будет доступно только текущему управляемому потоку (это один из способов реализовать локальное по отношению к волокну хранилище в среде CLR). Из-за ограничений модели программирования при работе в SQL изменять текущий языковой стандарт потока нельзя.

Правило анализа кода

SQL Server работает в режиме волокон. Не используйте локальное по отношению к потоку хранилище. Не рекомендуется выполнять вызовы неуправляемого кода для TlsAlloc, TlsFree, TlsGetValue и TlsSetValue.

Разрешить SQL Server обрабатывать олицетворение

Поскольку олицетворение реализуется на уровне потока, а SQL может работать в режиме волокон, в управляемом коде не должно осуществляться олицетворение пользователей и не должны использоваться вызовы RevertToSelf.

Правило анализа кода

Обработку олицетворения должен осуществлять SQL Server. Не используйте RevertToSelf, ImpersonateAnonymousToken, DdeImpersonateClient, ImpersonateDdeClientWindow, ImpersonateLoggedOnUser, ImpersonateNamedPipeClient, ImpersonateSelf, RpcImpersonateClient, RpcRevertToSelf, RpcRevertToSelfEx или SetThreadToken.

Не вызывайте Thread::Suspend

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

Правило анализа кода

Не вызывайте Suspend. Рекомендуется использовать реальный примитив синхронизации вместо Semaphore или ManualResetEvent.

Защита критически важных операций с ограниченными регионами выполнения и контрактами надежности

При выполнении сложной операции, которая обновляет общее состояние или должна детерминированно завершаться полным успехом или сбоем, используйте в целях защиты ограниченную область выполнения (CER). Таким образом, выполнение кода гарантируется во всех случаях, даже при неожиданном прерывании потока или выгрузки AppDomain.

Ограниченная область выполнения представляет собой отдельный блок try/finally, непосредственно перед которым выполняется вызов PrepareConstrainedRegions.

В этом случае JIT-компилятор получает инструкции подготовить весь код в блоке finally до того, как будет выполнен код в блоке try. Таким образом, код в блоке finally будет построен и выполнен в любом случае. В ограниченных областях выполнения пустые блоки try практически не используются. С помощью ограниченных областей выполнения можно реализовать защиту от асинхронного прерывания потоков и исключений, связанных с нехваткой памяти. В разделе ExecuteCodeWithGuaranteedCleanup показана форма ограниченной области выполнения, которая дополнительно обрабатывает случаи переполнения стека для кода большой глубины.

См. также