Асинхронні HttpWebRequest, реалізація інтерфейсів і ін (исходники), HTML, XML, DHTML, Інтернет-технології, статті

Питання: Ми з замовником працюємо над клієнтським додатком, що передає дані серверному додатку, видаючи запити 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


Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*