И, поверьте мне, она будет невероятно интересна разработчикам таких приложений, которые могут обрабатывать несколько небыстрых одновременных запросов от одного пользователя. Показательный пример – одновременное скачивание нескольких больших файлов. Это реальная проблема, с которой столкнулись мои коллеги и на отладку которой было потрачено неадекватно много времени.
Проблема состоит, в первую очередь, в неочевидности следующего факта: в рамках одной сессии (одного SessionID) параллельные запросы выполняются по очереди, если обработчики поддерживают сессию. При этом обращений к сессии в обработчике может не быть вообще – важна только поддержка сессии и её предварительная инициализация. В нашем примере это означает то, что пока не закачается один файл, закачивание других не начнется.
В принципе, ситуация с конкурентными запросами в рамках одной сессии описана в статье MSDN ASP.NET Session State Overview. Но есть нюансы. Во-первых, это один неприметный абзац в конце статьи (вместо выделенного блока в начале). Во-вторых, MSDN не раскрывает многих неочевидных моментов. И, что важно, ни один из моих знакомых ASP.NET разработчиков не знал об этой особенности.
Заинтересовались? Тогда продолжаем. Я сформулирую несколько правил работы сессии и покажу на примере, что они верны.
Несколько запросов и сессия ASP.NET
MSDN глаголет следующее:Access to ASP.NET session state is exclusive per session, which means that if two different users make concurrent requests, access to each separate session is granted concurrently. However, if two concurrent requests are made for the same session (by using the same SessionID value), the first request gets exclusive access to the session information. The second request executes only after the first request is finished. (The second session can also get access if the exclusive lock on the information is freed because the first request exceeds the lock time-out.)То есть, получаем основные правила:
- У разных сессий с одновременными запросами все в порядке.
- Если в одной сессии будет несколько параллельных запросов, выполнятся они будут по очереди. Кто успел первым – тот выполнится до конца (или до остановки по таймауту), и только потом начнется обработка второго.
However, read-only requests for session data might still have to wait for a lock set by a read-write request for session data to clear.И повторюсь, потому что это важно – если обработчик запроса не помечен особым образом, блокируются любые запросы, даже те, которые не обращаются к сессии. Для воспроизведения блокировки параллельных запросов важно только то, что хранилище для сессии должно быть предварительно инициализировано.
Экспериментальное подтверждение правил
Чтобы не было вопросов по поводу возможных трактовок ситуации, имеет смысл написать приложение, которое воспроизводит вышеописанное поведение. А заодно можно показать, что без обращения к сессии подобных проблем не возникает.Для иллюстрации режима “только для чтения” можно реализовать HttpHandler с маркерным интерфейсом IReadOnlySessionState вместо IRequiresSessionState. Мне показался более быстрым в реализации вариант со страницами директивой @Page.
Далее подразумевается работа в Microsoft Visual Studio 2010 и .NET 4.0. Детали реализации в других версиях могут незначительно отличаться. Достаточно создать проект “ASP.NET Empty Web Application”, но вы можете добавить страницы и в существующий проект.
Отмечу также, что некоторые браузеры (например, Opera) создают разные сессии для нескольких страниц одного пользователя, поэтому пример лучше проверять с помощью Internet Explorer (в моем случае IE9).
Страница инициализации сессии
Кстати, есть еще одна особенность сессии, которая может быть вам интересна. Начиная с .NET 2.0, хранилище для сессии не инициализируется до первого обращения к ней.[UPDATE] Еще один способ инициализации сессии – добавить метод Session_Start в Global.asax (спасибо Юрию за комментарий).
По этой причине, чтобы показать независимость блокировок от обращений к сессии, создадим страницу InitSession.aspx со следующим скриптом:
<script runat="server">
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
Session["0"] = ((int?)Session["0"] ?? 0) + 1;
Response.Write(Session["0"]);
Response.Write("<br/>");
Response.Write(Session.SessionID);
}
</script>
Медленная страница
Добавим к проекту страницу SlowPage.aspx, в явном виде зададим доступ к сессии только для чтения и напишем обработчик для Load:<%@ Page Language="C#" EnableSessionState="ReadOnly" %>
<script runat="server">
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
System.Threading.Thread.Sleep(30*1000);
}
</script>
Тестовая страница
Добавим к проекту страницу TestPage.aspx и напишем в ней следующий код:protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
Response.Write(Session.SessionID);
}
</script>
Проверка режима ReadOnly
Теперь откроем в IE InitSession.aspx, затем SlowPage.aspx, а в другой вкладке TestPage.aspx. При обновлении по F5 страницы TestPage.aspx (пока грузится SlowPage.aspx) никаких блокировок не происходит.Что произойдет, если перенести установку режима ReadOnly из SlowPage.aspx в TestPage.aspx? Теперь, после открытия SlowPage.aspx, TestPage.aspx будет блокироваться в течении 30 секунд. Это подтверждает сказанное ранее – ReadOnly запросы блокируются обычным запросом поддерживающим сессию.
Проверка обычного режима
Если убрать установку режима ReadOnly из всех страниц, то получим то же поведение, что и в варианте после переноса - блокировку. Таким образом, все правила, озвученные выше, подтверждаются.Разумеется, при отключении поддержки сессии никаких блокировок не происходит (только нужно убрать обращение к SessionID).
Что делать?
Что же делать в ситуации, когда мы хотим использовать сессию, но не хотим блокировать некоторые параллельные операции? Учитывая нечеткую формулировку вопроса (а частных случаев слишком много), вариантов множество, я перечислю только некоторые из них:- Использовать вместо сессии другой механизм хранения сессионных данных (свой сервис).
- Написать свой или использовать сторонние неблокирующие провайдеры сессии.
- Там где сессия только читается использовать режим ReadOnly. С оговорками, упомянутыми выше.
- Отключать сессию для тех операций, которым она не нужна. Об этом подробнее в следующий раз.
Резюме
Хотя блокировка запросов внутри одной сессии ASP.NET, разобранная в этой статье, вполне логична (можно провести аналогию с транзакцией в БД), в официальной документации это поведение описано довольно мутно.Поэтому повторюсь. В рамках одной сессии (одного SessionID) параллельные запросы выполняются по очереди, если обработчики поддерживают сессию. Обращения к сессии при этом не учитываются. Исключение составляет режим ReadOnly, который не блокирует другие запросы, но сам может быть заблокирован.
Если будет время и интерес читателей, напишу еще о сессии ASP.NET – в частности, про нестандартные провайдеры сессии и варианты отладки обращений к сессии. Также расскажу подробности о режиме ReadOnly, который не всегда ReadOnly :)
Если у вас есть замечания, пожелания или новые темы – пишите в комментариях, твиттер или на olegaxenow.reformal.ru. Постараюсь учесть.
Интересно, спасибо
ОтветитьУдалитьНадо обдумать, может вообще не пользоваться сессией? Она не так уж и полезна, по большому счету.
ОтветитьУдалитьTo @xor:
ОтветитьУдалитьВ принципе - это первый вариант ответа на вопрос "Что делать?" выше по тексту.
В принципе-то сессия как общая концепция бывает полезна, а конкретная реализация - не всегда.
После таких статей, я только в очередной раз убеждаюсь, что перейти в ASP.NET MVC из WebForms было правильном решением. Очень многие вещи в WebForms реализованы через жопу. Там кстати эта проблема то же есть, но решается просто заменой обычного контроллера на асинхронный контроллер.
ОтветитьУдалитьвы чо пургу несете, все правильно, несколько запрософ на одну сессию будут обрабатоватся синхронно, но есть еще и threding, так вот он работает нормально - асинхронно.
ОтветитьУдалитьпросто надо понимать что делаете.
Кстати, эту загадочную синхронность можно увидеть в дебагере, когда локалхост откроешь в нескольких браузерах одновременно
Синхронность не загадочная и поведение вполне логично объясняется. Только почему-то не все сходу соображают, почему у клиента файлы не закачиваются параллельно, "а еще неделю назад все работало" :)
ОтветитьУдалитьМеня просто поражает тот факт, что в каждой статье про WebForms в комментариях найдётся человек, который обязательно скажет, что MVC в 100500 раз круче.
ОтветитьУдалитьА за статью спасибо, не знал о таком поведении сессии.
Сам напарывался на это с MVC + long polling. Страница мониторинга вешала всё и сразу :) Ждала разблокировки от триггера базы. Пометил базовый контроллер для ajax запросов как ReadOnly для сессии. Асинхронные контроллеры тоже не всегда выручают http://bit.ly/svLkpB
ОтветитьУдалитьTo @Игорь Чакрыгин и к остальным :)
ОтветитьУдалитьНа всякий случай уточняю - поведение *общее для ASP.NET* как и эта статья. Если что, MVC от MS - тоже ASP.NET ;)
Да, вы правы насчёт статьи. Эта вещь очень давно освещалась в одном стареньком ресурсе: http://odetocode.com/Blogs/scott/archive/2006/05/21/session-state-uses-a-reader-writer-lock.aspx
ОтветитьУдалитьа объяснение простое, так как использование сессии безопасно в потоках, то там просто используются lock'и, которые не освобождаются, пока реквест не обработается.
To @Vest
ОтветитьУдалитьЭта тема и в MSDN достаточно подробно описана, но по факту я вижу, что слишком незаметно. Поэтому, собственно, и написал здесь.
MS давно пора основательно взяться за серверную часть и в идеале сотворить что-то креативно-новое. А то дождутся, что все потихоньку уйдут на php или node.js
ОтветитьУдалитьХотел еще добавить, насколько я помню, сессия инициализируется не только при прямом к ней обращении, т.е. Session[name] = value, но и при наличии в Global.asax метода Session_Start даже если он пустой.
ОтветитьУдалитьНасчёт Сессии, да, Юрий, вы правы, я сталкивался с этим, и забыл просто вспомнить :)
ОтветитьУдалитьА насчёт что-то нового, я думаю ничего уже не поделаешь - серверную часть сильно менять нельзя. Сколько кода написано.
А насчёт пхп - юзайте МВЦ
>Если будет время и интерес читателей, напишу еще о сессии ASP.NET
ОтветитьУдалитьC удовольствием будем читать!
To @Юрий
ОтветитьУдалитьВо-первых, спасибо за комментарий про Session_Start, статью дополнил.
Во-вторых, MVC+Razor я считаю достаточно прогрессивными и если бы не Drupal и иже с ним (Orchard'у и прочим CMS под ASP.NET до них думаю пару лет еще пилить), то эта связка была бы по всем фронтам лучше PHP (если не принимать во внимание более дорогие в среднем цены за хостинг).
Олег, все по делу! Хорошая статья! Ждем еще)))
ОтветитьУдалитьЯ бы с удовольствием почитал про нестандартные провайдеры сессии и варианты отладки.
ОтветитьУдалитьСпасибо за статью!
Хочу сделать акцент на то, что описанная здесь проблема очень актуальна в контексте разработки веб приложений на основе ajax запросов, которые несут с собой куки с id сессии. Да и вообще для любого типа клиента данного сервиса, будь то .NET HttpWebRequest или Java HttpURLConnection.
ОтветитьУдалитьСо временем данная проблема будет только обостряться, т.к. клиенты становятся все более асинхронными, а это вызывает желание выполнять одновременно сразу несколько обращений за данными к сервису.
И как уже было отмечено, это касается всего стека ASP.NET приложений, где включена поддержка сессий: aspx, asmx, ashx, MVC, WCF и т.д. И если не предпринимать специальных мер, то "грабли в темном углу" так и останутся.
Такая реализация сессии в ASP.NET (не важно WebForms или MVC) вполне обоснована. Обращение к глобальным ресурсам должно быть потокобезопасными.
ОтветитьУдалитьЕсли веб станет асинхронным это повлечет много проблем, и большинство программистов просто не поймут концепцию веба.
To @Анонимный
ОтветитьУдалитьТо что обоснована - не вопрос. Просто вещь, как показала практика, не очень очевидная.
Моё личное мнение - лучше чтобы большинство программистов поняли концепцию веба, чем вводить искусственные ограничения ради тех, кто её не хочет понимать ;)
Здравствуйте, Олег.
УдалитьУ меня есть задачка, ну во всяком случае для меня задачка так как уже больше недели над ней сижу.Есть два контроллера, тобиш таблицы в БД, Автор и Книга. По нажатию на кнопку создать автора в появившемся окне есть форма для внесения данных об авторе и ссылка добавить книгу, нажав на которую появляется всплывающее окно в котором ввожу данные книги. После нажатия кнопки “Ок” данные книги не отправляются в базу, а вставляются в разметку сразу под полями ввода данных инфо об авторе. Таким образом можно посредством AJAX добавить несколько книг (массив). Тут трудностей нет. Но трудность появилась вот в чем. После ввода данных автора и отправка всего этого в базу. Для временного хранения я пытался использовать сессию, но данные сессии почему-то null в методе контроллера “Автор”.
Есть две проблемы:
1.Каким образом передать в сессию или в любое другое временное хранилище массив JSON?
2.Ну и соответственно как их считать в методе контроллера для сохранения в БД?
Буду очень признателен если Вы мне поможете в решении данной задачи. Если Вы мне пришлете свой e-mail, я сброшу свой проект. Свой e-mail я отправил в twitter.
Я бы посоветовал накапливать данные на клиенте и отправлять когда всё готово - тогда сессия не понадобится.
УдалитьПередавать можно строкой, на сервере использовать Newtonsoft.Json.
охуенная статья!
ОтветитьУдалитьЗа статью спасибо, помогла в одной проблеме. Но также могу добавить, что при условии обработки двух одновременных запросов с одной сессией сразу после старта приложения (когда срабатывает событие Application_Start) правило не работает. То есть запросы выполняются параллельно :(.
ОтветитьУдалитьБлаго что вероятность такого случая где бы это было критично мала.
А как это обойти? У меня как раз проблема с этим.
Удалитьхм, а как часто апликуха перезапускаеться?
Удалить