Редко когда веб-сервис выполняет интенсивные вычисления с привязкой к процессору. Гораздо более распространенным для бизнес-приложений является веб-сервис, который выполняет одну или несколько интенсивных операций ввода-вывода. Обычно наш веб-сервис обращается к базе данных по сети, читает и записывает файлы или, возможно, вызывает другой веб-сервис. Если мы выполняем эти операции синхронно, поток, который обрабатывает запрос веб-службы, будет проводить большую часть своего времени в ожидании ввода-вывода. Выполняя операции ввода-вывода асинхронно, мы можем освободить поток, обрабатывающий запрос, для обработки других запросов, ожидая завершения операции ввода-вывода.
В моих экспериментах с простой автономной службой WCF я смог продемонстрировать до 7000 одновременных соединений, обрабатываемых всего 12 потоками.
Прежде чем я покажу вам, как написать асинхронную службу WCF, я хочу прояснить распространенное заблуждение (да, я тоже до года или около того назад), что асинхронные операции ввода-вывода порождают потоки. Многие из API в .NET BCL (библиотека базовых классов) предоставляют асинхронные версии своих методов. Так, например, HttpWebRequest имеет пару методов BeginGetResponse / EndGetResponse наряду с синхронным методом GetResponse. Этот шаблон называется моделью асинхронного программирования (APM). Когда APM поддерживает операции ввода-вывода, они реализуются с использованием службы операционной системы, называемой портами завершения ввода-вывода(IOCP). IOCP предоставляет очередь, где операции ввода-вывода могут быть припаркованы, пока ОС ожидает их завершения, и предоставляет пул потоков для обработки завершенных операций. Это означает, что выполняемые операции ввода-вывода не используют потоки.
Инфраструктура WCF позволяет вам определять ваши рабочие контракты, используя APM. Вот контракт для операции GetCustomer:
[ServiceContract(SessionMode = SessionMode.NotAllowed)] public interface ICustomerService { [OperationContract(AsyncPattern = true)] IAsyncResult BeginGetCustomerDetails(int customerId, AsyncCallback callback, object state); Customer EndGetCustomerDetails(IAsyncResult asyncResult); }
По сути, GetCustomerDetails принимает идентификатор клиента и возвращает клиента. Чтобы создать асинхронную версию контракта, я просто следовал шаблону APM и создал BeginGetCustomerDetails и EndGetCustomerDetails. Вы сообщаете WCF, что внедряете APM, устанавливая для AsyncPattern значение true в операционном контракте.
IAsyncResult, который возвращается из метода ‘begin’ и передается в качестве аргумента методу ‘end’, связывает их вместе. Вот простая реализация IAsyncResult, которую я использовал для этих экспериментов, вы должны иметь возможность использовать ее для любой асинхронной службы WCF:
public class SimpleAsyncResult<T> : IAsyncResult { private readonly object accessLock = new object(); private bool isCompleted = false; private T result; public SimpleAsyncResult(object asyncState) { AsyncState = asyncState; } public T Result { get { lock (accessLock) { return result; } } set { lock (accessLock) { result = value; } } } public bool IsCompleted { get { lock (accessLock) { return isCompleted; } } set { lock (accessLock) { isCompleted = value; } } } // WCF seems to use the async callback rather than checking the wait handle // so we can safely return null here. public WaitHandle AsyncWaitHandle { get { return null; } } // We will always be doing an async operation so completed synchronously should always // be false. public bool CompletedSynchronously { get { return false; } } public object AsyncState { get; private set; } }
Теперь у нас есть AsyncResult, мы можем реализовать наш сервис на основе APM, внедрив ICustomerService:
[ServiceBehavior( InstanceContextMode = InstanceContextMode.PerCall, ConcurrencyMode = ConcurrencyMode.Multiple)] public class CustomerService : ICustomerService { public const int DelayMilliseconds = 10000; public IAsyncResult BeginGetCustomerDetails(int customerId, AsyncCallback callback, object state) { var asyncResult = new SimpleAsyncResult<Customer>(state); // mimic a long running operation var timer = new System.Timers.Timer(DelayMilliseconds); timer.Elapsed += (_, args) => { asyncResult.Result = GetCustomer(customerId); asyncResult.IsCompleted = true; callback(asyncResult); timer.Enabled = false; timer.Close(); }; timer.Enabled = true; return asyncResult; } public Customer EndGetCustomerDetails(IAsyncResult asyncResult) { return ((SimpleAsyncResult<Customer>) asyncResult).Result; } private static Customer GetCustomer(int customerId) { return new Customer(customerId, "Mike_" + customerId); } }
Мы имитируем длительную операцию ввода-вывода с помощью таймера. Я считаю, что Таймер также использует порт завершения ввода-вывода, но не цитируйте меня по этому поводу. Когда WCF вызывает BeginGetCustomerDetails, мы сначала создаем новый SimpleAsyncResult с объектом состояния WCF. WCF передаст AsyncResult методу EndGetCustomerDetails после завершения таймера, поэтому мы можем использовать его для передачи любого состояния ответа. В нашем случае это экземпляр Customer.
Затем мы устанавливаем таймер и прикрепляем закрытие к событию Elapsed. Когда происходит событие Elapsed таймера, мы создаем экземпляр клиента, передаем его нашему AsyncResult, а затем передаем AsyncResult в функцию обратного вызова WCF.
После завершения метода BeginGetCustomerDetails WCF возвращает свой поток обратно в пул потоков WCF, чтобы он мог обслуживать другие запросы. Десять секунд спустя операционная система отправляет элемент в очередь IOCP, поток из пула выбирает элемент и выполняет продолжение. Это в свою очередь вызывает обратный вызов WCF, который в свою очередь выполняет EndGetCustomerDetails. Затем WCF упаковывает клиента в ответ SOAP и возвращает его клиенту.
Только небольшая часть этих 10 секунд фактически заняла поток, поэтому мы должны иметь возможность совершать тысячи одновременных вызовов этой службы.
В моих тестах этот сервис смог обработать чуть более 7000 одновременных соединений.
Код здесь: https://github.com/mikehadlow/Mike.AsyncWcf
Просто следуйте инструкциям README для запуска тестов и попробуйте сами.