Асинхронные операции в ASP.NET MVC используют поток ThreadPool на .NET 4

После этого вопроса мне становится удобно при использовании async операций в ASP.NET MVC. Итак, я написал два сообщения в блоге:

У меня слишком много недоразумений в моей голове об асинхронных операциях над ASP.NET MVC.

Я всегда слышу это предложение: Приложение может масштабироваться лучше, если операции выполняются асинхронно

И я также слышал такие предложения: , если у вас огромный объем трафика, вам может быть лучше не выполнять ваши запросы асинхронно - потребление 2 дополнительных потоков для обслуживания одного запроса отвлекает ресурсы от другие входящие запросы.

Я думаю, что эти два предложения непоследовательны.

У меня мало информации о том, как работает threadpool на ASP.NET, но я знаю, что threadpool имеет ограниченный размер для потоков. Таким образом, второе предложение должно быть связано с этой проблемой.

И я хотел бы знать, работают ли асинхронные операции в ASP.NET MVC с потоком ThreadPool на .NET 4?

Например, когда мы реализуем AsyncController, как структура приложения? Если я получаю огромный трафик, неплохо ли реализовать AsyncController?

Есть ли кто-нибудь, кто может забрать этот черный занавес перед моими глазами и объяснить мне сделку об асинхронности на ASP.NET MVC 3 (NET 4)?

Edit:

Я прочитал этот ниже документ почти сотни раз, и я понимаю основную сделку, но все же у меня путаница, потому что там слишком много непоследовательных комментариев.

Использование асинхронного контроллера в ASP.NET MVC

Edit:

Предположим, что у меня есть действие контроллера, как показано ниже (не реализация AsyncController хотя):

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

Как вы видите здесь, я запускаю операцию и забываю об этом. Затем я возвращаюсь немедленно, не дожидаясь завершения.

В этом случае нужно ли использовать поток из threadpool? Если это так, после того, как оно завершается, что происходит с этим потоком? Входит ли GC и очищается сразу после его завершения?

Edit:

Для ответа @Darin, вот пример асинхронного кода, который говорит с базой данных:

public class FooController : AsyncController {

    //EF 4.2 DbContext instance
    MyContext _context = new MyContext();

    public void IndexAsync() { 

        AsyncManager.OutstandingOperations.Increment(3);

        Task<IEnumerable<Foo>>.Factory.StartNew(() => { 

           return 
                _context.Foos;
        }).ContinueWith(t => {

            AsyncManager.Parameters["foos"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });

        Task<IEnumerable<Bars>>.Factory.StartNew(() => { 

           return 
                _context.Bars;
        }).ContinueWith(t => {

            AsyncManager.Parameters["bars"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });

        Task<IEnumerable<FooBar>>.Factory.StartNew(() => { 

           return 
                _context.FooBars;
        }).ContinueWith(t => {

            AsyncManager.Parameters["foobars"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });
    }

    public ViewResult IndexCompleted(
        IEnumerable<Foo> foos, 
        IEnumerable<Bar> bars,
        IEnumerable<FooBar> foobars) {

        //Do the regular stuff and return

    }
}

Ответ 1

Здесь отличная статья. Я бы рекомендовал вам прочитать, чтобы лучше понять асинхронную обработку в ASP.NET(что в основном представляют собой асинхронные контроллеры).

Рассмотрим сначала стандартное синхронное действие:

public ActionResult Index()
{
    // some processing
    return View();
}

Когда запрос делается на это действие, поток создается из пула потоков, и тело этого действия выполняется в этом потоке. Поэтому, если обработка внутри этого действия медленная, вы блокируете этот поток для всей обработки, поэтому этот поток нельзя использовать повторно для обработки других запросов. В конце выполнения запроса поток возвращается в пул потоков.

Теперь рассмотрим пример асинхронного шаблона:

public void IndexAsync()
{
    // perform some processing
}

public ActionResult IndexCompleted(object result)
{
    return View();
}

Когда запрос отправляется в действие индекса, поток вытягивается из пула потоков, и выполняется тело метода IndexAsync. Когда тело этого метода завершит выполнение, поток возвращается в пул потоков. Затем, используя стандартный AsyncManager.OutstandingOperations, после того, как вы сообщите о завершении операции async, из пула потоков будет извлечен другой поток, и на нем будет выполняться тело действия IndexCompleted и результат, полученный клиенту.

Итак, что мы видим в этом шаблоне, так это то, что один клиентский HTTP-запрос может выполняться двумя разными потоками.

Теперь интересная часть происходит внутри метода IndexAsync. Если в нем есть операция блокировки, вы полностью теряете всю цель асинхронных контроллеров, потому что вы блокируете рабочий поток (помните, что тело этого действия выполняется в потоке, взятом из пула потоков).

Итак, когда мы можем воспользоваться реальными преимуществами асинхронных контроллеров, вы можете спросить?

IMHO мы можем получить больше всего, когда у нас интенсивные операции ввода-вывода (например, база данных и сетевые вызовы для удаленных служб). Если у вас интенсивная работа с ЦП, асинхронные действия не принесут вам большой пользы.

Итак, почему мы можем извлечь выгоду из интенсивных операций ввода-вывода? Потому что мы могли бы использовать I/O Completion Ports. IOCP чрезвычайно эффективны, потому что вы не потребляете ни одного потока или ресурсов на сервере во время выполнения всей операции.

Как они работают?

Предположим, что мы хотим загрузить содержимое удаленной веб-страницы с помощью метода WebClient.DownloadStringAsync. Вы вызываете этот метод, который регистрирует IOCP в операционной системе и немедленно возвращается. Во время обработки всего запроса ни один поток не потребляется на вашем сервере. Все происходит на удаленном сервере. Это может занять много времени, но вам все равно, поскольку вы не ставите под угрозу свои рабочие потоки. Как только получен ответ, IOCP сигнализируется, поток вытягивается из пула потоков, и обратный вызов выполняется в этом потоке. Но, как вы видите, в течение всего процесса мы не монополизировали нити.

То же самое можно сказать о таких методах, как FileStream.BeginRead, SqlCommand.BeginExecute,...

Как насчет параллелизации нескольких вызовов базы данных? Предположим, что у вас было синхронное действие контроллера, в котором вы последовательно выполняли блокировку вызовов базы данных. Легко подсчитать, что если каждый вызов базы данных занимает 200 мс, действие вашего контроллера займет примерно 800 мс.

Если вам не нужно последовательно выполнять эти вызовы, распараллеливает ли их производительность?

Это большой вопрос, на который ответить непросто. Может быть да, а может быть и нет. Это будет полностью зависеть от того, как вы реализуете эти вызовы базы данных. Если вы используете асинхронные контроллеры и порты завершения ввода-вывода, как обсуждалось ранее, вы повышаете производительность этого действия контроллера и других действий, так как вы не будете монополизировать рабочие потоки.

С другой стороны, если вы их плохо реализуете (при вызове блокирующей базы данных, выполняемом в потоке из пула потоков), вы в основном снижаете общее время выполнения этого действия примерно до 200 мс, но вы бы потребляли 4 рабочих тем, что вы могли бы ухудшить производительность других запросов, которые могут стать голодающими из-за отсутствия потоков в пуле для их обработки.

Так что это очень сложно, и если вы не чувствуете себя готовым к проведению обширных тестов в своем приложении, не используйте асинхронные контроллеры, так как есть вероятность, что вы нанесете больше урона, чем пользы. Внедрите их только в том случае, если у вас есть повод для этого: например, вы определили, что стандартные действия синхронного контроллера являются узким местом для вашего приложения (после выполнения обширных тестов нагрузки и измерений, конечно).

Теперь рассмотрим ваш пример:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

При получении запроса для действия индекса поток выполняется из пула потоков, чтобы выполнить его тело, но его тело только планирует новую задачу, используя TPL. Таким образом, действие завершается, и поток возвращается в пул потоков. Кроме того, TPL использует потоки из пула потоков для выполнения их обработки. Поэтому, даже если исходный поток был возвращен в пул потоков, вы нарисовали еще один поток из этого пула, чтобы выполнить тело задачи. Таким образом, вы поставили под угрозу 2 потока из своего драгоценного пула.

Теперь рассмотрим следующее:

public ViewResult Index() { 

    new Thread(() => { 
        //Do an advanced looging here which takes a while
    }).Start();

    return View();
}

В этом случае мы вручную создаем поток. В этом случае выполнение тела действия индекса может занять немного больше (потому что нереста нового потока дороже, чем рисование одного из существующего пула). Но выполнение расширенной операции каротажа будет выполняться в потоке, который не является частью пула. Поэтому мы не ставим под угрозу потоки из пула, которые остаются свободными для подачи других запросов.

Ответ 2

Да - все потоки из потока-пула. Ваше приложение MVC уже многопоточно, когда запрос поступает в новый поток, будет взят из пула и используется для обслуживания запроса. Этот поток будет "заблокирован" (от других запросов) до тех пор, пока запрос не будет полностью обслуживаться и не будет завершен. Если в пуле нет потока, запрос должен будет ждать, пока он будет доступен.

Если у вас есть асинхронные контроллеры, они по-прежнему получают поток из пула, но при обслуживании запроса они могут отказаться от потока, ожидая чего-то (и этот поток может быть передан другому запросу) и когда исходный запрос требуется поток снова, он получает один из пула.

Разница в том, что если у вас много длительных запросов (где поток ожидает ответа от чего-то), у вас может закончиться поток из пула для обслуживания даже базовых запросов. Если у вас есть асинхронные контроллеры, у вас больше нет потоков, но те, которые ждут, возвращаются в пул и могут обслуживать другие запросы.

A почти пример реальной жизни... Подумайте об этом, например, о том, чтобы добраться до автобуса, там пять человек, ожидающих, чтобы добраться, первый получает, платит и садится (водитель обслуживает их просьбу), вы получаете (водитель обслуживает ваш запрос), но вы не можете найдите свои деньги; когда вы возитесь в карманах, водитель отказывается от вас и получает следующих двух человек (обслуживая их запросы), когда вы находите свои деньги, водитель снова начинает с вами обращаться (заполняя ваш запрос) - пятый человек должен ждать вы закончили, но третий и четвертый люди получили служение, пока вы были на полпути через службу. Это означает, что драйвер является единственным и единственным потоком из пула, и пассажиры являются запросами. Было слишком сложно написать, как это будет работать, если есть два драйвера, но вы можете себе представить...

Без асинхронного контроллера пассажирам, стоящим за вами, придется ждать времени, пока вы ищете свои деньги, между тем водитель автобуса не будет работать.

Таким образом, вывод состоит в том, что, если многие люди не знают, где их деньги (т.е. требуют много времени, чтобы ответить на что-то, что задал драйвер), асинхронные контроллеры могли бы хорошо справляться с пропускной способностью запросов, ускоряя процесс с некоторых, Без контроллера aysnc каждый ждет, пока лицо, на лице, не будет полностью обработано. НО не забывайте, что в MVC у вас много драйверов шины на одной шине, поэтому async не является автоматическим выбором.

Ответ 3

Здесь есть два понятия. Прежде всего, мы можем сделать наш код запущенным параллельно, чтобы выполнить быстрее или запланировать код в другом потоке, чтобы пользователь не подождал. Пример, который у вас был

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

относится ко второй категории. Пользователь получит более быстрый ответ, но общая рабочая нагрузка на сервере выше, потому что он должен выполнять ту же работу + обрабатывать потоки.

Другим примером этого может быть:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Make async web request to twitter with WebClient.DownloadString()
    });

    Task.Factory.StartNew(() => { 
        //Make async web request to facebook with WebClient.DownloadString()
    });


    //wait for both to be ready and merge the results

    return View();
}

Поскольку запросы выполняются параллельно, пользователю не нужно ждать, если они будут выполняться последовательно. Но вы должны понимать, что мы используем больше ресурсов здесь, чем если бы мы запускались в сериале, потому что мы запускаем код во многих потоках, пока у нас тоже есть ожидание потока.

Это совершенно нормально в клиентском сценарии. И довольно распространено там, чтобы обернуть синхронный длинный код в новой задаче (запустить ее в другом потоке), также сохранить отзывчивость или parallize, чтобы сделать его быстрее. Тем не менее поток все еще используется на протяжении всей продолжительности. На сервере с высокой нагрузкой это может иметь неприятные последствия, потому что вы фактически используете больше ресурсов. Это то, о чем люди предупреждали вас о

Асинхронные контроллеры в MVC имеют еще одну цель. Дело здесь в том, чтобы избежать того, чтобы нитки ничем не занимались (что может повредить масштабируемости). Это действительно имеет значение только в том случае, если API, который вы вызываете, имеет асинхронные методы. Как и WebClient.DowloadStringAsync().

Дело в том, что вы можете позволить потоку возвращаться для обработки новых запросов до тех пор, пока веб-запрос не будет завершен, где он вызовет обратный вызов, который получит тот же самый или новый поток и завершит запрос.

Надеюсь, вы понимаете разницу между асинхронным и параллельным. Подумайте о параллельном коде как код, где ваш поток сидит и ждет результата. В то время как асинхронный код - это код, в котором вы будете уведомлены, когда код будет выполнен, и вы можете вернуться к нему, тем временем поток может выполнять другую работу.

Ответ 4

Приложения могут масштабироваться лучше, если операции выполняются асинхронно, но , только если имеются ресурсы для обслуживания дополнительных операций.

Асинхронные операции гарантируют, что вы никогда не блокируете действие, поскольку существующий выполняется. ASP.NET имеет асинхронную модель, которая позволяет нескольким запросам выполнять бок о бок. Можно было бы поставить очередь запросов и обработать их FIFO, но это не будет хорошо масштабироваться, когда у вас есть сотни запросов в очереди, и каждый запрос занимает 100 мс для обработки.

Если у вас огромный объем трафика, вам может быть лучше не выполнять ваши запросы асинхронно, , поскольку для обслуживания запросов не может быть дополнительных ресурсов. Если резервных ресурсов нет, ваши запросы вынуждены стоять в очереди, экспоненциально более длинные или прямые сбои, и в этом случае асинхронные служебные данные (мьютексы и операции переключения контекста) ничего не дают.

Что касается ASP.NET, у вас нет выбора - он использует асинхронную модель, потому что это имеет смысл для модели server-client. Если вы собираетесь писать собственный код внутри, который использует шаблон асинхронизации, чтобы попытаться масштабироваться лучше, если вы не пытаетесь управлять ресурсом, который используется для всех запросов, вы не увидите никаких улучшений, потому что они уже завернуты в асинхронном процессе, который ничего не блокирует.

В конечном счете, все это субъективно, пока вы на самом деле не посмотрите, что вызывает узкое место в вашей системе. Иногда это очевидно, когда асинхронный шаблон поможет (путем предотвращения блокировки в очереди). В конечном итоге только измерение и анализ системы могут указывать, где вы можете повысить эффективность.

Edit:

В вашем примере вызов Task.Factory.StartNew остановит операцию в пуле потоков .NET. Характер потоков потоков потоков должен быть повторно использован (чтобы избежать затрат на создание/уничтожение множества потоков). По завершении операции поток возвращается в пул, который будет повторно использоваться другим запросом (сборщик мусора фактически не участвует, если вы не создали некоторые объекты в своих операциях, и в этом случае они собираются в соответствии с нормальным обзорный).

Что касается ASP.NET, здесь нет специальной операции. Запрос ASP.NET завершается без учета асинхронной задачи. Единственное беспокойство может быть, если ваш пул потоков насыщен (т.е. нет доступных потоков для обслуживания запроса прямо сейчас, а настройки пула не позволяют создавать больше потоков), и в этом случае запрос блокируется ожидания для запуска задачи до тех пор, пока поток пула не станет доступен.

Ответ 5

Да, они используют поток из пула потоков. На самом деле есть довольно отличное руководство от MSDN, которое будет решать все ваши вопросы и многое другое. Я нашел, что это было очень полезно в прошлом. Проверьте это!

http://msdn.microsoft.com/en-us/library/ee728598.aspx

Между тем комментарии + предложения, которые вы слышите об асинхронном коде, должны приниматься с солью. Во-первых, просто сделать что-то асинхронное не обязательно делает его масштабирование лучше, а в некоторых случаях может ухудшить масштаб приложения. Другой комментарий, который вы опубликовали о "огромном объеме трафика...", также является правильным только в определенных контекстах. Это действительно зависит от того, что делают ваши операции, и от того, как они взаимодействуют с другими частями системы.

Короче говоря, у многих людей есть много мнений об асинхронном режиме, но они могут быть неверными из контекста. Я бы сказал, сосредоточиться на ваших точных проблемах и провести базовое тестирование производительности, чтобы увидеть, какие контроллеры асинхронного и т.д. Действительно обрабатывают приложение .

Ответ 6

Во-первых, это не MVC, а IIS, который поддерживает пул потоков. Таким образом, любой запрос, который приходит в приложение MVC или ASP.NET, обслуживается из потоков, которые поддерживаются в пуле потоков. Только при создании приложения Asynch он вызывает это действие в другом потоке и немедленно выпускает поток, чтобы можно было выполнить другие запросы.

Я объяснил то же самое с подробным видео (http://www.youtube.com/watch?v=wvg13n5V0V0/ "MVC Asynch controller and thread starvation" ), который показывает, как происходит голодное истощение в MVC и как его минимизировать с помощью MVC Asynch контроллеров. Я также измерил очереди запросов с использованием perfmon, чтобы вы могли видеть, как очереди запросов уменьшаются для асинхронного MVC и как его худшее для операций Synch.