C# Language
Асинхронные / ожидающие, фоновые работы, задачи и примеры потоков
Поиск…
замечания
Чтобы запустить любой из этих примеров, просто назовите их так:
static void Main()
{
new Program().ProcessDataAsync();
Console.ReadLine();
}
ASP.NET Configure Await
Когда ASP.NET обрабатывает запрос, поток создается из пула потоков и создается контекст запроса . Контекст запроса содержит информацию о текущем запросе, к которому можно получить доступ через статическое свойство HttpContext.Current
. Затем контекст запроса для запроса присваивается потоку, обрабатывающему запрос.
Конкретный контекст запроса может быть активен только по одному потоку за раз .
Когда выполнение достигает await
, поток, обрабатывающий запрос, возвращается в пул потоков, пока выполняется асинхронный метод, и контекст запроса свободен для использования другого потока.
public async Task<ActionResult> Index()
{
// Execution on the initially assigned thread
var products = await dbContext.Products.ToListAsync();
// Execution resumes on a "random" thread from the pool
// Execution continues using the original request context.
return View(products);
}
Когда задача завершается, пул потоков назначает другой поток для продолжения выполнения запроса. Затем контекст запроса присваивается этому потоку. Это может быть или не быть исходной нитью.
блокировка
Когда результат вызова метода async
синхронно, могут возникнуть взаимоблокировки. Например, следующий код приведет к взаимоблокировке при IndexSync()
:
public async Task<ActionResult> Index()
{
// Execution on the initially assigned thread
List<Product> products = await dbContext.Products.ToListAsync();
// Execution resumes on a "random" thread from the pool
return View(products);
}
public ActionResult IndexSync()
{
Task<ActionResult> task = Index();
// Block waiting for the result synchronously
ActionResult result = Task.Result;
return result;
}
Это связано с тем, что по умолчанию ожидаемая задача, в этом случае db.Products.ToListAsync()
захватит контекст (в случае ASP.NET контекст запроса) и попытается использовать его после его завершения.
Когда весь стек вызовов асинхронен, проблем нет, потому что, как только await
достигнуто, исходный поток освобождается, освобождая контекст запроса.
Когда мы блокируем синхронное использование Task.Result
или Task.Wait()
(или других методов блокировки), исходный поток все еще активен и сохраняет контекст запроса. Ожидаемый метод все еще работает асинхронно, и как только обратный вызов пытается выполнить, т. Е. Как только ожидаемая задача вернулась, он пытается получить контекст запроса.
Поэтому тупик возникает из-за того, что, пока блокирующий поток с контекстом запроса ожидает завершения асинхронной операции, асинхронная операция пытается получить контекст запроса для завершения.
ConfigureAwait
По умолчанию вызовы ожидаемой задачи будут захватывать текущий контекст и пытаться возобновить выполнение в контексте после завершения.
Используя ConfigureAwait(false)
это может быть предотвращено, а взаимоблокировки можно избежать.
public async Task<ActionResult> Index()
{
// Execution on the initially assigned thread
List<Product> products = await dbContext.Products.ToListAsync().ConfigureAwait(false);
// Execution resumes on a "random" thread from the pool without the original request context
return View(products);
}
public ActionResult IndexSync()
{
Task<ActionResult> task = Index();
// Block waiting for the result synchronously
ActionResult result = Task.Result;
return result;
}
Это позволяет избежать блокировок, когда необходимо блокировать асинхронный код, однако это происходит за счет потери контекста в продолжении (код после ожидания вызова).
В ASP.NET это означает, что если ваш код после вызова await someTask.ConfigureAwait(false);
пытается получить доступ к информации из контекста, например HttpContext.Current.User
тогда информация была потеряна. В этом случае HttpContext.Current
имеет значение NULL. Например:
public async Task<ActionResult> Index()
{
// Contains information about the user sending the request
var user = System.Web.HttpContext.Current.User;
using (var client = new HttpClient())
{
await client.GetAsync("http://google.com").ConfigureAwait(false);
}
// Null Reference Exception, Current is null
var user2 = System.Web.HttpContext.Current.User;
return View();
}
Если ConfigureAwait(true)
используется (эквивалентно отсутствию ConfigureAwait), то как user
и user2
заполняются теми же данными.
По этой причине часто рекомендуется использовать ConfigureAwait(false)
в библиотечном коде, где контекст больше не используется.
Асинхронный / Await
Ниже приведен простой пример того, как использовать async / await для выполнения некоторых интенсивных операций времени в фоновом режиме, сохраняя при этом возможность делать некоторые другие вещи, которые не нужно ждать от времени, необходимого для завершения.
Однако, если вам нужно позже работать с результатом интенсивного времени, вы можете сделать это, ожидая выполнения.
public async Task ProcessDataAsync()
{
// Start the time intensive method
Task<int> task = TimeintensiveMethod(@"PATH_TO_SOME_FILE");
// Control returns here before TimeintensiveMethod returns
Console.WriteLine("You can read this while TimeintensiveMethod is still running.");
// Wait for TimeintensiveMethod to complete and get its result
int x = await task;
Console.WriteLine("Count: " + x);
}
private async Task<int> TimeintensiveMethod(object file)
{
Console.WriteLine("Start TimeintensiveMethod.");
// Do some time intensive calculations...
using (StreamReader reader = new StreamReader(file.ToString()))
{
string s = await reader.ReadToEndAsync();
for (int i = 0; i < 10000; i++)
s.GetHashCode();
}
Console.WriteLine("End TimeintensiveMethod.");
// return something as a "result"
return new Random().Next(100);
}
BackgroundWorker
Ниже приведен простой пример использования объекта BackgroundWorker
для выполнения интенсивных операций в фоновом потоке.
Вам нужно:
- Определите рабочий метод, который выполняет трудоемкую работу и вызывается из обработчика события для
DoWork
событияBackgroundWorker
. - Запустите выполнение с помощью
RunWorkerAsync
. Любой аргумент требуется по методе работника , прикрепленной кDoWork
может быть передан в черезDoWorkEventArgs
параметрRunWorkerAsync
.
В дополнение к событию DoWork
класс BackgroundWorker
также определяет два события, которые должны использоваться для взаимодействия с пользовательским интерфейсом. Это необязательно.
- Событие
RunWorkerCompleted
запускается, когда обработчикиDoWork
завершены. - Событие
ProgressChanged
запускается приReportProgress
методаReportProgress
.
public void ProcessDataAsync()
{
// Start the time intensive method
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += BwDoWork;
bw.RunWorkerCompleted += BwRunWorkerCompleted;
bw.RunWorkerAsync(@"PATH_TO_SOME_FILE");
// Control returns here before TimeintensiveMethod returns
Console.WriteLine("You can read this while TimeintensiveMethod is still running.");
}
// Method that will be called after BwDoWork exits
private void BwRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// we can access possible return values of our Method via the Parameter e
Console.WriteLine("Count: " + e.Result);
}
// execution of our time intensive Method
private void BwDoWork(object sender, DoWorkEventArgs e)
{
e.Result = TimeintensiveMethod(e.Argument);
}
private int TimeintensiveMethod(object file)
{
Console.WriteLine("Start TimeintensiveMethod.");
// Do some time intensive calculations...
using (StreamReader reader = new StreamReader(file.ToString()))
{
string s = reader.ReadToEnd();
for (int i = 0; i < 10000; i++)
s.GetHashCode();
}
Console.WriteLine("End TimeintensiveMethod.");
// return something as a "result"
return new Random().Next(100);
}
задача
Ниже приведен простой пример того, как использовать Task
для выполнения некоторого времени в фоновом процессе.
Все, что вам нужно сделать, это обернуть ваш метод интенсивного времени в Task.Run()
.
public void ProcessDataAsync()
{
// Start the time intensive method
Task<int> t = Task.Run(() => TimeintensiveMethod(@"PATH_TO_SOME_FILE"));
// Control returns here before TimeintensiveMethod returns
Console.WriteLine("You can read this while TimeintensiveMethod is still running.");
Console.WriteLine("Count: " + t.Result);
}
private int TimeintensiveMethod(object file)
{
Console.WriteLine("Start TimeintensiveMethod.");
// Do some time intensive calculations...
using (StreamReader reader = new StreamReader(file.ToString()))
{
string s = reader.ReadToEnd();
for (int i = 0; i < 10000; i++)
s.GetHashCode();
}
Console.WriteLine("End TimeintensiveMethod.");
// return something as a "result"
return new Random().Next(100);
}
Нить
Ниже приведен простой пример того, как использовать Thread
для выполнения некоторого времени в фоновом процессе.
public async void ProcessDataAsync()
{
// Start the time intensive method
Thread t = new Thread(TimeintensiveMethod);
// Control returns here before TimeintensiveMethod returns
Console.WriteLine("You can read this while TimeintensiveMethod is still running.");
}
private void TimeintensiveMethod()
{
Console.WriteLine("Start TimeintensiveMethod.");
// Do some time intensive calculations...
using (StreamReader reader = new StreamReader(@"PATH_TO_SOME_FILE"))
{
string v = reader.ReadToEnd();
for (int i = 0; i < 10000; i++)
v.GetHashCode();
}
Console.WriteLine("End TimeintensiveMethod.");
}
Как вы можете видеть, мы не можем вернуть значение из нашего TimeIntensiveMethod
потому что Thread
ожидает, что в качестве параметра будет использоваться метод void.
Чтобы получить возвращаемое значение из Thread
используйте либо событие, либо следующее:
int ret;
Thread t= new Thread(() =>
{
Console.WriteLine("Start TimeintensiveMethod.");
// Do some time intensive calculations...
using (StreamReader reader = new StreamReader(file))
{
string s = reader.ReadToEnd();
for (int i = 0; i < 10000; i++)
s.GetHashCode();
}
Console.WriteLine("End TimeintensiveMethod.");
// return something to demonstrate the coolness of await-async
ret = new Random().Next(100);
});
t.Start();
t.Join(1000);
Console.Writeline("Count: " + ret);
Задание «запустить и забыть»
В некоторых случаях (например, ведение журнала) может оказаться полезным запустить задачу и не ждать результата. Следующее расширение позволяет запустить задачу и продолжить выполнение кода останова:
public static class TaskExtensions
{
public static async void RunAndForget(
this Task task, Action<Exception> onException = null)
{
try
{
await task;
}
catch (Exception ex)
{
onException?.Invoke(ex);
}
}
}
Результат ожидается только внутри метода расширения. Поскольку используется async
/ await
, можно поймать исключение и вызвать необязательный метод его обработки.
Пример использования расширения:
var task = Task.FromResult(0); // Or any other task from e.g. external lib.
task.RunAndForget(
e =>
{
// Something went wrong, handle it.
});