Избегайте кэшировать медленные компоненты в объектах Application или Session. Кэшируйте данные на диске веб-сервера

Последнее обновление: 10.03.2016

Одним из ключевых нововведений последних версий фреймворка.NET стала асинхронность. Хотя фреймворк и раньше позволял использовать асинхронные методы, но с появлением библиотеки Task Parallel Library работа с асинхронным кодом была предельно упрощена, а сам формат работы изменился. Были добавлены новые возможности по созданию асинхронных методов с использованием новых ключевых слов, таких как async и await.

При создании нового контроллера мы в настройках уже можем указать, как нам нужен контроллер - синхронный или асинхронный. По умолчанию Visual Studio добавляет в проект стандартные контроллеры, методы которых, как правило, возвращают объект ActionResult. Но если мы при добавлении контроллера в папку Controllers выберем тип MVC 5 Controller with views, using Entity Framework , то в окне настройки нового контроллера специальное поле позволит нам указать, что новый контроллер будет содержать асинхронные методы:

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

Чтобы понять различие между синхронными и асинхронными методами, рассмотрим, как IIS обрабатывает входящие запросы. Веб-сервер поддерживает пул потоков, которые обслуживают запросы. При обращении пользователя к веб-ресурсу IIS выделяет поток из пула для обслуживания данного запроса. И пока данный поток не обработает предназначенный для него запрос, другие запросы он обрабатывать не может.

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

И если обработка запроса блокируется очень долго, то IIS начинает задействовать для обслуживания других входящих запросов новые потоки. Однако есть ограничения на общее количество потоков. Когда количество потоков достигает предела, то вновь входящие запросы помещаются в очередь ожидания. Однако и тут есть ограничение на количество запросов в очереди. И когда это количество превышает предел, то IIS просто отклоняет все остальные запросы с помощью статусного кода 503 (Service Unavailable).

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

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

Сравним на примере вызов синхронного и асинхронного метода:

Using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using AsyncContollers.Models; using System.Threading.Tasks; using System.Data.Entity; namespace AsyncContollers.Controllers { public class HomeController: Controller { BookContext db = new BookContext(); public ActionResult Index() { IEnumerable books = db.Books; ViewBag.Books = books; return View(); } // асинхронный метод public async Task BookList() { IEnumerable books = await db.Books.ToListAsync(); ViewBag.Books = books; return View("Index"); } } }

Оба метода выполняют одну и ту же операцию - извлечение данных из БД и получают идентичные результаты. Но если первый синхронный метод Index представляет привычную для нас запись, то асинхронный метод BookList уже выглядит необычно.

Этот метод возвращает не объект ActionResult, а объект Task . Task представляет асинхронную операцию, выполняющуюся продолжительное время.

Кроме того, чтобы обозначить метод как асинхронный, перед возвращаемым типом ставится ключевое слово async .

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

Но также следует учитывать, что await используется с методами, возвращающими объект Task . Поэтому для получения данных из БД используется метод await db.Books.ToListAsync() , который также извлекает данные из БД, но уже в асинхронном режиме.

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

A thread is defined as the execution path of a program. Each thread defines a unique flow of control. If your application involves complicated and time consuming operations such as database access or some intense I/O operations, then it is often helpful to set different execution paths or threads, with each thread performing a particular job.

Threads are lightweight processes. One common example of use of thread is implementation of concurrent programming by modern operating systems. Use of threads saves wastage of CPU cycle and increases efficiency of an application.

So far we compiled programs where a single thread runs as a single process which is the running instance of the application. However, this way the application can perform one job at a time. To make it execute multiple tasks at a time, it could be divided into smaller threads.

In .Net, the threading is handled through the "System.Threading" namespace. Creating a variable of the System.Threading.Thread type allows you to create a new thread to start working with. It allows you to create and access individual threads in a program.

Creating Thread

A thread is created by creating a Thread object, giving its constructor a ThreadStart reference.

ThreadStart childthreat = new ThreadStart(childthreadcall);

Thread Life Cycle

The life cycle of a thread starts when an object of the System.Threading.Thread class is created and ends when the thread is terminated or completes execution.

Following are the various states in the life cycle of a thread:

    The Unstarted State : It is the situation when the instance of the thread is created but the Start method is not called.

    The Ready State : It is the situation when the thread is ready to execute and waiting CPU cycle.

    The Not Runnable State : a thread is not runnable, when:

    • Sleep method has been called
    • Wait method has been called
    • Blocked by I/O operations
  • The Dead State : It is the situation when the thread has completed execution or has been aborted.

Thread Priority

The Priority property of the Thread class specifies the priority of one thread with respect to other. The .Net runtime selects the ready thread with the highest priority.

The priorities could be categorized as:

  • Above normal
  • Below normal
  • Highest
  • Lowest
  • Normal

Once a thread is created, its priority is set using the Priority property of the thread class.

NewThread.Priority = ThreadPriority.Highest;

Thread Properties & Methods

The Thread class has the following important properties:

Property Description
CurrentContext Gets the current context in which the thread is executing.
CurrentCulture Gets or sets the culture for the current thread.
CurrentPrinciple Gets or sets the thread"s current principal for role-based security.
CurrentThread Gets the currently running thread.
CurrentUICulture Gets or sets the current culture used by the Resource Manager to look up culture-specific resources at run time.
ExecutionContext Gets an ExecutionContext object that contains information about the various contexts of the current thread.
IsAlive Gets a value indicating the execution status of the current thread.
IsBackground Gets or sets a value indicating whether or not a thread is a background thread.
IsThreadPoolThread Gets a value indicating whether or not a thread belongs to the managed thread pool.
ManagedThreadId Gets a unique identifier for the current managed thread.
Name Gets or sets the name of the thread.
Priority Gets or sets a value indicating the scheduling priority of a thread.
ThreadState Gets a value containing the states of the current thread.

The Thread class has the following important methods:

Methods Description
Abort Raises a ThreadAbortException in the thread on which it is invoked, to begin the process of terminating the thread. Calling this method usually terminates the thread.
AllocateDataSlot Allocates an unnamed data slot on all the threads. For better performance, use fields that are marked with the ThreadStaticAttribute attribute instead.
AllocateNamedDataSlot Allocates a named data slot on all threads. For better performance, use fields that are marked with the ThreadStaticAttribute attribute instead.
BeginCriticalRegion Notifies a host that execution is about to enter a region of code in which the effects of a thread abort or unhandled exception might endanger other tasks in the application domain.
BeginThreadAffinity Notifies a host that managed code is about to execute instructions that depend on the identity of the current physical operating system thread.
EndCriticalRegion Notifies a host that execution is about to enter a region of code in which the effects of a thread abort or unhandled exception are limited to the current task.
EndThreadAffinity Notifies a host that managed code has finished executing instructions that depend on the identity of the current physical operating system thread.
FreeNamedDataSlot Eliminates the association between a name and a slot, for all threads in the process. For better performance, use fields that are marked with the ThreadStaticAttribute attribute instead.
GetData Retrieves the value from the specified slot on the current thread, within the current thread"s current domain. For better performance, use fields that are marked with the ThreadStaticAttribute attribute instead.
GetDomain Returns the current domain in which the current thread is running.
GetDomainID Returns a unique application domain identifier.
GetNamedDataSlot Looks up a named data slot. For better performance, use fields that are marked with the ThreadStaticAttribute attribute instead.
Interrupt Interrupts a thread that is in the WaitSleepJoin thread state.
Join Blocks the calling thread until a thread terminates, while continuing to perform standard COM and SendMessage pumping. This method has different overloaded forms.
MemoryBarrier Synchronizes memory access as follows: The processor executing the current thread cannot reorder instructions in such a way that memory accesses prior to the call to MemoryBarrier execute after memory accesses that follow the call to MemoryBarrier.
ResetAbort Cancels an Abort requested for the current thread.
SetData Sets the data in the specified slot on the currently running thread, for that thread"s current domain. For better performance, use fields marked with the ThreadStaticAttribute attribute instead.
Start Starts a thread.
Sleep Makes the thread pause for a period of time.
SpinWait Causes a thread to wait the number of times defined by the iterations parameter.
VolatileRead() Reads the value of a field. The value is the latest written by any processor in a computer, regardless of the number of processors or the state of processor cache. This method has different overloaded forms.
VolatileWrite() Writes a value to a field immediately, so that the value is visible to all processors in the computer. This method has different overloaded forms.
Yield Causes the calling thread to yield execution to another thread that is ready to run on the current processor. The operating system selects the thread to yield to.

Example

The following example illustrates the uses of the Thread class. The page has a label control for displaying messages from the child thread. The messages from the main program are directly displayed using the Response.Write() method. Hence they appear on the top of the page.

The source file is as follows:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="threaddemo._Default" %> Untitled Page

Thread Example

The code behind file is as follows:

Using System; using System.Collections; using System.Configuration; using System.Data; using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Xml.Linq; using System.Threading; namespace threaddemo { public partial class _Default: System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { ThreadStart childthreat = new ThreadStart(childthreadcall); Response.Write("Child Thread Started
"); Thread child = new Thread(childthreat); child.Start(); Response.Write("Main sleeping for 2 seconds.......
"); Thread.Sleep(2000); Response.Write("
Main aborting child thread
"); child.Abort(); } public void childthreadcall() { try{ lblmessage.Text = "
Child thread started
"; lblmessage.Text += "Child Thread: Coiunting to 10"; for(int i =0; i<10; i++) { Thread.Sleep(500); lblmessage.Text += "
in Child thread
"; } lblmessage.Text += "
child thread finished"; }catch(ThreadAbortException e){ lblmessage.Text += "
child thread - exception"; }finally{ lblmessage.Text += "
child thread - unable to catch the exception"; } } } }

Observe the following

    When the page is loaded, a new thread is started with the reference of the method childthreadcall(). The main thread activities are displayed directly on the web page.

    The second thread runs and sends messages to the label control.

    The main thread sleeps for 2000 ms, during which the child thread executes.

    The child thread runs till it is aborted by the main thread. It raises the ThreadAbortException and is terminated.

    Control returns to the main thread.

When executed the program sends the following messages.

Sponsored By

A few years back Phil Haack wrote a great article on the dangers of recurring background tasks in ASP.NET . In it he points out a few gotchas that are SO common when folks try to do work in the background. Read it, but here"s a summary from his post.

  • An unhandled exception in a thread not associated with a request will take down the process.
  • If you run your site in a Web Farm, you could end up with multiple instances of your app that all attempt to run the same task at the same time.
  • The AppDomain your site runs in can go down for a number of reasons and take down your background task with it.

If you think you can just write a background task yourself, it"s likely you"ll get it wrong. I"m not impugning your skills, I"m just saying it"s subtle . Plus, why should you have to?

There"s LOT of great ways for you to do things in the background and a lot of libraries and choices available.

Some ASP.NET apps will be hosted in IIS in your data center and others will be hosted in the Azure cloud. The spectrum of usage is roughly this, in my opinion:

  • General: Hangfire (or similar similar open source libraries)
    • used for writing background tasks in your ASP.NET website
  • Cloud:
    • A formal Azure feature used for offloading running of background tasks outside of your Website and scale the workload
  • Advanced: Azure Worker Role in a Cloud Service
    • scale the background processing workload independently of your Website and you need control over the machine

There"s lots of great articles and videos on how to use , and lots of documentation on how Worker Roles in scalable Azure Cloud Services work, but not a lot about how your hosted ASP.NET application and easily have a background service. Here"s a few.

WebBackgrounder

As it says "WebBackgrounder is a proof-of-concept of a web-farm friendly background task manager meant to just work with a vanilla ASP.NET web application." Its code hasn"t been touched in years, BUT the WebBackgrounder NuGet package has been downloaded almost a half-million times.

The goal of this project is to handle one task only, manage a recurring task on an interval in the background for a web app.

If your ASP.NET application just needs one background task to runs an a basic scheduled interval, than perhaps you just need the basics of WebBackgrounder.

Using System;
using System.Threading;
using System.Threading.Tasks;

namespace WebBackgrounder.DemoWeb
{
public class SampleJob: Job
{
public SampleJob(TimeSpan interval, TimeSpan timeout)
: base("Sample Job", interval, timeout)
{
}

Public override Task Execute()
{
return new Task(() => Thread.Sleep(3000));
}
}
}

Built in: QueueBackgroundWorkItem - Added in .NET 4.5.2

Somewhat in response to the need for WebBackgrounder, .NET 4.5.2 added QueueBackgroundWorkItem as a new API . It"s not just a "Task.Run," it tries to be more:

QBWI schedules a task which can run in the background, independent of any request. This differs from a normal ThreadPool work item in that ASP.NET automatically keeps track of how many work items registered through this API are currently running, and the ASP.NET runtime will try to delay AppDomain shutdown until these work items have finished executing.

It can try to delay an AppDomain for as long as 90 seconds in order to allow your task to complete. If you can"t finish in 90 seconds, then you"ll need a different (and more robust, meaning, out of process) technique.

The API is pretty straightforward, taking Func. Here"s an example that kicks of a background work item from an MVC action:

Public ActionResult SendEmail( User user)
{
if (ModelState.IsValid)
{
HostingEnvironment.QueueBackgroundWorkItem(ct => SendMailAsync(user.Email));
return RedirectToAction("Index", "Home");
}

Return View(user);
}

FluentScheduler

FluentScheduler is a more sophisticated and complex scheduler that features a (you guessed it) fluent interface. You have really explicit control over when your tasks run.

Using FluentScheduler;

public class MyRegistry: Registry
{
public MyRegistry()
{
// Schedule an ITask to run at an interval
Schedule().ToRunNow().AndEvery(2).Seconds();

// Schedule a simple task to run at a specific time
Schedule(() => Console.WriteLine("Timed Task - Will run every day at 9:15pm: " + DateTime.Now)).ToRunEvery(1).Days().At(21, 15);

// Schedule a more complex action to run immediately and on an monthly interval
Schedule(() =>
{
Console.WriteLine("Complex Action Task Starts: " + DateTime.Now);
Thread.Sleep(1000);
Console.WriteLine("Complex Action Task Ends: " + DateTime.Now);
}).ToRunNow().AndEvery(1).Months().OnTheFirst(DayOfWeek.Monday).At(3, 0);
}
}

FluentScheduler also embraces IoC and can easily plug into your favorite Dependency Injection tool of choice by just implementing their ITaskFactory interface.

Quartz.NET

Quartz.NET is a .NET port of the popular Java job scheduling framework of the (almost) same name. It"s very actively developed. Quartz has an IJob interface with just one method, Execute, to implement.

Using Quartz;
using Quartz.Impl;
using System;

namespace ScheduledTaskExample.ScheduledTasks
{
public class JobScheduler
{
public static void Start()
{
IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();
scheduler.Start();

IJobDetail job = JobBuilder.Create().Build();

ITrigger trigger = TriggerBuilder.Create()
.WithDailyTimeIntervalSchedule
(s =>
s.WithIntervalInHours(24)
.OnEveryDay()
.StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(0, 0))
.Build();

Scheduler.ScheduleJob(job, trigger);
}
}
}

Then, inside your Application_Start, you call JobScheduler.Start(). There"s a great getting started article on Quartz at Mikesdotnetting you should check out.

Hangfire

And last but definitely not least, the most polished (IMHO) of the group,