При выполнении асинхронных операций в ASP.NET MVC используйте поток из ThreadPool в .NET 4 - PullRequest
157 голосов
/ 05 января 2012

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

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

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

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

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

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

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

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

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

Редактировать:

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

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

Редактировать:

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

public ViewResult Index() { 

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

    return View();
}

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

В этом случае нужно ли использовать поток из пула потоков?Если так, после того, как это завершится, что случится с этим потоком?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

    }
}

Ответы [ 6 ]

174 голосов
/ 05 января 2012

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

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

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

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

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

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

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

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

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

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

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

ИМХО, мы можем получить больше всего, когда у нас интенсивные операции ввода / вывода (например, вызовы базы данных и сетевые вызовы удаленных служб). Если у вас интенсивная загрузка процессора, асинхронные действия не принесут вам большой пользы.

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

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

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

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

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

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

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

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

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

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

public ViewResult Index() { 

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

    return View();
}

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

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

public ViewResult Index() { 

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

    return View();
}

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

47 голосов
/ 05 января 2012

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

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

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

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

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

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

10 голосов
/ 05 января 2012

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

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();
}

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

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

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

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

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

6 голосов
/ 05 января 2012

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

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

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

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

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

Edit:

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

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

2 голосов
/ 05 января 2012

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

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

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

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

0 голосов
/ 29 июня 2013

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

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

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