Редко когда веб-сервис выполняет интенсивные вычисления с привязкой к процессору. Гораздо более распространенным для бизнес-приложений является веб-сервис, который выполняет одну или несколько интенсивных операций ввода-вывода. Обычно наш веб-сервис обращается к базе данных по сети, читает и записывает файлы или, возможно, вызывает другой веб-сервис. Если мы выполняем эти операции синхронно, поток, который обрабатывает запрос веб-службы, будет проводить большую часть своего времени в ожидании ввода-вывода. Выполняя операции ввода-вывода асинхронно, мы можем освободить поток, обрабатывающий запрос, для обработки других запросов, ожидая завершения операции ввода-вывода.
В моих экспериментах с простой автономной службой 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 для запуска тестов и попробуйте сами.