Шаблон синглтон в пуле приложений с множеством рабочих потоков
Данная статья посвящена «межрабочепоточным» решениям по реализации шаблона синглтон в ASP.NET.
Введение
Синглтон (тип, допускающий создание только одного своего экземпляра в каждом домене приложения) – очень полезный шаблон, находящий своей применение почти в каждом большом проекте – особенно если это многопоточное многопользовательское приложение. Создавать и применять синглтоны в веб-среде при разработке веб-приложений на C# в каркасе ASP.NET не всегда просто.
В веб-приложениях используются три вида синглтонов:
• Один экземпляр на каждый веб-запрос
• Один экземпляр на каждого пользователя (сессию)
• Один экземпляр на все веб-приложение
Первые два случая не являются проблемой; один экземпляр на каждый веб-запрос часто нельзя назвать синглтоном, но это зависит от сложности веб-запроса. Наиболее интересен третий пункт “Один экземпляр на все веб-приложение”, являющийся трудным.
Обычно стандартный шаблон синглтон применяется, когда по умолчанию есть только один рабочий процесс. Проблема заключается в том, как реализовать синглтон в среде с множеством рабочих потоков. При наличии единственного рабочего потока реализовать синглтон просто, так как все веб-запросы будут совместно использовать его статический экземпляр по всему рабочему потоку; единственная проблема – как защитить его, поскольку среда все же многопоточная, и во избежание неприятностей можно добавить "lock", как в примере ниже.
public class Singleton {
static Singleton instance = null;
static readonly object padlock = new object();
Singleton() { }
public static Singleton Instance {
get {
lock (padlock) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
}
}
Это будет хорошо работать при использовании одного рабочего потока в пуле приложений, но в наши дни большинство текущих серверных платформ имеют многоядерные процессоры, и при одном рабочем потоке они не используются по максимуму. Другой минус наличия одного рабочего потока заключается в том, что когда один из запросов работает медленно, от этого непосредственно пострадают все прочие запросы. Чтобы масштабировать производительность, надо повысить количество рабочих потоков. Но, к сожалению, статические переменные совместно используются только через один рабочий поток, следовательно, получилось бы много синглтонов, один на каждый поток – что иногда допустимо, но часто становится серьезной проблемой (например, могут быть нарушения доступа) и, в конце концов, это уже не соблюдает шаблон синглтон.
Синглтон дистанционной связи .NET
Ярким примером синглтона, для которого несколько рабочих потоков создают проблемы, является класс «Регистратор файла» – когда каждый запрос записывает некоторые данные в один и тот же файл. Вообразите, что произошло бы, если бы два синглтона одновременно попытались писать в один и тот же файл.
Чтобы решить эту проблему, надо осуществлять связь между рабочими потоками, чтобы все они обращались к синглтону, существующему только в одном из них. Чтобы достичь этого, можно использовать систему дистанционной связи .NET
Идея в том, что канал HTTP-сервера синглтона будет создан при первой попытке вызвать метод GetInstance(), стало быть, имеется полное отложенное создание экземпляра (как в первом примере кода). Если канал сервера уже создан (что можно выяснить путем захвата исключения сокета: второй блок кода), рабочий поток попытается подключиться к нему, чтобы создать посредника, позволяющего обращаться к синглтону.
try {
channel = new HttpChannel(8089);
ChannelServices.RegisterChannel(channel, false);
RemotingConfiguration.RegisterWellKnownServiceType(typeof(Singleton),
"Singleton", WellKnownObjectMode.Singleton);
instance = (Singleton)Activator.GetObject(typeof(Singleton),
"http://localhost:8089/Singleton");
} catch (SocketException) {
channel = new HttpChannel();
ChannelServices.RegisterChannel(channel, false);
instance = (Singleton)Activator.GetObject(typeof(Singleton),
"http://localhost:8089/Singleton");
}
В этом примере используются жестко закодированные значения для номера порта и имени службы – однако лучше хранить их в Web.Config.
Итак, если на сервере уже был создан экземпляр канала сервера на конкретном номере порта, то new HttpChannel(8089) сгенерирует исключение сокета, следовательно, пропустите часть создания сервера и попытайтесь получить текущий экземпляр от существующего канала.
Может быть проблема, если канал создан, но синглтон еще не зарегистрирован полностью. В случае если другой рабочий поток попытается подключиться к нему, то потерпит неудачу. Эту проблему трудно решить, поэтому не создавайте огромные конструкторы. Если есть долго выполняющийся конструктор, то выясните наихудший вариант развития событий и добавьте цикл с задержкой, чтобы иметь возможность выделить немного времени, если синглтон еще не создан (третий блок кода).
for (int i = 0; i <= 6; i++) {
try {
instance = (Singleton)Activator.GetObject(typeof(Singleton),
"http://localhost:8089/Singleton");
break;
} catch (RemotingException) {
Thread.Sleep(300);
}
}
Другая проблема – что делать, когда главный рабочий поток (создавший синглтон и канал сервера) исчезает?
Настройки пула приложений IIS позволяют задать время простоя, и можно максимально увеличить его во избежание вышеназванного. Но это не лучшая идея, так как если в конкретно рабочем потоке произойдет ошибка, он разрушится – и синглтон будет утрачен, и посредники в других потоках будут ссылаться на несуществующий объект.
Чтобы решить эту проблему, надо проверять действительность подключения и объекта перед возвратом экземпляра вызывающей функции GetInstance(). Для достижения этого создается пустой метод CheckConnection() (пример ниже), возвращающий логическое значение. Если этот метод генерирует исключение сокета, то ссылка больше недействительна.
public bool CheckConnection() {
return true;
}
Если стало известно, что подключение к серверу синглтона исчезло, то создается новое подключение в текущем рабочем потоке.
Некоторые мысли в дополнение
Конечно, это не единственное решение данной проблемы. Можно создать службу Windows с каким-то интерфейсом, веб-службы, работающие на одном рабочем потоке, с которыми приложение может установить связь, и многое другое. Главное преимущество описанного решения - удобство эксплуатации. Не надо создавать другой пул приложений, веб-службы, или регистрировать и следить, чтобы служба Windows работала правильно. Это решение также очень надежно; большинство исключений обрабатывается в методе GetInstace(). Решение удобно использовать с позиции программиста – не надо инициировать подключение или проверять, что уже есть некоторый экземпляр, созданный где-то, поскольку все это осуществляет метод GetInstace().
Все перечисленные преимущества очень выгодны в условиях, когда приходится поддерживать множество маленьких и средних приложений. Можно утверждать, что дистанционная связь .NET не является лучшим решением для синглтонов с позиции быстродействия, но имеет место компромисс: приложение, работающее на несколько рабочих потоков, гораздо более масштабируемо, но в некоторых частях приложения приходится использовать дистанционную связь. Конечно, вам решать, сказывается она на быстродействии или нет. Вероятно, дистанционная связь работала бы быстрее через двоичные каналы TCP или IPC, но, к сожалению, IIS поддерживает только HTTP.