Асинхронные HttpWebRequest, реализация интерфейсов и др. (исходники)

Источник: MSDN Magazine
Стивен Тауб (Stephen Toub)

Вопрос: Мы с заказчиком работаем над клиентским приложением, которое передает данные серверному приложению, выдавая запросы HttpWebRequest. Нам нужно было ограничивать число одновременных соединений, открываемых клиентом, чтобы регулировать нагрузку на сервер. Поначалу мы пытались делать запросы к серверу из потоков ThreadPool, но постоянно получали исключения из-за нехватки потоков. У меня два вопроса. Во-первых, почему в ThreadPool кончаются потоки, разве ThreadPool не должен блокировать выполнение рабочих элементов в очереди, пока в пуле не появятся свободные потоки? И, во-вторых, как регулировать число одновременных соединений, если не удается делать это через ThreadPool?

Ответ: Отличные вопросы, над которыми, судя по результатам беглого поиска в Web, бились многие разработчики. Во-первых, имейте в виду, что в .NET Framework 1.x запросы через HttpWebRequest всегда выполняются асинхронно. Что я имею в виду, спросите вы? Чтобы понять это, взгляните на код HttpWebRequest.GetResponse в Shared Source CLI (SSCLI) (msdn.microsoft.com/net/sscli).

Ниже дана выдержка из него, в которой опущена проверка на получение ответов и на таймауты:

public override WebResponse GetResponse() {
...
IAsyncResult asyncResult = BeginGetResponse(null, null);
...
return EndGetResponse(asyncResult);
}

Как видите, HttpWebRequest.GetResponse - просто оболочка парных методов BeginGetResponse и EndGetResponse. Это асинхронные методы, т.е. BeginGetResponse в действительности выдает HTTP-запрос из потока, отличного от того, откуда его вызвали, а EndGetResponse блокируется до завершения запроса. В итоге HttpWebRequest ставит в очередь ThreadPool рабочий элемент для каждого исходящего запроса. Итак, HttpWebRequest использует потоки ThreadPool, но почему вызов GetResponse из потока ThreadPool вызывает проблемы? Причина - взаимная блокировка потоков.

Как вы заметили в своем вопросе, рабочие элементы в очереди ThreadPool ожидают появления свободных потоков, которые смогут их обработать. Для примера предположим, что пул ThreadPool содержит единственный поток (хотя по умолчанию их намного больше). Вы ставите в очередь метод, вызывающий HttpWebRequest.GetResponse, который начинает выполняться единственным потоком ThreadPool. Далее GetResponse вызывает BeginGetResponse, а тот ставит в очередь ThreadPool рабочий элемент и вызывает EndGetResponse, который должен ждать завершения обработки этого рабочего элемента. Увы, этому не суждено случиться. Рабочий элемент, помещенный в очередь методом BeginGetResponse, не будет выполнен, пока не освободится поток ThreadPool, но един-ственный поток из пула сейчас занят обработкой вызова EndGetResponse, а тот ожидает завершения HTTP-запроса - вот вам и взаимная блокировка. Вы можете возразить, что пример с единственным потоком в пуле - наду-манный, поскольку в действительности потоков много. Хорошо, давайте расширим наш пример. Что, если в очередь пула из двух потоков достаточно быстро поместить два метода, вызывающих GetResponse до завершения обработ-ки рабочего элемента хотя бы одного из них? Результат тот же - взаимная блокировка. И даже если в пуле 25 потоков, то, быстро поставив в очередь 25 таких методов, вы снова получите взаимную блокировку. Ясно, в чем проблема?

Чтобы решить ее, разработчики .NET Framework реализовали исключение, с которым вам пришлось столкнуться. При завершении BeginGetResponse, непосредственно перед постановкой в очередь рабочего элемента, для проверки числа свободных потоков в пуле вызывается System.Net.Connection.IsThreadPoolLow. Если потоков мало (меньше двух), генерируется исключение InvalidOperationException.

К счастью, в .NET Framework 2.0 синхронные запросы посредством HttpWebRequest.GetResponse действительно выполняются синхронно. В результате эта проблема не возникает, и можно ставить в очередь сколько угодно методов, вызывающих GetResponse. А как же быть тем, кто пользуется версией 1.x? Одно решение вы уже назвали, оно состоит в том, чтобы явно регулировать число рабочих элементов, ожидающих в очереди ThreadPool и выполняемых его потоками. Для реализации этого подхода нужен механизм, который позволит следить за числом ожидающих рабочих элементов и блокировать новые запросы по достижении заданного предела.

Семафоры - это синхронизирующие примитивы со счетчиком, принимающим значения от нуля до заданного предела. Значение счетчика семафора можно увеличивать и уменьшать, а хитрость в том, что поток, попытавшийся уменьшить значение счетчика, и так равного нулю, блокируется, пока другой поток не увеличит значение счетчика. Такие свойства делают семафоры идеальным средством в некоторых ситуациях. О первой и, наверное, самой распространенной рассказывают студентам, изучающим архитектуру ОС: это модель производителя и потребителя (producer/consumer model). В ней потоки-производители генерируют некий продукт, который затем используют потоки-потребители. Вторая и, возможно, более общая ситуация включает управление доступом к общим ресурсам, поддер-живающим ограниченное число пользователей. В подобных ситуациях семафоры играют роль охранника, блокирующего попытки обращения к ресурсу, когда максимальное число пользователей уже достигнуто. Может, семафоры подойдут и в нашем случае?

Так оно и есть, но семафоры, увы, не реализованы в .NET Framework 1.x. Впрочем, чтобы создать простую оболочку для Win32-семафора достаточно несколько строк кода (рис. 1), только учтите, что в .NET Framework 2.0 уже есть реализация семафоров, куда более полная, чем показанная здесь. Атрибут DllImport используется с P/Invoke для импорта из kernel32.dll функций CreateSemaphore (Win32-функция, создающая объект-семафор) и ReleaseSemaphore (Win32-функция, увеличивающая счетчик семафора). Для ссылки на объект-семафор служит описатель, возвращаемый CreateSemaphore. Это дает прекрасную возможность создать производный класс Semaphore от WaitHandle, чтобы воспользоваться его поддержкой ожидания на синхронизирующих объектах (например методом WaitOne). Вызов WaitOne производного класса уменьшит значение счетчика семафора. Если значение этого счетчика уже равно нулю, вызывающий поток будет заблокирован, пока значение счетчика не увеличится или не пройдет заданное время.

На основе этого семафора несложно создать класс, регулирующий число элементов в очереди ThreadPool; пример такого класса показан на рис. 2. Начальное (оно же максимальное) значение счетчика семафора равно максимально допустимому числу одновременно обрабатываемых запросов. При вызове ThreadPoolThrottle.QueueUserWorkItem вызывается метод WaitOne семафора, который уменьшает значение счетчика. Если максимальное число одновременных запросов уже достигнуто, выполнение метода блокируется. Как и в классе ThreadPoolWait, описанном в октябрьском выпуске этой рубрики за 2004 г. (см. msdn.microsoft.com/msdnmag/issues/04/10/NetMatters), указанный пользователем WaitCallback вместе с его состоянием упаковывается в новый объект состояния, который помещается в очередь ThreadPool как состояние закрытого метода HandleWorkItem. При вызове ThreadPool метод HandleWorkItem вызывает заданный пользователем делегат с пользовательским же состоянием. После этого он увеличивает счетчик семафора, оповещая о завершении обработки своего рабочего элемента, а это позволяет следующему заблокированному потоку пробудиться и продолжить обработку своего запроса. При условии, что больше никто не использует потоки из пула (и не уменьшает число доступных потоков), класс вроде ThreadPoolThrottle позволит успешно регулировать число рабочих элементов в очереди.

Вопрос Я пишу инструмент, использующий отражение. Этот инструмент должен по методу класса определять реализованный в нем метод интерфейса или сообщать, что интерфейсы в данном классе не реализованы. Возможно ли это?

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

interface SomeInterface1 {
void Method1();
}
interface SomeInterface2 {
void Method1();
}
class SomeClass : SomeInterface1, SomeInterface2 {
public void Method1(){}
}

Здесь в SomeClass.Method1 неявно реализованы методы двух интерфейсов, SomeInterface1.Method1 и SomeInterface2.Method1. C# поддерживает два способа сопоставления методов класса методам интерфейса. Первый состоит в явной реализации члена интерфейса, при этом имя члена класса указывают вместе с именами интерфейса и его метода, например:

class SomeClass : SomeInterface1
{
void SomeInterface1.Method1() {}
}

Такая реализация будет доступна как открытый метод, но только через этот интерфейс. Второй способ - неявная реализация, в которой открытые методы (кроме статических) сопоставляют методам интерфейса, указывая их имена, типы и списки формальных параметров. В принципе, C# допускает реализацию в методе класса нескольких методов различных интерфейсов (см. пример выше), но запрещает реализацию в одном методе класса разных методов одного интерфейса. Причина - в запрете на существование нескольких методов одного интерфейса с одинаковым именем, типом и формальными параметрами, что необходимо для сопоставления методу класса нескольких методов одного интерфейса. В Visual Basic .NET эта проблема решается принуждением разработчика к явному указанию методов интерфейса, реализованных в методе класса. Например, в Visual Basic .NET допускается реализация в одном методе класса двух методов одного и того же интерфейса:

Interface SomeInterface
Sub Method1()
Sub Method2()
End Interface
Class SomeClass
Implements SomeInterface
Public Sub SomeMethod() Implements _
SomeInterface.Method1, SomeInterface.Method2
Console.WriteLine("SomeMethod called.")
End Sub
End Class

Здесь в SomeClass.SomeMethod реализованы оба метода - SomeInterface.Method1 и SomeInterface.Method2.

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

У класса Type имеется метод GetInterfaceMap, который принимает объект Type, представляющий интерфейс, и возвращает сопоставление между заданным интерфейсом и методами класса, где этот интерфейс реализован. InterfaceMap (объект, возвращаемый GetInterfaceMap) содержит два важных поля: TargetMethods и InterfaceMethods, в которых хранят

ся массивы. В первом поле находится массив объектов MethodInfo, представляющих методы типа, реализующих методы интерфейса, а во втором - массив методов интерфейсов, соответствующих элементам первого массива. Так, метод, представленный объектом MethodInfo из массива TargetMethods[2], реализует метод интерфейса, представленный аналогичным объектом из InterfaceMethods[2].

Метод GetImplementedInterfaces, показанный на рис. 3, принимает объект MethodInfo, представляющий метод, и возвращает массив объектов Type, каждый из которых представляет один интерфейс, реализованный в заданном методе. GetImplementedInterfaces начинает с получения значения ReflectedType для метода, имя которого передано как аргумент. Это нужно для получения всех интерфейсов, реализованных в данном типе, а также их методов, сопоставленных методам этого типа. Возможно, вы заметили, что у класса MemberInfo (это предок MethodInfo) помимо ReflectedType есть свойство DeclaringType. Здесь важно использовать именно ReflectedType. Разница между ними в том, что DeclaringType возвращает тип, в котором объявлен данный член, а ReflectedType - тип, для которого получен данный MethodInfo. Чем отличаются эти типы и почему это так важно? Дело в том, что для каждого из интерфейсов, реализованного в классе-контейнере метода, мне требуется перебрать в цикле все целевые методы, указанные в карте данного интерфейса, чтобы найти метод, соответствующий заданному. Найденное совпадение указывает на реализованный в классе метод интерфейса. Но иногда MethodInfo метода, в котором реализован метод интерфейса, не соответ-ствует ни одному из MethodInfo, хранящихся в массиве TargetMethods. Так бывает, когда DeclaringType отличается от ReflectedType. Рассмотрим следующий пример:

interface SomeInterface {
void Method1();
}
class Base : SomeInterface {
public void Method1() {}
}
class Derived : Base {}

В действительности Derived.Method1 реализован в классе Base, поэтому команды:

typeof(Derived).GetMethod("Method1")

и

typeof(Base).GetMethod("Method1")

вернут разные объекты MethodInfo.

Если бы я, вызывая GetInterfaceMap из базового типа (в данном случае это тип, где объявлен Method1), получал MethodInfo как typeof(Derived), моему MethodInfo не соответствовал бы ни один объект в массиве TargetMethods. Так что важно вызывать GetInterfaceMap из того типа, для которого был получен MethodInfo (этот тип возвращается свойством ReflectedType объекта MethodInfo).

Вопрос Недавно я читал о моделях потоков в COM. Хотелось бы узнать, как вписывается в эту картину .NET. Знаю, что задать исходную модель потоков для основного потока приложения можно при помощи атрибутов STAThread и MTAThread, а свойство Thread.ApartmentState позволяет сменить ее в дальнейшем, но, как я понимаю, все это работает до первого вызова COM-объекта, после чего никакие изменения этих параметров невозможны. Так ли это? Если да, то как применять несколько COM-объектов, требующих разных моделей потоков?

Ответ Свойство Thread.ApartmentState можно менять сколько угодно, но только до первого вызова COM-объекта. После этого сменить модель для основного потока не удастся. Стандартное решение для работы с COM-объектами, которым нужна модель потоков, отличная от используемой основным потоком, заключается в следующем. Создайте новый поток с соответственно заданным свойством ApartmentState и выполняйте код, использующий такой COM-объект, в новом потоке [чтобы освежить в памяти вопросы, связанные с моделями потоков в COM, читайте Web-журнал Ларри Остермана (Larry Osterman) на blogs.msdn.com/larryosterman/archive/2004/04/28/122240.aspx].

Чтобы слегка упростить этот процесс, я написал класс, показанный на рис. 4. У класса ApartmentState-Switcher имеется единственный статический метод Execute. Этот метод принимает вызываемый делегат, его параметры и значение ApartmentState, с которым следует выполнить данный делегат. Если значение ApartmentState текущего потока совпадает с тем, что задал пользователь при вызове Execute, этот метод просто вызывает делегат через его метод DynamicInvoke, поддерживающий вызов методов с поздним связыванием. Если же задано иное значение ApartmentState, вызов делегата осуществляется в новом потоке с соответствующим значением ApartmentState. Для этого создается небольшой объект, который служит для передачи состояния между текущим и новым потоками. В него записываются делегат и его параметры.

После этого создается поток с соответствующим значением ApartmentState, далее этот поток запускается и немедленно присоединяется к текущему потоку, блокируя его до завершения своего выполнения. Главным методом нового потока является Run, закрытый метод класса, выполняющий делегат с переданными параметрами. Возвращаемые делегатом значения и сгенерированные в период его выполнения исключения записываются в объект состояния (в .NET Framework 1.x исключения, сгенерированные в рабочих потоках, исполняющая среда просто «проглатывает», а в версии 2.0 эти исключения приводят к уничтожению AppDomain, но нигде исключения не передаются основному потоку, поэтому приходится делать это вручную). По завершении рабочего потока метод Thread.Join завершается и производится проверка исключений и возвращенных значений, сохраненных в классе состояния. Найденные исключения генерируются заново, а если исключений нет, просто возвращается значение, которое вернул делегат.

Чтобы задействовать этот класс, достаточно создать делегат для кода, который вам требуется выполнить (а это проще всего делать с помощью анонимных делегатов C# 2.0), и передать его ApartmentStateSwitcher.Execute, как показано ниже:

[MTAThread]
static void Main(string[] args) {
PrintCurrentState();
ApartmentStateSwitcher.Execute(
new ThreadStart(PrintCurrentState), null,
ApartmentState.STA);
PrintCurrentState();
}
static void PrintCurrentState() {
Console.WriteLine("Thread apartment state: " +
Thread.CurrentThread.ApartmentState);
}

Этот код выводит на консоль следующее сообщение:

Thread apartment state: MTAThread apartment state: STAThread apartment state: MTA


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=5743